package drbg

import (
	"crypto/cipher"
	"encoding/binary"
	"errors"
	"time"

	"github.com/emmansun/gmsm/internal/subtle"
	"github.com/emmansun/gmsm/sm4"
)

// CtrDrbg CTR DRBG structure, its instance is NOT goroutine safe!!!
type CtrDrbg struct {
	BaseDrbg
	cipherProvider func(key []byte) (cipher.Block, error)
	key            []byte
	keyLen         int
}

// NewCtrDrbg create one CTR DRBG instance
func NewCtrDrbg(cipherProvider func(key []byte) (cipher.Block, error), keyLen int, securityLevel SecurityLevel, gm bool, entropy, nonce, personalization []byte) (*CtrDrbg, error) {
	hd := &CtrDrbg{}

	hd.gm = gm
	hd.setSecurityLevel(securityLevel)

	// here for the min length, we just check <=0 now
	if len(entropy) == 0 || (hd.gm && len(entropy) < 32) || len(entropy) >= MAX_BYTES {
		return nil, errors.New("drbg: invalid entropy length")
	}

	// here for the min length, we just check <=0 now
	if len(nonce) == 0 || (hd.gm && len(nonce) < 16) || len(nonce) >= MAX_BYTES>>1 {
		return nil, errors.New("drbg: invalid nonce length")
	}

	if len(personalization) >= MAX_BYTES {
		return nil, errors.New("drbg: personalization is too long")
	}

	hd.cipherProvider = cipherProvider
	hd.keyLen = keyLen
	temp := make([]byte, hd.keyLen)
	block, err := cipherProvider(temp)
	if err != nil {
		return nil, err
	}
	hd.seedLength = block.BlockSize() + keyLen
	hd.v = make([]byte, block.BlockSize())
	hd.key = make([]byte, hd.keyLen)

	// seed_material = entropy_input || instantiation_nonce || personalization_string
	seedMaterial := make([]byte, len(entropy)+len(nonce)+len(personalization))
	copy(seedMaterial, entropy)
	copy(seedMaterial[len(entropy):], nonce)
	copy(seedMaterial[len(entropy)+len(nonce):], personalization)
	// seed_material = Block_Cipher_df(seed_material, seed_length)
	seedMaterial = hd.derive(seedMaterial, hd.seedLength)
	// CTR_DRBG_Updae(seed_material, Key, V)
	hd.update(seedMaterial)

	hd.reseedCounter = 1
	hd.reseedTime = time.Now()
	return hd, nil
}

// NewNISTCtrDrbg create one CTR DRBG implementation which follows NIST standard
func NewNISTCtrDrbg(cipherProvider func(key []byte) (cipher.Block, error), keyLen int, securityLevel SecurityLevel, entropy, nonce, personalization []byte) (*CtrDrbg, error) {
	return NewCtrDrbg(cipherProvider, keyLen, securityLevel, false, entropy, nonce, personalization)
}

// NewGMCtrDrbg create one CTR DRBG implementation which follows GM/T 0105-2021 standard
func NewGMCtrDrbg(securityLevel SecurityLevel, entropy, nonce, personalization []byte) (*CtrDrbg, error) {
	return NewCtrDrbg(sm4.NewCipher, 16, securityLevel, true, entropy, nonce, personalization)
}

func (hd *CtrDrbg) Reseed(entropy, additional []byte) error {
	// here for the min length, we just check <=0 now
	if len(entropy) <= 0 || (hd.gm && len(entropy) < 32) || len(entropy) >= MAX_BYTES {
		return errors.New("drbg: invalid entropy length")
	}

	if len(additional) >= MAX_BYTES {
		return errors.New("drbg: additional input too long")
	}

	// seed_material = entropy_input || additional_input
	var seedMaterial []byte
	if len(additional) == 0 {
		seedMaterial = entropy
	} else {
		seedMaterial = make([]byte, len(entropy)+len(additional))
		copy(seedMaterial, entropy)
		copy(seedMaterial[len(entropy):], additional)
	}
	// seed_material = Block_Cipher_df(seed_material, seed_length)
	seedMaterial = hd.derive(seedMaterial, hd.seedLength)
	// CTR_DRBG_Updae(seed_material, Key, V)
	hd.update(seedMaterial)

	hd.reseedCounter = 1
	hd.reseedTime = time.Now()
	return nil
}

func (hd *CtrDrbg) newBlockCipher(key []byte) cipher.Block {
	block, err := hd.cipherProvider(key)
	if err != nil {
		panic(err)
	}
	return block
}

func (hd *CtrDrbg) MaxBytesPerRequest() int {
	if hd.gm {
		return len(hd.v)
	}
	return MAX_BYTES_PER_GENERATE
}

// Generate CTR DRBG pseudorandom bits generate process.
func (hd *CtrDrbg) Generate(b, additional []byte) error {
	if hd.NeedReseed() {
		return ErrReseedRequired
	}
	outlen := len(hd.v)
	if (hd.gm && len(b) > outlen) || (!hd.gm && len(b) > MAX_BYTES_PER_GENERATE) {
		return errors.New("drbg: too many bytes requested")
	}

	// If len(additional_input) > 0, then
	// additional_input = Block_Cipher_df(additional_input, seed_length)
	// CTR_DRBG_Update(additional_input, Key, V)
	if len(additional) > 0 {
		additional = hd.derive(additional, hd.seedLength)
		hd.update(additional)
	}

	block := hd.newBlockCipher(hd.key)
	temp := make([]byte, outlen)

	m := len(b)
	limit := uint64(m+outlen-1) / uint64(outlen)
	for i := 0; i < int(limit); i++ {
		// V = (V + 1) mod 2^outlen)
		addOne(hd.v, outlen)
		// output_block = Encrypt(Key, V)
		block.Encrypt(temp, hd.v)
		copy(b[i*outlen:], temp)
	}
	hd.update(additional)
	hd.reseedCounter++
	return nil
}

func (cd *CtrDrbg) update(seedMaterial []byte) {
	temp := make([]byte, cd.seedLength)
	block := cd.newBlockCipher(cd.key)

	outlen := block.BlockSize()
	v := make([]byte, outlen)
	output := make([]byte, outlen)
	copy(v, cd.v)
	for i := 0; i < (cd.seedLength+outlen-1)/outlen; i++ {
		// V = (V + 1) mod 2^outlen
		addOne(v, outlen)
		// output_block = Encrypt(Key, V)
		block.Encrypt(output, v)
		copy(temp[i*outlen:], output)
	}
	// temp = temp XOR seed_material
	subtle.XORBytes(temp, temp, seedMaterial)
	// Key = leftmost(temp, key_length)
	copy(cd.key, temp)
	// V = rightmost(temp, outlen)
	copy(cd.v, temp[cd.keyLen:])
}

// derive Block_Cipher_df
func (cd *CtrDrbg) derive(seedMaterial []byte, returnBytes int) []byte {
	outlen := cd.seedLength - cd.keyLen
	lenS := ((4 + 4 + len(seedMaterial) + outlen) / outlen) * outlen
	S := make([]byte, lenS+outlen)

	// S = counter || len(seed_material) || len(return_bytes) || seed_material || 0x80
	// len(S) = ((outlen + 4 + 4 + len(seed_material) + 1 + outlen - 1) / outlen) * outlen
	binary.BigEndian.PutUint32(S[outlen:], uint32(len(seedMaterial)))
	binary.BigEndian.PutUint32(S[outlen+4:], uint32(returnBytes))
	copy(S[outlen+8:], seedMaterial)
	S[outlen+8+len(seedMaterial)] = 0x80

	key := make([]byte, cd.keyLen)
	for i := 0; i < cd.keyLen; i++ {
		key[i] = byte(i)
	}
	blocks := (cd.seedLength + outlen - 1) / outlen
	temp := make([]byte, blocks*outlen)
	block := cd.newBlockCipher(key)

	for i := 0; i < blocks; i++ {
		binary.BigEndian.PutUint32(S, uint32(i))
		copy(temp[i*outlen:], cd.bcc(block, S))
	}

	key = temp[:cd.keyLen]
	X := temp[cd.keyLen:cd.seedLength]
	temp = make([]byte, returnBytes)
	block = cd.newBlockCipher(key)
	for i := 0; i < (returnBytes+outlen-1)/outlen; i++ {
		block.Encrypt(X, X)
		copy(temp[i*outlen:], X)
	}
	return temp
}

func (cd *CtrDrbg) bcc(block cipher.Block, data []byte) []byte {
	chainingValue := make([]byte, block.BlockSize())
	for i := 0; i < len(data)/block.BlockSize(); i++ {
		subtle.XORBytes(chainingValue, chainingValue, data[i*block.BlockSize():])
		block.Encrypt(chainingValue, chainingValue)
	}
	return chainingValue
}
