package deoxysii

import "base:intrinsics"
import "core:crypto"
import aes "core:crypto/_aes/ct64"
import "core:encoding/endian"
import "core:simd"

// This uses the bitlsiced 64-bit general purpose register SWAR AES
// round function.  The encryption pass skips orthogonalizing the
// AES round function input as it is aways going to be the leading 0
// padded IV, and doing a 64-byte copy is faster.

@(private = "file")
TWEAK_SIZE :: 16

@(private = "file")
State_SW :: struct {
	ctx:        ^Context,
	q_stk, q_b: [8]u64,
}

@(private = "file")
auth_tweak :: #force_inline proc "contextless" (
	dst: ^[TWEAK_SIZE]byte,
	prefix: byte,
	block_nr: int,
) {
	endian.unchecked_put_u64be(dst[8:], u64(block_nr))
	endian.unchecked_put_u64le(dst[0:], u64(prefix) << PREFIX_SHIFT) // dst[0] = prefix << PREFIX_SHIFT
}

@(private = "file")
enc_tweak :: #force_inline proc "contextless" (
	dst: ^[TWEAK_SIZE]byte,
	tag: ^[TAG_SIZE]byte,
	block_nr: int,
) {
	tmp: [8]byte
	endian.unchecked_put_u64be(tmp[:], u64(block_nr))

	copy(dst[:], tag[:])
	dst[0] |= 0x80
	for i in 0 ..< 8 {
		dst[i+8] ~= tmp[i]
	}
}

@(private = "file")
enc_plaintext :: #force_inline proc "contextless" (
	dst: ^[8]u64,
	iv:  []byte,
) {
	tmp: [BLOCK_SIZE]byte = ---
	tmp[0] = 0
	copy(tmp[1:], iv[:])

	q_0, q_1 := aes.load_interleaved(tmp[:])
	for i in 0 ..< 4 {
		dst[i], dst[i+4] = q_0, q_1
	}
	aes.orthogonalize(dst)
}

@(private = "file")
bc_x4 :: proc "contextless" (
	ctx:     ^Context,
	dst:     []byte,
	tweaks:  ^[4][TWEAK_SIZE]byte,
	q_stk:   ^[8]u64,
	q_b:     ^[8]u64, // Orthogonalized
	n:       int,
) {
	tk1s: [4]simd.u8x16
	for j in 0 ..< n {
		tk1s[j] = intrinsics.unaligned_load((^simd.u8x16)(&tweaks[j]))
	}

	// Deoxys-BC-384
	for i in 0 ..= BC_ROUNDS {
		// Derive the round's subtweakkey
		sk := intrinsics.unaligned_load((^simd.u8x16)(&ctx._subkeys[i]))
		for j in 0 ..< n {
			if i != 0 {
				tk1s[j] = h(tk1s[j])
			}
			intrinsics.unaligned_store(
				(^simd.u8x16)(raw_data(dst)),
				simd.bit_xor(sk, tk1s[j]),
			)
			q_stk[j], q_stk[j+4] = aes.load_interleaved(dst[:])
		}
		aes.orthogonalize(q_stk)

		if i != 0 {
			aes.sub_bytes(q_b)
			aes.shift_rows(q_b)
			aes.mix_columns(q_b)
		}
		aes.add_round_key(q_b, q_stk[:])
	}

	aes.orthogonalize(q_b)
	for i in 0 ..< n {
		aes.store_interleaved(dst[i*BLOCK_SIZE:], q_b[i], q_b[i+4])
	}
}

@(private = "file", require_results)
bc_absorb :: proc "contextless" (
	st:           ^State_SW,
	dst:          []byte,
	src:          []byte,
	tweak_prefix: byte,
	stk_block_nr: int,
) -> int {
	tweaks: [4][TWEAK_SIZE]byte = ---
	tmp: [BLOCK_SIZE*4]byte = ---

	src, stk_block_nr := src, stk_block_nr
	dst_ := intrinsics.unaligned_load((^simd.u8x16)(raw_data(dst)))

	nr_blocks := len(src) / BLOCK_SIZE
	for nr_blocks > 0 {
		// Derive the tweak(s), orthogonalize the plaintext
		n := min(nr_blocks, 4)
		for i in 0 ..< n {
			auth_tweak(&tweaks[i], tweak_prefix, stk_block_nr + i)
			st.q_b[i], st.q_b[i + 4] = aes.load_interleaved(src)
			src = src[BLOCK_SIZE:]
		}
		aes.orthogonalize(&st.q_b)

		// Deoxys-BC-384
		bc_x4(st.ctx, tmp[:], &tweaks, &st.q_stk, &st.q_b, n)

		// XOR in the existing Auth/tag
		for i in 0 ..< n {
			dst_ = simd.bit_xor(
				dst_,
				intrinsics.unaligned_load((^simd.u8x16)(raw_data(tmp[i*BLOCK_SIZE:]))),
			)
		}

		stk_block_nr += n
		nr_blocks -= n
	}

	intrinsics.unaligned_store((^simd.u8x16)(raw_data(dst)), dst_)

	crypto.zero_explicit(&tweaks, size_of(tweaks))
	crypto.zero_explicit(&tmp, size_of(tmp))

	return stk_block_nr
}

@(private = "file")
bc_final :: proc "contextless" (
	st:  ^State_SW,
	dst: []byte,
	iv:  []byte,
) {
	tweaks: [4][TWEAK_SIZE]byte = ---

	tweaks[0][0] = PREFIX_TAG << PREFIX_SHIFT
	copy(tweaks[0][1:], iv)

	st.q_b[0], st.q_b[4] = aes.load_interleaved(dst)
	aes.orthogonalize(&st.q_b)

	bc_x4(st.ctx, dst, &tweaks, &st.q_stk, &st.q_b, 1)
}

@(private = "file", require_results)
bc_encrypt :: proc "contextless" (
	st:           ^State_SW,
	dst:          []byte,
	src:          []byte,
	q_n:          ^[8]u64, // Orthogonalized
	tweak_tag:    ^[TAG_SIZE]byte,
	stk_block_nr: int,
) -> int {
	tweaks: [4][TWEAK_SIZE]byte = ---
	tmp: [BLOCK_SIZE*4]byte = ---

	dst, src, stk_block_nr := dst, src, stk_block_nr

	nr_blocks := len(src) / BLOCK_SIZE
	for nr_blocks > 0 {
		// Derive the tweak(s)
		n := min(nr_blocks, 4)
		for i in 0 ..< n {
			enc_tweak(&tweaks[i], tweak_tag, stk_block_nr + i)
		}
		st.q_b = q_n^ // The plaintext is always `0^8 || N`

		// Deoxys-BC-384
		bc_x4(st.ctx, tmp[:], &tweaks, &st.q_stk, &st.q_b, n)

		// XOR the ciphertext
		for i in 0 ..< n {
			intrinsics.unaligned_store(
				(^simd.u8x16)(raw_data(dst[i*BLOCK_SIZE:])),
				simd.bit_xor(
					intrinsics.unaligned_load((^simd.u8x16)(raw_data(src[i*BLOCK_SIZE:]))),
					intrinsics.unaligned_load((^simd.u8x16)(raw_data(tmp[i*BLOCK_SIZE:]))),
				),
			)
		}

		dst, src = dst[n*BLOCK_SIZE:], src[n*BLOCK_SIZE:]
		stk_block_nr += n
		nr_blocks -= n
	}

	crypto.zero_explicit(&tweaks, size_of(tweaks))
	crypto.zero_explicit(&tmp, size_of(tmp))

	return stk_block_nr
}

@(private)
e_ref :: proc "contextless" (ctx: ^Context, dst, tag, iv, aad, plaintext: []byte) #no_bounds_check {
	st: State_SW = ---
	st.ctx = ctx

	// Algorithm 3
	//
	// Associated data
	// A_1 || ... || A_la || A_∗ <- A where each |A_i| = n and |A_∗| < n
	// Auth <- 0^n
	// for i = 0 to la − 1 do
	//   Auth <- Auth ^ EK(0010 || i, A_i+1)
	// end
	// if A_∗ != nil then
	//   Auth <- Auth ^ EK(0110 || la, pad10∗(A_∗))
	// end
	auth: [TAG_SIZE]byte
	aad := aad
	n := bc_absorb(&st, auth[:], aad, PREFIX_AD_BLOCK, 0)
	aad = aad[n*BLOCK_SIZE:]
	if l := len(aad); l > 0 {
		a_star: [BLOCK_SIZE]byte

		copy(a_star[:], aad)
		a_star[l] = 0x80

		_ = bc_absorb(&st, auth[:], a_star[:], PREFIX_AD_FINAL, n)
	}

	// Message authentication and tag generation
	// M_1 || ... || M_l || M_∗ <- M where each |M_j| = n and |M_∗| < n
	// tag <- Auth
	// for j = 0 to l − 1 do
	//   tag <- tag ^ EK(0000 || j, M_j+1)
	// end
	// if M_∗ != nil then
	//   tag <- tag ^ EK(0100 || l, pad10∗(M_∗))
	// end
	// tag <- EK(0001 || 0^4 || N, tag)
	m := plaintext
	n = bc_absorb(&st, auth[:], m, PREFIX_MSG_BLOCK, 0)
	m = m[n*BLOCK_SIZE:]
	if l := len(m); l > 0 {
		m_star: [BLOCK_SIZE]byte

		copy(m_star[:], m)
		m_star[l] = 0x80

		_ = bc_absorb(&st, auth[:], m_star[:], PREFIX_MSG_FINAL, n)
	}
	bc_final(&st, auth[:], iv)

	// Message encryption
	// for j = 0 to l − 1 do
	//   C_j <- M_j ^ EK(1 || tag ^ j, 0^8 || N)
	// end
	// if M_∗ != nil then
	//   C_∗ <- M_* ^ EK(1 || tag ^ l, 0^8 || N)
	// end
	//
	// return (C_1 || ... || C_l || C_∗, tag)
	q_iv: [8]u64 = ---
	enc_plaintext(&q_iv, iv)

	m = plaintext
	n = bc_encrypt(&st, dst, m, &q_iv, &auth, 0)
	m = m[n*BLOCK_SIZE:]
	if l := len(m); l > 0 {
		m_star: [BLOCK_SIZE]byte

		copy(m_star[:], m)
		_ = bc_encrypt(&st, m_star[:], m_star[:], &q_iv, &auth, n)

		copy(dst[n*BLOCK_SIZE:], m_star[:])

		crypto.zero_explicit(&m_star, size_of(m_star))
	}

	copy(tag, auth[:])

	crypto.zero_explicit(&st.q_stk, size_of(st.q_stk))
	crypto.zero_explicit(&st.q_b, size_of(st.q_b))
}

@(private, require_results)
d_ref :: proc "contextless" (ctx: ^Context, dst, iv, aad, ciphertext, tag: []byte) -> bool {
	st: State_SW = ---
	st.ctx = ctx

	// Algorithm 4
	//
	// Message decryption
	// C_1 || ... || C_l || C_∗ <- C where each |C_j| = n and |C_∗| < n
	// for j = 0 to l − 1 do
	//   M_j <- C_j ^ EK(1 || tag ^ j, 0^8 || N)
	// end
	// if C_∗ != nil then
	//   M_∗ <- C_∗ ^ EK(1 || tag ^ l, 0^8 || N)
	// end
	q_iv: [8]u64 = ---
	enc_plaintext(&q_iv, iv)

	auth: [TAG_SIZE]byte
	copy(auth[:], tag)

	m := ciphertext
	n := bc_encrypt(&st, dst, m, &q_iv, &auth, 0)
	m = m[n*BLOCK_SIZE:]
	if l := len(m); l > 0 {
		m_star: [BLOCK_SIZE]byte

		copy(m_star[:], m)
		_ = bc_encrypt(&st, m_star[:], m_star[:], &q_iv, &auth, n)

		copy(dst[n*BLOCK_SIZE:], m_star[:])

		crypto.zero_explicit(&m_star, size_of(m_star))
	}

	// Associated data
	// A_1 || ... || Al_a || A_∗ <- A where each |Ai_| = n and |A_∗| < n
	// Auth <- 0
	// for i = 0 to la − 1 do
	//   Auth <- Auth ^ EK(0010 || i, A_i+1)
	// end
	// if A∗ != nil then
	//   Auth <- Auth ^ EK(0110| | l_a, pad10∗(A_∗))
	// end
	auth = 0
	aad := aad
	n = bc_absorb(&st, auth[:], aad, PREFIX_AD_BLOCK, 0)
	aad = aad[n*BLOCK_SIZE:]
	if l := len(aad); l > 0 {
		a_star: [BLOCK_SIZE]byte

		copy(a_star[:], aad)
		a_star[l] = 0x80

		_ = bc_absorb(&st, auth[:], a_star[:], PREFIX_AD_FINAL, n)
	}

	// Message authentication and tag generation
	// M_1 || ... || M_l || M_∗ <- M where each |M_j| = n and |M_∗| < n
	// tag0 <- Auth
	// for j = 0 to l − 1 do
	//   tag0 <- tag0 ^ EK(0000 || j, M_j+1)
	// end
	// if M_∗ != nil then
	//   tag0 <- tag0 ^ EK(0100 || l, pad10∗(M_∗))
	// end
	// tag0 <- EK(0001 || 0^4 || N, tag0)
	m = dst[:len(ciphertext)]
	n = bc_absorb(&st, auth[:], m, PREFIX_MSG_BLOCK, 0)
	m = m[n*BLOCK_SIZE:]
	if l := len(m); l > 0 {
		m_star: [BLOCK_SIZE]byte

		copy(m_star[:], m)
		m_star[l] = 0x80

		_ = bc_absorb(&st, auth[:], m_star[:], PREFIX_MSG_FINAL, n)

		crypto.zero_explicit(&m_star, size_of(m_star))
	}
	bc_final(&st, auth[:], iv)

	// Tag verification
	// if tag0 = tag then return (M_1 || ... || M_l || M_∗)
	// else return false
	ok := crypto.compare_constant_time(auth[:], tag) == 1

	crypto.zero_explicit(&auth, size_of(auth))
	crypto.zero_explicit(&st.q_stk, size_of(st.q_stk))
	crypto.zero_explicit(&st.q_b, size_of(st.q_b))

	return ok
}