// Package frodo640shake implements the variant FrodoKEM-640 with SHAKE.
package frodo640shake

import (
	"bytes"
	cryptoRand "crypto/rand"
	"crypto/subtle"
	"io"

	"github.com/cloudflare/circl/internal/sha3"
	"github.com/cloudflare/circl/kem"
)

const (
	paramN = 640

	// Denoted by 'mbar' in the FrodoKEM spec.
	paramNbar = 8

	logQ       = 15
	logQMask   = ((1 << logQ) - 1)
	seedASize  = 16
	pkHashSize = 16

	// Denoted by 'B' in the FrodoKEM spec.
	extractedBits = 2

	messageSize        = 16
	matrixBpPackedSize = (logQ * (paramN * paramNbar)) / 8
)

const (
	// Size of seed for NewKeyFromSeed.
	// = len(s) + len(seedSE) + len(z).
	KeySeedSize = SharedKeySize + SharedKeySize + 16

	// Size of seed for EncapsulateTo.
	EncapsulationSeedSize = 16

	// Size of the established shared key.
	SharedKeySize = 16

	// Size of the encapsulated shared key.
	CiphertextSize = 9720

	// Size of a packed public key.
	PublicKeySize = 9616

	// Size of a packed private key.
	PrivateKeySize = 19888
)

// Multi-dimensional arrays are stored in 1-dimensional arrays in
// row-major order.
type (
	nByNU16       [paramN * paramN]uint16
	nByNbarU16    [paramN * paramNbar]uint16
	nbarByNU16    [paramNbar * paramN]uint16
	nbarByNbarU16 [paramNbar * paramNbar]uint16
)

// Type of a FrodoKEM-640-SHAKE public key
type PublicKey struct {
	seedA   [seedASize]byte
	matrixB nByNbarU16
}

// Type of a FrodoKEM-640-SHAKE private key
type PrivateKey struct {
	hashInputIfDecapsFail [SharedKeySize]byte
	pk                    *PublicKey

	// matrixS stores transpose(S)
	matrixS nByNbarU16

	// H(packed(pk))
	hpk [pkHashSize]byte
}

// NewKeyFromSeed derives a public/private keypair deterministically
// from the given seed.
//
// Panics if seed is not of length KeySeedSize.
func newKeyFromSeed(seed []byte) (*PublicKey, *PrivateKey) {
	if len(seed) != KeySeedSize {
		panic("seed must be of length KeySeedSize")
	}

	var sk PrivateKey
	var pk PublicKey

	var E nByNbarU16
	var byteSE [2 * (len(sk.matrixS) + len(E))]byte

	var A nByNU16

	// Generate the secret value s, and the seed for S, E, and A. Add seedA to the public key
	shake128 := sha3.NewShake128()
	_, _ = shake128.Write(seed[2*SharedKeySize:])
	_, _ = shake128.Read(pk.seedA[:])

	shake128.Reset()
	_, _ = shake128.Write([]byte{0x5F})
	_, _ = shake128.Write(seed[SharedKeySize : 2*SharedKeySize])
	_, _ = shake128.Read(byteSE[:])

	i := 0
	for i < len(sk.matrixS) {
		sk.matrixS[i] = uint16(byteSE[i*2]) | (uint16(byteSE[(i*2)+1]) << 8)
		i++
	}
	sample(sk.matrixS[:])

	for j := range E {
		E[j] = uint16(byteSE[i*2]) | (uint16(byteSE[(i*2)+1]) << 8)
		i++
	}
	sample(E[:])

	expandSeedIntoA(&A, &pk.seedA, &shake128)
	mulAddASPlusE(&pk.matrixB, &A, &sk.matrixS, &E)

	// Populate the private key
	copy(sk.hashInputIfDecapsFail[:], seed[0:SharedKeySize])
	sk.pk = &pk

	// Add H(pk) to the private key
	shake128.Reset()
	var ppk [PublicKeySize]byte
	pk.Pack(ppk[:])
	_, _ = shake128.Write(ppk[:])
	_, _ = shake128.Read(sk.hpk[:])

	return &pk, &sk
}

// GenerateKeyPair generates public and private keys using entropy from rand.
// If rand is nil, crypto/rand.Reader will be used.
func generateKeyPair(rand io.Reader) (*PublicKey, *PrivateKey, error) {
	var seed [KeySeedSize]byte
	if rand == nil {
		rand = cryptoRand.Reader
	}
	_, err := io.ReadFull(rand, seed[:])
	if err != nil {
		return nil, nil, err
	}
	pk, sk := newKeyFromSeed(seed[:])
	return pk, sk, err
}

// EncapsulateTo generates a shared key and a ciphertext containing said key
// from the public key and the randomness from seed and writes the shared key
// to ss and ciphertext to ct.
//
// Panics if ss, ct, or seed are not of length SharedKeySize, CiphertextSize
// and EncapsulationSeedSize respectively.
//
// seed may be nil, in which case crypto/rand.Reader is used to generate one.
func (pk *PublicKey) EncapsulateTo(ct []byte, ss []byte, seed []byte) {
	if seed == nil {
		seed = make([]byte, EncapsulationSeedSize)
		_, _ = cryptoRand.Read(seed[:])
	}
	if len(seed) != EncapsulationSeedSize {
		panic("seed must be of length EncapsulationSeedSize")
	}
	if len(ct) != CiphertextSize {
		panic("ct must be of length CiphertextSize")
	}
	if len(ss) != SharedKeySize {
		panic("ss must be of length SharedKeySize")
	}

	var G2out [2 * SharedKeySize]byte

	var SpEpEpp [(paramN * paramNbar) + (paramN * paramNbar) + (paramNbar * paramNbar)]uint16
	var byteSpEpEpp [2 * len(SpEpEpp)]byte
	Sp := SpEpEpp[:paramN*paramNbar]
	Ep := SpEpEpp[paramN*paramNbar : 2*paramN*paramNbar]
	Epp := SpEpEpp[2*paramN*paramNbar:]

	var Bp nbarByNU16

	var V nbarByNbarU16
	var C nbarByNbarU16

	var A nByNU16

	var hpk [pkHashSize]byte

	var mu [messageSize]byte
	copy(mu[:], seed[:messageSize])

	// compute hpk = G_1(packed(pk))
	shake128 := sha3.NewShake128()
	var ppk [PublicKeySize]byte
	pk.Pack(ppk[:])
	_, _ = shake128.Write(ppk[:])
	_, _ = shake128.Read(hpk[:])

	// compute (seedSE || k) = G_2(hpk || mu)
	shake128.Reset()
	_, _ = shake128.Write(hpk[:])
	_, _ = shake128.Write(mu[:])
	_, _ = shake128.Read(G2out[:])

	// Generate Sp, Ep, Epp, and A, and compute:
	// Bp = Sp*A + Ep
	// V = Sp*B + Epp
	shake128.Reset()
	_, _ = shake128.Write([]byte{0x96})
	_, _ = shake128.Write(G2out[:SharedKeySize])
	_, _ = shake128.Read(byteSpEpEpp[:])
	for i := range SpEpEpp {
		SpEpEpp[i] = uint16(byteSpEpEpp[i*2]) | (uint16(byteSpEpEpp[(i*2)+1]) << 8)
	}
	sample(SpEpEpp[:])

	expandSeedIntoA(&A, &pk.seedA, &shake128)
	mulAddSAPlusE(&Bp, Sp, &A, Ep)

	mulAddSBPlusE(&V, Sp, &pk.matrixB, Epp)

	// Encode mu, and compute C = V + enc(mu) (mod q)
	encodeMessage(&C, &mu)
	add(&C, &V, &C)

	// Prepare the ciphertext
	pack(ct[:matrixBpPackedSize], Bp[:])
	pack(ct[matrixBpPackedSize:], C[:])

	// Compute ss = F(ct||k)
	shake128.Reset()
	_, _ = shake128.Write(ct[:])
	_, _ = shake128.Write(G2out[SharedKeySize:])
	_, _ = shake128.Read(ss[:])
}

// DecapsulateTo computes the shared key that is encapsulated in ct
// from the private key.
//
// Panics if ct or ss are not of length CiphertextSize and SharedKeySize
// respectively.
func (sk *PrivateKey) DecapsulateTo(ss, ct []byte) {
	if len(ct) != CiphertextSize {
		panic("ct must be of length CiphertextSize")
	}
	if len(ss) != SharedKeySize {
		panic("ss must be of length SharedKeySize")
	}

	var Bp nbarByNU16
	var C nbarByNbarU16

	var W nbarByNbarU16
	var CC nbarByNbarU16
	var BBp nbarByNU16

	var SpEpEpp [(paramN * paramNbar) + (paramN * paramNbar) + (paramNbar * paramNbar)]uint16
	var byteSpEpEpp [2 * len(SpEpEpp)]byte
	Sp := SpEpEpp[:paramN*paramNbar]
	Ep := SpEpEpp[paramN*paramNbar : 2*paramN*paramNbar]
	Epp := SpEpEpp[2*paramN*paramNbar:]

	var A nByNU16

	var muprime [messageSize]byte
	var G2out [2 * SharedKeySize]byte

	kprime := G2out[SharedKeySize:]

	// Compute W = C - Bp*S (mod q), and decode the randomness mu
	unpack(Bp[:], ct[0:matrixBpPackedSize])
	unpack(C[:], ct[matrixBpPackedSize:])
	mulBS(&W, &Bp, &sk.matrixS)
	sub(&W, &C, &W)

	decodeMessage(&muprime, &W)

	// Generate (seedSE' || k') = G_2(hpk || mu')
	shake128 := sha3.NewShake128()
	_, _ = shake128.Write(sk.hpk[:])
	_, _ = shake128.Write(muprime[:])
	_, _ = shake128.Read(G2out[:])

	// Generate Sp, Ep, Epp, A, and compute BBp = Sp*A + Ep.
	shake128.Reset()
	_, _ = shake128.Write([]byte{0x96})
	_, _ = shake128.Write(G2out[:SharedKeySize])
	_, _ = shake128.Read(byteSpEpEpp[:])
	for i := range SpEpEpp {
		SpEpEpp[i] = uint16(byteSpEpEpp[i*2]) | (uint16(byteSpEpEpp[(i*2)+1]) << 8)
	}

	sample(SpEpEpp[:])

	expandSeedIntoA(&A, &sk.pk.seedA, &shake128)
	mulAddSAPlusE(&BBp, Sp[:], &A, Ep[:])

	// Reduce BBp modulo q
	for i := range BBp {
		BBp[i] = BBp[i] & logQMask
	}

	// compute W = Sp*B + Epp
	mulAddSBPlusE(&W, Sp, &sk.pk.matrixB, Epp)

	// Encode mu, and compute CC = W + enc(mu') (mod q)
	encodeMessage(&CC, &muprime)
	add(&CC, &W, &CC)

	// Prepare input to F

	// If (Bp == BBp & C == CC) then ss = F(ct || k'), else ss = F(ct || s)
	// Needs to avoid branching on secret data as per:
	//     Qian Guo, Thomas Johansson, Alexander Nilsson. A key-recovery timing attack on post-quantum
	//     primitives using the Fujisaki-Okamoto transformation and its application on FrodoKEM. In CRYPTO 2020.
	selector := ctCompareU16(Bp[:], BBp[:]) | ctCompareU16(C[:], CC[:])
	// If (selector == 0) then load k' to do ss = F(ct || k'), else if (selector == 1) load s to do ss = F(ct || s)
	subtle.ConstantTimeCopy(selector, kprime[:], sk.hashInputIfDecapsFail[:])

	shake128.Reset()
	_, _ = shake128.Write(ct[:])
	_, _ = shake128.Write(kprime[:])
	_, _ = shake128.Read(ss[:])
}

// Packs sk to buf.
//
// Panics if buf is not of size PrivateKeySize.
func (sk *PrivateKey) Pack(buf []byte) {
	if len(buf) != PrivateKeySize {
		panic("buf must be of length PrivateKeySize")
	}

	copy(buf[:SharedKeySize], sk.hashInputIfDecapsFail[:])
	buf = buf[SharedKeySize:]

	sk.pk.Pack(buf[:PublicKeySize])
	buf = buf[PublicKeySize:]

	j := 0
	for i := range sk.matrixS {
		buf[j] = byte(sk.matrixS[i])
		buf[j+1] = byte(sk.matrixS[i] >> 8)
		j += 2
	}
	buf = buf[j:]

	copy(buf[:], sk.hpk[:])
}

// Unpacks sk from buf.
//
// Panics if buf is not of size PrivateKeySize.
func (sk *PrivateKey) Unpack(buf []byte) {
	if len(buf) != PrivateKeySize {
		panic("buf must be of length PrivateKeySize")
	}

	copy(sk.hashInputIfDecapsFail[:], buf[:SharedKeySize])
	buf = buf[SharedKeySize:]

	sk.pk = new(PublicKey)
	sk.pk.Unpack(buf[:PublicKeySize])
	buf = buf[PublicKeySize:]

	for i := range sk.matrixS {
		sk.matrixS[i] = uint16(buf[i*2]) | (uint16(buf[(i*2)+1]) << 8)
	}
	buf = buf[len(sk.matrixS)*2:]

	copy(sk.hpk[:], buf[:])
}

// Packs pk to buf.
//
// Panics if buf is not of size PublicKeySize.
func (pk *PublicKey) Pack(buf []byte) {
	if len(buf) != PublicKeySize {
		panic("buf must be of length PublicKeySize")
	}

	copy(buf[:seedASize], pk.seedA[:])
	pack(buf[seedASize:], pk.matrixB[:])
}

// TODO: Unpacks pk from buf.
//
// Panics if buf is not of size PublicKeySize.
func (pk *PublicKey) Unpack(buf []byte) {
	if len(buf) != PublicKeySize {
		panic("buf must be of length PublicKeySize")
	}

	copy(pk.seedA[:], buf[:seedASize])
	unpack(pk.matrixB[:], buf[seedASize:])
}

// Boilerplate down below for the KEM scheme API.

type scheme struct{}

var sch kem.Scheme = &scheme{}

// Scheme returns a KEM interface.
func Scheme() kem.Scheme { return sch }

func (scheme) Name() string                { return "FrodoKEM-640-SHAKE" }
func (*scheme) PublicKeySize() int         { return PublicKeySize }
func (*scheme) PrivateKeySize() int        { return PrivateKeySize }
func (*scheme) SeedSize() int              { return KeySeedSize }
func (*scheme) SharedKeySize() int         { return SharedKeySize }
func (*scheme) CiphertextSize() int        { return CiphertextSize }
func (*scheme) EncapsulationSeedSize() int { return EncapsulationSeedSize }

func (sk *PrivateKey) Scheme() kem.Scheme { return sch }
func (pk *PublicKey) Scheme() kem.Scheme  { return sch }

func (sk *PrivateKey) MarshalBinary() ([]byte, error) {
	var ret [PrivateKeySize]byte
	sk.Pack(ret[:])
	return ret[:], nil
}

func (sk *PrivateKey) Equal(other kem.PrivateKey) bool {
	oth, ok := other.(*PrivateKey)
	if !ok {
		return false
	}
	if sk.pk == nil && oth.pk == nil {
		return true
	}
	if sk.pk == nil || oth.pk == nil {
		return false
	}
	return ctCompareU16(sk.matrixS[:], oth.matrixS[:]) == 0 &&
		subtle.ConstantTimeCompare(sk.hashInputIfDecapsFail[:], oth.hashInputIfDecapsFail[:]) == 1 &&
		sk.pk.Equal(oth.pk) &&
		bytes.Equal(sk.hpk[:], oth.hpk[:])
}

func (pk *PublicKey) Equal(other kem.PublicKey) bool {
	oth, ok := other.(*PublicKey)
	if !ok {
		return false
	}
	if pk == nil && oth == nil {
		return true
	}
	if pk == nil || oth == nil {
		return false
	}

	for i := range pk.matrixB {
		if (pk.matrixB[i] & logQMask) != (oth.matrixB[i] & logQMask) {
			return false
		}
	}
	return bytes.Equal(pk.seedA[:], oth.seedA[:])
}

func (sk *PrivateKey) Public() kem.PublicKey {
	return sk.pk
}

func (pk *PublicKey) MarshalBinary() ([]byte, error) {
	var ret [PublicKeySize]byte
	pk.Pack(ret[:])
	return ret[:], nil
}

func (*scheme) GenerateKeyPair() (kem.PublicKey, kem.PrivateKey, error) {
	return generateKeyPair(cryptoRand.Reader)
}

func (*scheme) DeriveKeyPair(seed []byte) (kem.PublicKey, kem.PrivateKey) {
	if len(seed) != KeySeedSize {
		panic(kem.ErrSeedSize)
	}
	return newKeyFromSeed(seed[:])
}

func (*scheme) Encapsulate(pk kem.PublicKey) (ct, ss []byte, err error) {
	ct = make([]byte, CiphertextSize)
	ss = make([]byte, SharedKeySize)

	pub, ok := pk.(*PublicKey)
	if !ok {
		return nil, nil, kem.ErrTypeMismatch
	}
	pub.EncapsulateTo(ct, ss, nil)
	return
}

func (*scheme) EncapsulateDeterministically(
	pk kem.PublicKey, seed []byte,
) (ct, ss []byte, err error) {
	if len(seed) != EncapsulationSeedSize {
		return nil, nil, kem.ErrSeedSize
	}

	ct = make([]byte, CiphertextSize)
	ss = make([]byte, SharedKeySize)

	pub, ok := pk.(*PublicKey)
	if !ok {
		return nil, nil, kem.ErrTypeMismatch
	}
	pub.EncapsulateTo(ct, ss, seed)
	return
}

func (*scheme) Decapsulate(sk kem.PrivateKey, ct []byte) ([]byte, error) {
	if len(ct) != CiphertextSize {
		return nil, kem.ErrCiphertextSize
	}

	priv, ok := sk.(*PrivateKey)
	if !ok {
		return nil, kem.ErrTypeMismatch
	}
	ss := make([]byte, SharedKeySize)
	priv.DecapsulateTo(ss, ct)
	return ss, nil
}

func (*scheme) UnmarshalBinaryPublicKey(buf []byte) (kem.PublicKey, error) {
	if len(buf) != PublicKeySize {
		return nil, kem.ErrPubKeySize
	}
	var ret PublicKey
	ret.Unpack(buf)
	return &ret, nil
}

func (*scheme) UnmarshalBinaryPrivateKey(buf []byte) (kem.PrivateKey, error) {
	if len(buf) != PrivateKeySize {
		return nil, kem.ErrPrivKeySize
	}
	var ret PrivateKey
	ret.Unpack(buf)
	return &ret, nil
}
