package ctrrng

import (
	"crypto/cipher"
	"encoding/binary"
	"io"
	"runtime"
	"time"

	"xdx.jelly/xgcl/grand/drng/internal"
	"xdx.jelly/xgcl/internal/xor"
	"xdx.jelly/xgcl/sm/sm4"
)

type SM4BlockNewer struct{}

func (b SM4BlockNewer) BlockSize() int {
	return sm4.BlockSize
}

func (b SM4BlockNewer) New(key []byte) cipher.Block {
	if len(key) != sm4.BlockSize {
		panic("ctrrng: invalid block key length")
	}

	block, _ := sm4.NewCipher(key)
	return block
}

func NewCtrDrng(newBlock NewBlock, config *internal.DrngConfig) (internal.RawDrng, error) {
	hd := &CtrDrng{
		config:   *config,
		newBlock: newBlock,
		df:       newDF(newBlock),
	}

	// 析构时对内部状态置0
	runtime.SetFinalizer(hd, func(hd *CtrDrng) {
		hd.state.zeromize()
	})
	return hd, nil
}

type internalState struct {
	v []byte // blockLen
	k []byte // keyLen

	reseedCounter  uint32
	lastReseedTime time.Time
}

// zeromize 对内部状态置0
func (s *internalState) zeromize() {
	for i := 0; i < len(s.v); i++ {
		s.v[i] = 0
	}

	for i := 0; i < len(s.k); i++ {
		s.k[i] = 0
	}

	s.reseedCounter = 0
}

type CtrDrng struct {
	// mu sync.Mutex

	config                internal.DrngConfig
	newBlock              NewBlock
	df                    func(input []byte, n int) []byte
	state                 internalState
	personalizationString []byte //个性化字符串，来自应用的比特串
}

type NewBlock interface {
	BlockSize() int
	New(key []byte) cipher.Block
}

// newDF 返回一个使用block作为杂凑的df函数
// 例：SM3_df = newDF(sm.SM3)
func newDF(newBlock NewBlock) func(input []byte, n int) []byte {
	keylen := newBlock.BlockSize()
	outlen := newBlock.BlockSize()
	keymatial := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 31}

	return func(input []byte, n int) []byte {
		L := len(input)

		// S = L || n || input || 0x80 || 00 such that len(S) % n = 0
		S := make([]byte, ((9+L+n-1)/n)*n)
		binary.BigEndian.PutUint32(S, uint32(L))
		binary.BigEndian.PutUint32(S[4:], uint32(n))
		copy(S[8:], input)
		S[8+L] = 0x80

		K := keymatial[:keylen]
		temp := make([]byte, 0, keylen+n)
		ivs := make([]byte, outlen+len(S)) // iv || S
		copy(ivs[4:], S)
		i := uint32(0)
		block := newBlock.New(K)
		for len(temp) < keylen+n {
			binary.BigEndian.PutUint32(ivs, i)
			i++
			temp = append(temp, cbcMac(block, ivs)...)
		}

		K = temp[:keylen]
		X := temp[keylen:]
		block = newBlock.New(K)
		temp = []byte{}
		for len(temp) < n {
			block.Encrypt(X, X)
			temp = append(temp, X...)
		}
		return temp[:n]
	}
}

// len(data) % block.BlockSize() == 0
func cbcMac(block cipher.Block, data []byte) []byte {
	blockSize := block.BlockSize()
	iv := make([]byte, blockSize)
	n := 0
	for n+blockSize <= len(data) {
		xor.XorBytes(iv, iv, data[n:])
		block.Encrypt(iv, iv)
		n += blockSize
	}
	return iv
}

func (cd *CtrDrng) keyLen() int {
	return cd.newBlock.BlockSize()
}

func (cd *CtrDrng) blockLen() int {
	return cd.newBlock.BlockSize()
}

func (cd *CtrDrng) seedLen() int {
	return cd.keyLen() + cd.OutLen()
}

func (cd *CtrDrng) OutLen() int {
	return cd.config.Outlen
}

func (cd *CtrDrng) Config() *internal.DrngConfig {
	return &cd.config
}

// Instantiate 初始化HashDrng
func (cd *CtrDrng) Instantiate(personalizationString []byte, nonce []byte, entropySource io.Reader) (internal.Drng, error) {
	// cd.mu.Lock()
	// defer cd.mu.Unlock()

	cfg := cd.config
	cd.personalizationString = personalizationString
	cd.state.k = make([]byte, cd.keyLen())
	cd.state.v = make([]byte, cd.blockLen())

	seedMaterial := make([]byte, 2*cfg.MinEntropyInputLength, int(2*cfg.MinEntropyInputLength)+len(nonce)+len(personalizationString))
	var m, n int
	var err error
	var errCount int
	for n < int(cfg.MinEntropyInputLength) {
		m, err = entropySource.Read(seedMaterial[n:])
		n += m
		if err != nil {
			errCount++
		}
		if errCount > 3 {
			return cd, err
		}
	}
	seedMaterial = append(seedMaterial, nonce...)
	seedMaterial = append(seedMaterial, personalizationString...)
	seedMaterial = cd.df(seedMaterial, cd.seedLen())
	cd.update(seedMaterial)
	return cd, nil
}

func (cd *CtrDrng) update(seedMaterial []byte) {
	seedlen := cd.seedLen()
	temp := make([]byte, seedlen)
	n := 0
	block := cd.newBlock.New(cd.state.k)
	for n+cd.blockLen() < seedlen {
		internal.IncBytes(cd.state.v)
		block.Encrypt(temp[n:n+cd.blockLen()], cd.state.v)
		n += cd.blockLen()
	}
	xor.XorBytes(temp, temp, seedMaterial)
	copy(cd.state.k, temp)
	copy(cd.state.v, temp[cd.keyLen():])
}

func (cd *CtrDrng) Reseed(entropyInput, additionalInput []byte) {
	// cd.mu.Lock()
	// defer cd.mu.Unlock()

	seedMaterial := make([]byte, len(entropyInput)+len(additionalInput))
	copy(seedMaterial, entropyInput)
	copy(seedMaterial[len(entropyInput):], additionalInput)
	seedMaterial = cd.df(seedMaterial, cd.seedLen())
	cd.update(seedMaterial)
	cd.state.reseedCounter = 1
	cd.state.lastReseedTime = time.Now()
	for i := 0; i < len(seedMaterial); i++ {
		seedMaterial[i] = 0
	}

}

func (cd *CtrDrng) Generate(b []byte, additionalInput []byte) error {
	// cd.mu.Lock()
	// defer cd.mu.Unlock()

	if len(b) > cd.config.Outlen {
		return internal.ErrReturnedBitsTooLong
	}

	// 需要重播种
	if cd.state.reseedCounter > uint32(cd.config.ReseedIntervalInCounter) ||
		time.Since(cd.state.lastReseedTime) > cd.config.ReseedIntervalInTime {
		return internal.ErrNeedReseed
	}

	if len(additionalInput) > 0 {
		additionalInput = cd.df(additionalInput, cd.seedLen())
		cd.update(additionalInput)
	} else {
		additionalInput = make([]byte, cd.seedLen())
	}
	block := cd.newBlock.New(cd.state.k)
	internal.IncBytes(cd.state.v)
	outputBlock := make([]byte, block.BlockSize())
	block.Encrypt(outputBlock, cd.state.v)
	copy(b, outputBlock)
	cd.update(additionalInput)
	cd.state.reseedCounter++
	return nil
}
