package chacha20

import "core:crypto/util"
import "core:math/bits"
import "core:mem"

KEY_SIZE :: 32
NONCE_SIZE :: 12
XNONCE_SIZE :: 24

@(private)
_MAX_CTR_IETF :: 0xffffffff

@(private)
_BLOCK_SIZE :: 64
@(private)
_STATE_SIZE_U32 :: 16
@(private)
_ROUNDS :: 20

@(private)
_SIGMA_0 : u32 : 0x61707865
@(private)
_SIGMA_1 : u32 : 0x3320646e
@(private)
_SIGMA_2 : u32 : 0x79622d32
@(private)
_SIGMA_3 : u32 : 0x6b206574

Context :: struct {
	_s: [_STATE_SIZE_U32]u32,

	_buffer: [_BLOCK_SIZE]byte,
	_off: int,

	_is_ietf_flavor: bool,
	_is_initialized: bool,
}

init :: proc (ctx: ^Context, key, nonce: []byte) {
	if len(key) != KEY_SIZE {
		panic("crypto/chacha20: invalid ChaCha20 key size")
	}
	if n_len := len(nonce); n_len != NONCE_SIZE && n_len != XNONCE_SIZE {
		panic("crypto/chacha20: invalid (X)ChaCha20 nonce size")
	}

	k, n := key, nonce

	// Derive the XChaCha20 subkey and sub-nonce via HChaCha20.
	is_xchacha := len(nonce) == XNONCE_SIZE
	if is_xchacha {
		sub_key := ctx._buffer[:KEY_SIZE]
		_hchacha20(sub_key, k, n)
		k = sub_key
		n = n[16:24]
	}

	ctx._s[0] = _SIGMA_0
	ctx._s[1] = _SIGMA_1
	ctx._s[2] = _SIGMA_2
	ctx._s[3] = _SIGMA_3
	ctx._s[4] = util.U32_LE(k[0:4])
	ctx._s[5] = util.U32_LE(k[4:8])
	ctx._s[6] = util.U32_LE(k[8:12])
	ctx._s[7] = util.U32_LE(k[12:16])
	ctx._s[8] = util.U32_LE(k[16:20])
	ctx._s[9] = util.U32_LE(k[20:24])
	ctx._s[10] = util.U32_LE(k[24:28])
	ctx._s[11] = util.U32_LE(k[28:32])
	ctx._s[12] = 0
	if !is_xchacha {
		ctx._s[13] = util.U32_LE(n[0:4])
		ctx._s[14] = util.U32_LE(n[4:8])
		ctx._s[15] = util.U32_LE(n[8:12])
	} else {
		ctx._s[13] = 0
		ctx._s[14] = util.U32_LE(n[0:4])
		ctx._s[15] = util.U32_LE(n[4:8])

		// The sub-key is stored in the keystream buffer.  While
		// this will be overwritten in most circumstances, explicitly
		// clear it out early.
		mem.zero_explicit(&ctx._buffer, KEY_SIZE)
	}

	ctx._off = _BLOCK_SIZE
	ctx._is_ietf_flavor = !is_xchacha
	ctx._is_initialized = true
}

seek :: proc (ctx: ^Context, block_nr: u64) {
	assert(ctx._is_initialized)

	if ctx._is_ietf_flavor {
		if block_nr > _MAX_CTR_IETF {
			panic("crypto/chacha20: attempted to seek past maximum counter")
		}
	} else {
		ctx._s[13] = u32(block_nr >> 32)
	}
	ctx._s[12] = u32(block_nr)
	ctx._off = _BLOCK_SIZE
}

xor_bytes :: proc (ctx: ^Context, dst, src: []byte) {
	assert(ctx._is_initialized)

	// TODO: Enforcing that dst and src alias exactly or not at all
	// is a good idea, though odd aliasing should be extremely uncommon.

	src, dst := src, dst
	if dst_len := len(dst); dst_len < len(src) {
		src = src[:dst_len]
	}

	for remaining := len(src); remaining > 0; {
		// Process multiple blocks at once
		if ctx._off == _BLOCK_SIZE {
			if nr_blocks := remaining / _BLOCK_SIZE; nr_blocks > 0 {
				direct_bytes := nr_blocks * _BLOCK_SIZE
				_do_blocks(ctx, dst, src, nr_blocks)
				remaining -= direct_bytes
				if remaining == 0 {
					return
				}
				dst = dst[direct_bytes:]
				src = src[direct_bytes:]
			}

			// If there is a partial block, generate and buffer 1 block
			// worth of keystream.
			_do_blocks(ctx, ctx._buffer[:], nil, 1)
			ctx._off = 0
		}

		// Process partial blocks from the buffered keystream.
		to_xor := min(_BLOCK_SIZE - ctx._off, remaining)
		buffered_keystream := ctx._buffer[ctx._off:]
		for i := 0; i < to_xor; i = i + 1 {
			dst[i] = buffered_keystream[i] ~ src[i]
		}
		ctx._off += to_xor
		dst = dst[to_xor:]
		src = src[to_xor:]
		remaining -= to_xor
	}
}

keystream_bytes :: proc (ctx: ^Context, dst: []byte) {
	assert(ctx._is_initialized)

	dst := dst
	for remaining := len(dst); remaining > 0; {
		// Process multiple blocks at once
		if ctx._off == _BLOCK_SIZE {
			if nr_blocks := remaining / _BLOCK_SIZE; nr_blocks > 0 {
				direct_bytes := nr_blocks * _BLOCK_SIZE
				_do_blocks(ctx, dst, nil, nr_blocks)
				remaining -= direct_bytes
				if remaining == 0 {
					return
				}
				dst = dst[direct_bytes:]
			}

			// If there is a partial block, generate and buffer 1 block
			// worth of keystream.
			_do_blocks(ctx, ctx._buffer[:], nil, 1)
			ctx._off = 0
		}

		// Process partial blocks from the buffered keystream.
		to_copy := min(_BLOCK_SIZE - ctx._off, remaining)
		buffered_keystream := ctx._buffer[ctx._off:]
		copy(dst[:to_copy], buffered_keystream[:to_copy])
		ctx._off += to_copy
		dst = dst[to_copy:]
		remaining -= to_copy
	}
}

reset :: proc (ctx: ^Context) {
	mem.zero_explicit(&ctx._s, size_of(ctx._s))
	mem.zero_explicit(&ctx._buffer, size_of(ctx._buffer))

	ctx._is_initialized = false
}

@(private)
_do_blocks :: proc (ctx: ^Context, dst, src: []byte, nr_blocks: int) {
	// Enforce the maximum consumed keystream per nonce.
	//
	// While all modern "standard" definitions of ChaCha20 use
	// the IETF 32-bit counter, for XChaCha20 most common
	// implementations allow for a 64-bit counter.
	//
	// Honestly, the answer here is "use a MRAE primitive", but
	// go with common practice in the case of XChaCha20.
	if ctx._is_ietf_flavor {
		if u64(ctx._s[12]) + u64(nr_blocks) > 0xffffffff {
			panic("crypto/chacha20: maximum ChaCha20 keystream per nonce reached")
		}
	} else {
		ctr := (u64(ctx._s[13]) << 32) | u64(ctx._s[12])
		if _, carry := bits.add_u64(ctr, u64(nr_blocks), 0); carry != 0 {
			panic("crypto/chacha20: maximum XChaCha20 keystream per nonce reached")
		}
	}

	dst, src := dst, src
	x := &ctx._s
	for n := 0; n < nr_blocks; n = n + 1 {
		x0, x1, x2, x3 := _SIGMA_0, _SIGMA_1, _SIGMA_2, _SIGMA_3
		x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15 := x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[11], x[12], x[13], x[14], x[15]

		for i := _ROUNDS; i > 0; i = i - 2 {
			// Even when forcing inlining manually inlining all of
			// these is decently faster.

			// quarterround(x, 0, 4, 8, 12)
			x0 += x4
			x12 ~= x0
			x12 = util.ROTL32(x12, 16)
			x8 += x12
			x4 ~= x8
			x4 = util.ROTL32(x4, 12)
			x0 += x4
			x12 ~= x0
			x12 = util.ROTL32(x12, 8)
			x8 += x12
			x4 ~= x8
			x4 = util.ROTL32(x4, 7)

			// quarterround(x, 1, 5, 9, 13)
			x1 += x5
			x13 ~= x1
			x13 = util.ROTL32(x13, 16)
			x9 += x13
			x5 ~= x9
			x5 = util.ROTL32(x5, 12)
			x1 += x5
			x13 ~= x1
			x13 = util.ROTL32(x13, 8)
			x9 += x13
			x5 ~= x9
			x5 = util.ROTL32(x5, 7)

			// quarterround(x, 2, 6, 10, 14)
			x2 += x6
			x14 ~= x2
			x14 = util.ROTL32(x14, 16)
			x10 += x14
			x6 ~= x10
			x6 = util.ROTL32(x6, 12)
			x2 += x6
			x14 ~= x2
			x14 = util.ROTL32(x14, 8)
			x10 += x14
			x6 ~= x10
			x6 = util.ROTL32(x6, 7)

			// quarterround(x, 3, 7, 11, 15)
			x3 += x7
			x15 ~= x3
			x15 = util.ROTL32(x15, 16)
			x11 += x15
			x7 ~= x11
			x7 = util.ROTL32(x7, 12)
			x3 += x7
			x15 ~= x3
			x15 = util.ROTL32(x15, 8)
			x11 += x15
			x7 ~= x11
			x7 = util.ROTL32(x7, 7)

			// quarterround(x, 0, 5, 10, 15)
			x0 += x5
			x15 ~= x0
			x15 = util.ROTL32(x15, 16)
			x10 += x15
			x5 ~= x10
			x5 = util.ROTL32(x5, 12)
			x0 += x5
			x15 ~= x0
			x15 = util.ROTL32(x15, 8)
			x10 += x15
			x5 ~= x10
			x5 = util.ROTL32(x5, 7)

			// quarterround(x, 1, 6, 11, 12)
			x1 += x6
			x12 ~= x1
			x12 = util.ROTL32(x12, 16)
			x11 += x12
			x6 ~= x11
			x6 = util.ROTL32(x6, 12)
			x1 += x6
			x12 ~= x1
			x12 = util.ROTL32(x12, 8)
			x11 += x12
			x6 ~= x11
			x6 = util.ROTL32(x6, 7)

			// quarterround(x, 2, 7, 8, 13)
			x2 += x7
			x13 ~= x2
			x13 = util.ROTL32(x13, 16)
			x8 += x13
			x7 ~= x8
			x7 = util.ROTL32(x7, 12)
			x2 += x7
			x13 ~= x2
			x13 = util.ROTL32(x13, 8)
			x8 += x13
			x7 ~= x8
			x7 = util.ROTL32(x7, 7)

			// quarterround(x, 3, 4, 9, 14)
			x3 += x4
			x14 ~= x3
			x14 = util.ROTL32(x14, 16)
			x9 += x14
			x4 ~= x9
			x4 = util.ROTL32(x4, 12)
			x3 += x4
			x14 ~= x3
			x14 = util.ROTL32(x14, 8)
			x9 += x14
			x4 ~= x9
			x4 = util.ROTL32(x4, 7)
		}

		x0 += _SIGMA_0
		x1 += _SIGMA_1
		x2 += _SIGMA_2
		x3 += _SIGMA_3
		x4 += x[4]
		x5 += x[5]
		x6 += x[6]
		x7 += x[7]
		x8 += x[8]
		x9 += x[9]
		x10 += x[10]
		x11 += x[11]
		x12 += x[12]
		x13 += x[13]
		x14 += x[14]
		x15 += x[15]

		// While the "correct" answer to getting more performance out of
		// this is "use vector operations", support for that is currently
		// a work in progress/to be designed.
		//
		// Until dedicated assembly can be written leverage the fact that
		// the callers of this routine ensure that src/dst are valid.

		when ODIN_ARCH == .i386 || ODIN_ARCH == .amd64 {
			// util.PUT_U32_LE/util.U32_LE are not required on little-endian
			// systems that also happen to not be strict about aligned
			// memory access.

			dst_p := transmute(^[16]u32)(&dst[0])
			if src != nil {
				src_p := transmute(^[16]u32)(&src[0])
				dst_p[0] = src_p[0] ~ x0
				dst_p[1] = src_p[1] ~ x1
				dst_p[2] = src_p[2] ~ x2
				dst_p[3] = src_p[3] ~ x3
				dst_p[4] = src_p[4] ~ x4
				dst_p[5] = src_p[5] ~ x5
				dst_p[6] = src_p[6] ~ x6
				dst_p[7] = src_p[7] ~ x7
				dst_p[8] = src_p[8] ~ x8
				dst_p[9] = src_p[9] ~ x9
				dst_p[10] = src_p[10] ~ x10
				dst_p[11] = src_p[11] ~ x11
				dst_p[12] = src_p[12] ~ x12
				dst_p[13] = src_p[13] ~ x13
				dst_p[14] = src_p[14] ~ x14
				dst_p[15] = src_p[15] ~ x15
				src = src[_BLOCK_SIZE:]
			} else {
				dst_p[0] = x0
				dst_p[1] = x1
				dst_p[2] = x2
				dst_p[3] = x3
				dst_p[4] = x4
				dst_p[5] = x5
				dst_p[6] = x6
				dst_p[7] = x7
				dst_p[8] = x8
				dst_p[9] = x9
				dst_p[10] = x10
				dst_p[11] = x11
				dst_p[12] = x12
				dst_p[13] = x13
				dst_p[14] = x14
				dst_p[15] = x15
			}
			dst = dst[_BLOCK_SIZE:]
		} else {
			#no_bounds_check {
				if src != nil {
					util.PUT_U32_LE(dst[0:4], util.U32_LE(src[0:4]) ~ x0)
					util.PUT_U32_LE(dst[4:8], util.U32_LE(src[4:8]) ~ x1)
					util.PUT_U32_LE(dst[8:12], util.U32_LE(src[8:12]) ~ x2)
					util.PUT_U32_LE(dst[12:16], util.U32_LE(src[12:16]) ~ x3)
					util.PUT_U32_LE(dst[16:20], util.U32_LE(src[16:20]) ~ x4)
					util.PUT_U32_LE(dst[20:24], util.U32_LE(src[20:24]) ~ x5)
					util.PUT_U32_LE(dst[24:28], util.U32_LE(src[24:28]) ~ x6)
					util.PUT_U32_LE(dst[28:32], util.U32_LE(src[28:32]) ~ x7)
					util.PUT_U32_LE(dst[32:36], util.U32_LE(src[32:36]) ~ x8)
					util.PUT_U32_LE(dst[36:40], util.U32_LE(src[36:40]) ~ x9)
					util.PUT_U32_LE(dst[40:44], util.U32_LE(src[40:44]) ~ x10)
					util.PUT_U32_LE(dst[44:48], util.U32_LE(src[44:48]) ~ x11)
					util.PUT_U32_LE(dst[48:52], util.U32_LE(src[48:52]) ~ x12)
					util.PUT_U32_LE(dst[52:56], util.U32_LE(src[52:56]) ~ x13)
					util.PUT_U32_LE(dst[56:60], util.U32_LE(src[56:60]) ~ x14)
					util.PUT_U32_LE(dst[60:64], util.U32_LE(src[60:64]) ~ x15)
					src = src[_BLOCK_SIZE:]
				} else {
					util.PUT_U32_LE(dst[0:4], x0)
					util.PUT_U32_LE(dst[4:8], x1)
					util.PUT_U32_LE(dst[8:12], x2)
					util.PUT_U32_LE(dst[12:16], x3)
					util.PUT_U32_LE(dst[16:20], x4)
					util.PUT_U32_LE(dst[20:24], x5)
					util.PUT_U32_LE(dst[24:28], x6)
					util.PUT_U32_LE(dst[28:32], x7)
					util.PUT_U32_LE(dst[32:36], x8)
					util.PUT_U32_LE(dst[36:40], x9)
					util.PUT_U32_LE(dst[40:44], x10)
					util.PUT_U32_LE(dst[44:48], x11)
					util.PUT_U32_LE(dst[48:52], x12)
					util.PUT_U32_LE(dst[52:56], x13)
					util.PUT_U32_LE(dst[56:60], x14)
					util.PUT_U32_LE(dst[60:64], x15)
				}
				dst = dst[_BLOCK_SIZE:]
			}
		}

		// Increment the counter.  Overflow checking is done upon
		// entry into the routine, so a 64-bit increment safely
		// covers both cases.
		new_ctr := ((u64(ctx._s[13]) << 32) | u64(ctx._s[12])) + 1
		x[12] = u32(new_ctr)
		x[13] = u32(new_ctr >> 32)
	}
}

@(private)
_hchacha20 :: proc (dst, key, nonce: []byte) {
	x0, x1, x2, x3 := _SIGMA_0, _SIGMA_1, _SIGMA_2, _SIGMA_3
	x4 := util.U32_LE(key[0:4])
	x5 := util.U32_LE(key[4:8])
	x6 := util.U32_LE(key[8:12])
	x7 := util.U32_LE(key[12:16])
	x8 := util.U32_LE(key[16:20])
	x9 := util.U32_LE(key[20:24])
	x10 := util.U32_LE(key[24:28])
	x11 := util.U32_LE(key[28:32])
	x12 := util.U32_LE(nonce[0:4])
	x13 := util.U32_LE(nonce[4:8])
	x14 := util.U32_LE(nonce[8:12])
	x15 := util.U32_LE(nonce[12:16])

	for i := _ROUNDS; i > 0; i = i - 2 {
		// quarterround(x, 0, 4, 8, 12)
		x0 += x4
		x12 ~= x0
		x12 = util.ROTL32(x12, 16)
		x8 += x12
		x4 ~= x8
		x4 = util.ROTL32(x4, 12)
		x0 += x4
		x12 ~= x0
		x12 = util.ROTL32(x12, 8)
		x8 += x12
		x4 ~= x8
		x4 = util.ROTL32(x4, 7)

		// quarterround(x, 1, 5, 9, 13)
		x1 += x5
		x13 ~= x1
		x13 = util.ROTL32(x13, 16)
		x9 += x13
		x5 ~= x9
		x5 = util.ROTL32(x5, 12)
		x1 += x5
		x13 ~= x1
		x13 = util.ROTL32(x13, 8)
		x9 += x13
		x5 ~= x9
		x5 = util.ROTL32(x5, 7)

		// quarterround(x, 2, 6, 10, 14)
		x2 += x6
		x14 ~= x2
		x14 = util.ROTL32(x14, 16)
		x10 += x14
		x6 ~= x10
		x6 = util.ROTL32(x6, 12)
		x2 += x6
		x14 ~= x2
		x14 = util.ROTL32(x14, 8)
		x10 += x14
		x6 ~= x10
		x6 = util.ROTL32(x6, 7)

		// quarterround(x, 3, 7, 11, 15)
		x3 += x7
		x15 ~= x3
		x15 = util.ROTL32(x15, 16)
		x11 += x15
		x7 ~= x11
		x7 = util.ROTL32(x7, 12)
		x3 += x7
		x15 ~= x3
		x15 = util.ROTL32(x15, 8)
		x11 += x15
		x7 ~= x11
		x7 = util.ROTL32(x7, 7)

		// quarterround(x, 0, 5, 10, 15)
		x0 += x5
		x15 ~= x0
		x15 = util.ROTL32(x15, 16)
		x10 += x15
		x5 ~= x10
		x5 = util.ROTL32(x5, 12)
		x0 += x5
		x15 ~= x0
		x15 = util.ROTL32(x15, 8)
		x10 += x15
		x5 ~= x10
		x5 = util.ROTL32(x5, 7)

		// quarterround(x, 1, 6, 11, 12)
		x1 += x6
		x12 ~= x1
		x12 = util.ROTL32(x12, 16)
		x11 += x12
		x6 ~= x11
		x6 = util.ROTL32(x6, 12)
		x1 += x6
		x12 ~= x1
		x12 = util.ROTL32(x12, 8)
		x11 += x12
		x6 ~= x11
		x6 = util.ROTL32(x6, 7)

		// quarterround(x, 2, 7, 8, 13)
		x2 += x7
		x13 ~= x2
		x13 = util.ROTL32(x13, 16)
		x8 += x13
		x7 ~= x8
		x7 = util.ROTL32(x7, 12)
		x2 += x7
		x13 ~= x2
		x13 = util.ROTL32(x13, 8)
		x8 += x13
		x7 ~= x8
		x7 = util.ROTL32(x7, 7)

		// quarterround(x, 3, 4, 9, 14)
		x3 += x4
		x14 ~= x3
		x14 = util.ROTL32(x14, 16)
		x9 += x14
		x4 ~= x9
		x4 = util.ROTL32(x4, 12)
		x3 += x4
		x14 ~= x3
		x14 = util.ROTL32(x14, 8)
		x9 += x14
		x4 ~= x9
		x4 = util.ROTL32(x4, 7)
	}

	util.PUT_U32_LE(dst[0:4], x0)
	util.PUT_U32_LE(dst[4:8], x1)
	util.PUT_U32_LE(dst[8:12], x2)
	util.PUT_U32_LE(dst[12:16], x3)
	util.PUT_U32_LE(dst[16:20], x12)
	util.PUT_U32_LE(dst[20:24], x13)
	util.PUT_U32_LE(dst[24:28], x14)
	util.PUT_U32_LE(dst[28:32], x15)
}
