package aead

import "core:crypto/aegis"
import "core:crypto/aes"
import "core:crypto/chacha20"
import "core:crypto/chacha20poly1305"
import "core:crypto/deoxysii"
import "core:reflect"

// Implementation is an AEAD implementation.  Most callers will not need
// to use this as the package will automatically select the most performant
// implementation available.
Implementation :: union {
	aes.Implementation,
	chacha20.Implementation,
}

// MAX_TAG_SIZE is the maximum size tag that can be returned by any of the
// Algorithms supported via this package.
MAX_TAG_SIZE :: 32

// Algorithm is the algorithm identifier associated with a given Context.
Algorithm :: enum {
	Invalid,
	AES_GCM_128,
	AES_GCM_192,
	AES_GCM_256,
	CHACHA20POLY1305,
	XCHACHA20POLY1305,
	AEGIS_128L,
	AEGIS_128L_256, // AEGIS-128L (256-bit tag)
	AEGIS_256,
	AEGIS_256_256, // AEGIS-256 (256-bit tag)
	DEOXYS_II_256,
}

// ALGORITM_NAMES is the Algorithm to algorithm name string.
ALGORITHM_NAMES := [Algorithm]string {
	.Invalid           = "Invalid",
	.AES_GCM_128       = "AES-GCM-128",
	.AES_GCM_192       = "AES-GCM-192",
	.AES_GCM_256       = "AES-GCM-256",
	.CHACHA20POLY1305  = "chacha20poly1305",
	.XCHACHA20POLY1305 = "xchacha20poly1305",
	.AEGIS_128L        = "AEGIS-128L",
	.AEGIS_128L_256    = "AEGIS-128L-256",
	.AEGIS_256         = "AEGIS-256",
	.AEGIS_256_256     = "AEGIS-256-256",
	.DEOXYS_II_256     = "Deoxys-II-256",
}

// TAG_SIZES is the Algorithm to tag size in bytes.
TAG_SIZES := [Algorithm]int {
	.Invalid           = 0,
	.AES_GCM_128       = aes.GCM_TAG_SIZE,
	.AES_GCM_192       = aes.GCM_TAG_SIZE,
	.AES_GCM_256       = aes.GCM_TAG_SIZE,
	.CHACHA20POLY1305  = chacha20poly1305.TAG_SIZE,
	.XCHACHA20POLY1305 = chacha20poly1305.TAG_SIZE,
	.AEGIS_128L        = aegis.TAG_SIZE_128,
	.AEGIS_128L_256    = aegis.TAG_SIZE_256,
	.AEGIS_256         = aegis.TAG_SIZE_128,
	.AEGIS_256_256     = aegis.TAG_SIZE_256,
	.DEOXYS_II_256     = deoxysii.TAG_SIZE,
}

// KEY_SIZES is the Algorithm to key size in bytes.
KEY_SIZES := [Algorithm]int {
	.Invalid           = 0,
	.AES_GCM_128       = aes.KEY_SIZE_128,
	.AES_GCM_192       = aes.KEY_SIZE_192,
	.AES_GCM_256       = aes.KEY_SIZE_256,
	.CHACHA20POLY1305  = chacha20poly1305.KEY_SIZE,
	.XCHACHA20POLY1305 = chacha20poly1305.KEY_SIZE,
	.AEGIS_128L        = aegis.KEY_SIZE_128L,
	.AEGIS_128L_256    = aegis.KEY_SIZE_128L,
	.AEGIS_256         = aegis.KEY_SIZE_256,
	.AEGIS_256_256     = aegis.KEY_SIZE_256,
	.DEOXYS_II_256     = deoxysii.KEY_SIZE,
}

// IV_SIZES is the Algorithm to initialization vector size in bytes.
//
// Note: Some algorithms (such as AES-GCM) support variable IV sizes.
IV_SIZES := [Algorithm]int {
	.Invalid           = 0,
	.AES_GCM_128       = aes.GCM_IV_SIZE,
	.AES_GCM_192       = aes.GCM_IV_SIZE,
	.AES_GCM_256       = aes.GCM_IV_SIZE,
	.CHACHA20POLY1305  = chacha20poly1305.IV_SIZE,
	.XCHACHA20POLY1305 = chacha20poly1305.XIV_SIZE,
	.AEGIS_128L        = aegis.IV_SIZE_128L,
	.AEGIS_128L_256    = aegis.IV_SIZE_128L,
	.AEGIS_256         = aegis.IV_SIZE_256,
	.AEGIS_256_256     = aegis.IV_SIZE_256,
	.DEOXYS_II_256     = deoxysii.IV_SIZE,
}

// Context is a concrete instantiation of a specific AEAD algorithm.
Context :: struct {
	_algo: Algorithm,
	_impl: union {
		aes.Context_GCM,
		chacha20poly1305.Context,
		aegis.Context,
		deoxysii.Context,
	},
}

@(private)
_IMPL_IDS := [Algorithm]typeid {
	.Invalid           = nil,
	.AES_GCM_128       = typeid_of(aes.Context_GCM),
	.AES_GCM_192       = typeid_of(aes.Context_GCM),
	.AES_GCM_256       = typeid_of(aes.Context_GCM),
	.CHACHA20POLY1305  = typeid_of(chacha20poly1305.Context),
	.XCHACHA20POLY1305 = typeid_of(chacha20poly1305.Context),
	.AEGIS_128L        = typeid_of(aegis.Context),
	.AEGIS_128L_256    = typeid_of(aegis.Context),
	.AEGIS_256         = typeid_of(aegis.Context),
	.AEGIS_256_256     = typeid_of(aegis.Context),
	.DEOXYS_II_256     = typeid_of(deoxysii.Context),
}

// init initializes a Context with a specific AEAD Algorithm.
init :: proc(ctx: ^Context, algorithm: Algorithm, key: []byte, impl: Implementation = nil) {
	if ctx._impl != nil {
		reset(ctx)
	}

	ensure(len(key) == KEY_SIZES[algorithm], "crypto/aead: invalid key size")

	// Directly specialize the union by setting the type ID (save a copy).
	reflect.set_union_variant_typeid(
		ctx._impl,
		_IMPL_IDS[algorithm],
	)
	switch algorithm {
	case .AES_GCM_128, .AES_GCM_192, .AES_GCM_256:
		impl_ := impl != nil ? impl.(aes.Implementation) : aes.DEFAULT_IMPLEMENTATION
		aes.init_gcm(&ctx._impl.(aes.Context_GCM), key, impl_)
	case .CHACHA20POLY1305:
		impl_ := impl != nil ? impl.(chacha20.Implementation) : chacha20.DEFAULT_IMPLEMENTATION
		chacha20poly1305.init(&ctx._impl.(chacha20poly1305.Context), key, impl_)
	case .XCHACHA20POLY1305:
		impl_ := impl != nil ? impl.(chacha20.Implementation) : chacha20.DEFAULT_IMPLEMENTATION
		chacha20poly1305.init_xchacha(&ctx._impl.(chacha20poly1305.Context), key, impl_)
	case .AEGIS_128L, .AEGIS_128L_256, .AEGIS_256, .AEGIS_256_256:
		impl_ := impl != nil ? impl.(aes.Implementation) : aes.DEFAULT_IMPLEMENTATION
		aegis.init(&ctx._impl.(aegis.Context), key, impl_)
	case .DEOXYS_II_256:
		impl_ := impl != nil ? impl.(aes.Implementation) : aes.DEFAULT_IMPLEMENTATION
		deoxysii.init(&ctx._impl.(deoxysii.Context), key, impl_)
	case .Invalid:
		panic("crypto/aead: uninitialized algorithm")
	case:
		panic("crypto/aead: invalid algorithm")
	}

	ctx._algo = algorithm
}

// seal_ctx encrypts the plaintext and authenticates the aad and ciphertext,
// with the provided Context and iv, stores the output in dst and tag.
//
// dst and plaintext MUST alias exactly or not at all.
seal_ctx :: proc(ctx: ^Context, dst, tag, iv, aad, plaintext: []byte) {
	ensure(len(tag) == TAG_SIZES[ctx._algo], "crypto/aead: invalid tag size")

	switch &impl in ctx._impl {
	case aes.Context_GCM:
		aes.seal_gcm(&impl, dst, tag, iv, aad, plaintext)
	case chacha20poly1305.Context:
		chacha20poly1305.seal(&impl, dst, tag, iv, aad, plaintext)
	case aegis.Context:
		aegis.seal(&impl, dst, tag, iv, aad, plaintext)
	case deoxysii.Context:
		deoxysii.seal(&impl, dst, tag, iv, aad, plaintext)
	case:
		panic("crypto/aead: uninitialized algorithm")
	}
}

// open_ctx authenticates the aad and ciphertext, and decrypts the ciphertext,
// with the provided Context, iv, and tag, and stores the output in dst,
// returning true if and only if (⟺) the authentication was successful.  If authentication
// fails, the destination buffer will be zeroed.
//
// dst and plaintext MUST alias exactly or not at all.
@(require_results)
open_ctx :: proc(ctx: ^Context, dst, iv, aad, ciphertext, tag: []byte) -> bool {
	ensure(len(tag) == TAG_SIZES[ctx._algo], "crypto/aead: invalid tag size")

	switch &impl in ctx._impl {
	case aes.Context_GCM:
		return aes.open_gcm(&impl, dst, iv, aad, ciphertext, tag)
	case chacha20poly1305.Context:
		return chacha20poly1305.open(&impl, dst, iv, aad, ciphertext, tag)
	case aegis.Context:
		return aegis.open(&impl, dst, iv, aad, ciphertext, tag)
	case deoxysii.Context:
		return deoxysii.open(&impl, dst, iv, aad, ciphertext, tag)
	case:
		panic("crypto/aead: uninitialized algorithm")
	}
}

// reset sanitizes the Context.  The Context must be re-initialized to
// be used again.
reset :: proc(ctx: ^Context) {
	switch &impl in ctx._impl {
	case aes.Context_GCM:
		aes.reset_gcm(&impl)
	case chacha20poly1305.Context:
		chacha20poly1305.reset(&impl)
	case aegis.Context:
		aegis.reset(&impl)
	case deoxysii.Context:
		deoxysii.reset(&impl)
	case:
		// Calling reset repeatedly is fine.
	}

	ctx._algo = .Invalid
	ctx._impl = nil
}

// algorithm returns the Algorithm used by a Context instance.
algorithm :: proc(ctx: ^Context) -> Algorithm {
	return ctx._algo
}

// iv_size returns the IV size of a Context instance in bytes.
iv_size :: proc(ctx: ^Context) -> int {
	return IV_SIZES[ctx._algo]
}

// tag_size returns the tag size of a Context instance in bytes.
tag_size :: proc(ctx: ^Context) -> int {
	return TAG_SIZES[ctx._algo]
}
