/**
 *  @file
 *  @copyright defined in aergo/LICENSE.txt
 */

package types

import (
	"bytes"
	"encoding/binary"
	"io"
	"math"
	"math/big"
	"reflect"
	"runtime"
	"sync/atomic"
	"time"

	"github.com/aergoio/aergo/v2/internal/common"
	"github.com/aergoio/aergo/v2/internal/enc/base58"
	"github.com/aergoio/aergo/v2/internal/enc/proto"
	"github.com/aergoio/aergo/v2/internal/merkle"
	"github.com/libp2p/go-libp2p/core/crypto"
	"github.com/minio/sha256-simd"
)

const (
	// DefaultMaxBlockSize is the maximum block size (currently 1MiB)
	DefaultMaxBlockSize = 1 << 20
	DefaultEvictPeriod  = 12

	// DefaultMaxHdrSize is the max size of the proto-buf serialized non-body
	// fields. For the estimation detail, check 'TestBlockHeaderLimit' in
	// 'blockchain_test.go.' Caution: Be sure to adjust the value below if the
	// structure of the header is changed.
	DefaultMaxHdrSize = 400
	lastFieldOfBH     = "Consensus"
)

type TxHash = []byte
type AvgTime struct {
	val  atomic.Value
	mavg *MovingAverage
}

var (
	DefaultVerifierCnt = int(math.Max(float64(runtime.NumCPU()/2), float64(1)))
	DefaultAvgTimeSize = 60 * 60 * 24
	AvgTxVerifyTime    = NewAvgTime(DefaultAvgTimeSize)
	// MaxAER is maximum value of aergo
	MaxAER *big.Int
	// StakingMinimum is minimum amount for staking
	StakingMinimum *big.Int
	// ProposalPrice is default value of creating proposal
	ProposalPrice *big.Int
	lastIndexOfBH int
)

func init() {
	MaxAER = NewAmount(5*1e8, Aergo)       // 500,000,000 aergo
	StakingMinimum = NewAmount(1e4, Aergo) // 10,000 aergo
	ProposalPrice = NewZeroAmount()        // 0 aergo
	lastIndexOfBH = getLastIndexOfBH()
}

func NewAvgTime(sizeMavg int) *AvgTime {
	avgTime := &AvgTime{}
	avgTime.mavg = NewMovingAverage(sizeMavg)
	avgTime.val.Store(time.Duration(0))
	return avgTime
}

func (avgTime *AvgTime) Get() time.Duration {
	var avg time.Duration
	aopv := avgTime.val.Load()
	if aopv != nil {
		avg = aopv.(time.Duration)
	} else {
		panic("AvgTxSignTime is not set")
	}
	return avg
}

func (avgTime *AvgTime) UpdateAverage(cur time.Duration) time.Duration {
	newAvg := time.Duration(avgTime.mavg.Add(int64(cur)))
	avgTime.set(newAvg)

	return newAvg
}

func (avgTime *AvgTime) set(val time.Duration) {
	avgTime.val.Store(val)
}

func getLastIndexOfBH() (lastIndex int) {
	v := reflect.ValueOf(BlockHeader{})

	nField := v.NumField()
	var i int
	for i = 0; i < nField; i++ {
		name := v.Type().Field(i).Name
		if name == lastFieldOfBH {
			lastIndex = i
			break
		}
	}

	return i
}

//go:generate stringer -type=SystemValue
type SystemValue int

const (
	StakingTotal SystemValue = 0 + iota
	StakingMin
	GasPrice
	NamePrice
	TotalVotingPower
	VotingReward
)

/*
  func (s SystemValue) String() string {
	  return [...]string{"StakingTotal", "StakingMin", "GasPrice", "NamePrice"}[s]
  }
*/

// ChainAccessor is an interface for a another actor module to get info of chain
type ChainAccessor interface {
	GetGenesisInfo() *Genesis
	GetConsensusInfo() string
	GetBestBlock() (*Block, error)
	// GetBlock return block of blockHash. It return nil and error if not found block of that hash or there is a problem in db store
	GetBlock(blockHash []byte) (*Block, error)
	// GetHashByNo returns hash of block. It return nil and error if not found block of that number or there is a problem in db store
	GetHashByNo(blockNo BlockNo) ([]byte, error)
	GetChainStats() string
	GetSystemValue(key SystemValue) (*big.Int, error)

	// GetEnterpriseConfig always return non-nil object if there is no error, but it can return EnterpriseConfig with empty values
	GetEnterpriseConfig(key string) (*EnterpriseConfig, error)
	ChainID(bno BlockNo) *ChainID

	// HardforkHeights returns a map with hardfork version number as keys and their corresponding block numbers as values.
	HardforkHeights() map[string]BlockNo
}

type SyncContext struct {
	Seq uint64

	PeerID PeerID

	BestNo   BlockNo
	TargetNo BlockNo //sync target blockno

	CommonAncestor *Block

	TotalCnt   uint64
	RemainCnt  uint64
	LastAnchor BlockNo

	NotifyC chan error
}

func NewSyncCtx(seq uint64, peerID PeerID, targetNo uint64, bestNo uint64, notifyC chan error) *SyncContext {
	return &SyncContext{Seq: seq, PeerID: peerID, TargetNo: targetNo, BestNo: bestNo, LastAnchor: 0, NotifyC: notifyC}
}

func (ctx *SyncContext) SetAncestor(ancestor *Block) {
	ctx.CommonAncestor = ancestor
	ctx.TotalCnt = ctx.TargetNo - ctx.CommonAncestor.BlockNo()
	ctx.RemainCnt = ctx.TotalCnt
}

// BlockInfo is used for actor message to send block info
type BlockInfo struct {
	Hash []byte
	No   BlockNo
}

func (bi *BlockInfo) Equal(target *BlockInfo) bool {
	if target == nil {
		return false
	}

	if bi.No == target.No && bytes.Equal(bi.Hash, target.Hash) {
		return true
	} else {
		return false
	}
}

// BlockNo is the height of a block, which starts from 0 (genesis block).
type BlockNo = uint64

func Uint64ToBytes(num uint64) []byte {
	buf := make([]byte, 8)
	binary.LittleEndian.PutUint64(buf, num)
	return buf
}

func BytesToUint64(data []byte) uint64 {
	buf := binary.LittleEndian.Uint64(data)
	return buf
}

// BlockNoToBytes represents to serialize block no to bytes
func BlockNoToBytes(bn BlockNo) []byte {
	return Uint64ToBytes(bn)
}

// BlockNoFromBytes represents to deserialize bytes to block no
func BlockNoFromBytes(raw []byte) BlockNo {
	buf := BytesToUint64(raw)
	return BlockNo(buf)
}

type BlockVersionner interface {
	Version(no BlockNo) int32
	IsV2Fork(BlockNo) bool
}

type DummyBlockVersionner int32

func (v DummyBlockVersionner) Version(BlockNo) int32 {
	return int32(v)
}

func (v DummyBlockVersionner) IsV2Fork(BlockNo) bool {
	return (v >= 2)
}

// NewBlock represents to create a block to store transactions.
func NewBlock(bi *BlockHeaderInfo, blockRoot []byte, receipts *Receipts, txs []*Tx, coinbaseAcc []byte, consensus []byte) *Block {
	return &Block{
		Header: &BlockHeader{
			ChainID:          bi.ChainId,
			PrevBlockHash:    bi.PrevBlockHash,
			BlockNo:          bi.No,
			Timestamp:        bi.Ts,
			BlocksRootHash:   blockRoot,
			TxsRootHash:      CalculateTxsRootHash(txs),
			ReceiptsRootHash: receipts.MerkleRoot(),
			CoinbaseAccount:  coinbaseAcc,
			Consensus:        consensus,
		},
		Body: &BlockBody{
			Txs: txs,
		},
	}
}

// Localtime retrurns a time.Time object, which is coverted from block
// timestamp.
func (block *Block) Localtime() time.Time {
	return time.Unix(0, block.GetHeader().GetTimestamp())
}

// calculateBlockHash computes sha256 hash of block header.
func (block *Block) calculateBlockHash() []byte {
	digest := sha256.New()
	writeBlockHeader(digest, block.Header)
	return digest.Sum(nil)
}

func writeBlockHeader(w io.Writer, bh *BlockHeader) error {
	for _, f := range []interface{}{
		bh.ChainID,
		bh.PrevBlockHash,
		bh.BlockNo,
		bh.Timestamp,
		bh.BlocksRootHash,
		bh.TxsRootHash,
		bh.ReceiptsRootHash,
		bh.Confirms,
		bh.PubKey,
		bh.CoinbaseAccount,
		bh.Sign,
		bh.Consensus,
	} {
		if err := binary.Write(w, binary.LittleEndian, f); err != nil {
			return err
		}
	}

	return nil
}

func writeBlockHeaderOmitSign(w io.Writer, bh *BlockHeader) error {
	for _, f := range []interface{}{
		bh.ChainID,
		bh.PrevBlockHash,
		bh.BlockNo,
		bh.Timestamp,
		bh.BlocksRootHash,
		bh.TxsRootHash,
		bh.ReceiptsRootHash,
		bh.Confirms,
		bh.PubKey,
		bh.CoinbaseAccount,
		// bh.Sign, // omit ignore sign value
		bh.Consensus,
	} {
		if err := binary.Write(w, binary.LittleEndian, f); err != nil {
			return err
		}
	}

	return nil
}

// BlockHash returns block hash. It returns a calculated value if the hash is nil.
func (block *Block) BlockHash() []byte {
	hash := block.GetHash()
	if len(hash) == 0 {
		block.Hash = block.calculateBlockHash()
	}

	return block.GetHash()
}

// BlockID converts block hash ([]byte) to BlockID.
func (block *Block) BlockID() BlockID {
	return ToBlockID(block.BlockHash())
}

// PrevBlockID converts parent block hash ([]byte) to BlockID.
func (block *Block) PrevBlockID() BlockID {
	return ToBlockID(block.GetHeader().GetPrevBlockHash())
}

// SetChainID sets id to block.ChainID
func (block *Block) SetChainID(id []byte) {
	block.Header.ChainID = id
}

// ValidChildOf reports whether block is a varid child of parent.
func (block *Block) ValidChildOf(parent *Block) bool {
	parChainID := parent.GetHeader().GetChainID()
	curChainID := block.GetHeader().GetChainID()

	// empty chain id case: an older verion of block has no chain id in its
	// block header.
	if len(parChainID) == 0 && len(curChainID) == 0 {
		return true
	}

	return ChainIdEqualWithoutVersion(parChainID, curChainID)
}

// Size returns a block size where the tx size is individually calculated. A
// similar method is used to limit the block size by the block factory.
//
// THE REASON WHY THE BLOCK FACTORY DOESN'T USE THE EXACT SIZE OF A MARSHALED
// BLOCK: The actual size of a marshaled block is larger than this because it
// includes an additional data associated with the marshaling of the
// transations array in the block body. It is ineffective that the (DPoS) block
// factory measures the exact size of the additional probuf data when it
// produces a block. Thus we use the slightly(?) different and less expensive
// estimation of the block size.
func (block *Block) Size() int {
	size := proto.Size(block.GetHeader()) + len(block.GetHash())
	for _, tx := range block.GetBody().GetTxs() {
		size += proto.Size(tx)
	}
	return size
}

// Confirms returns block.Header.Confirms which indicates how many block is confirmed
// by block.
func (block *Block) Confirms() BlockNo {
	return block.GetHeader().GetConfirms()
}

// SetConfirms sets block.Header.Confirms to confirms.
func (block *Block) SetConfirms(confirms BlockNo) {
	block.Header.Confirms = confirms
}

// BlockNo returns the block number of block.
func (block *Block) BlockNo() BlockNo {
	return block.GetHeader().GetBlockNo()
}

// Sign adds a pubkey and a block signature to block.
func (block *Block) Sign(privKey crypto.PrivKey) error {
	var err error

	if err = block.setPubKey(privKey.GetPublic()); err != nil {
		return err
	}

	var msg []byte
	if msg, err = block.Header.bytesForDigest(); err != nil {
		return err
	}

	var sig []byte
	if sig, err = privKey.Sign(msg); err != nil {
		return err
	}
	block.Header.Sign = sig

	return nil
}

func (bh *BlockHeader) bytesForDigest() ([]byte, error) {
	var buf bytes.Buffer

	if err := writeBlockHeaderOmitSign(&buf, bh); err != nil {
		return nil, err
	}

	return buf.Bytes(), nil
}

// VerifySign verifies the signature of block.
func (block *Block) VerifySign() (valid bool, err error) {
	var pubKey crypto.PubKey
	if pubKey, err = crypto.UnmarshalPublicKey(block.Header.PubKey); err != nil {
		return false, err
	}

	var msg []byte
	if msg, err = block.Header.bytesForDigest(); err != nil {
		return false, err
	}

	if valid, err = pubKey.Verify(msg, block.Header.Sign); err != nil {
		return
	}

	return valid, nil
}

// BPID returns its Block Producer's ID from block.
func (block *Block) BPID() (id PeerID, err error) {
	return block.Header.BPID()
}

// BPID2Str returns its Block Producer's ID in base64 format.
func (block *Block) BPID2Str() string {
	id, err := block.BPID()
	if err != nil {
		return ""
	}

	return base58.Encode([]byte(id))
}

// ID returns the base64 encoded formated ID (hash) of block.
func (block *Block) ID() string {
	hash := block.BlockHash()
	if hash != nil {
		return base58.Encode(hash)
	}

	return ""

}

// PrevID returns the base64 encoded formated ID (hash) of the parent block.
func (block *Block) PrevID() string {
	hash := block.GetHeader().GetPrevBlockHash()
	if hash != nil {
		return base58.Encode(hash)
	}

	return ""

}

// SetPubKey sets block.Header.PubKey to pubkey.
func (block *Block) setPubKey(pubKey crypto.PubKey) error {
	var pk []byte
	var err error
	if pk, err = crypto.MarshalPublicKey(pubKey); err != nil {
		return err
	}
	block.Header.PubKey = pk

	return nil
}

func (block *Block) SetBlocksRootHash(blockRootHash []byte) {
	block.GetHeader().BlocksRootHash = blockRootHash
}

// GetMetadata generates Metadata object for block
func (block *Block) GetMetadata() *BlockMetadata {
	return &BlockMetadata{
		Hash:    block.BlockHash(),
		Header:  block.GetHeader(),
		Txcount: int32(len(block.GetBody().GetTxs())),
		Size:    int64(proto.Size(block)),
	}
}

// CalculateTxsRootHash generates merkle tree of transactions and returns root hash.
func CalculateTxsRootHash(txs []*Tx) []byte {
	mes := make([]merkle.MerkleEntry, len(txs))
	for i, tx := range txs {
		mes[i] = tx
	}
	return merkle.CalculateMerkleRoot(mes)
}

func NewTx() *Tx {
	tx := &Tx{
		Body: &TxBody{
			Nonce: uint64(1),
		},
	}
	return tx
}

func (tx *Tx) CalculateTxHash() []byte {
	txBody := tx.Body
	digest := sha256.New()
	binary.Write(digest, binary.LittleEndian, txBody.Nonce)

	digest.Write(txBody.Account)
	digest.Write(txBody.Recipient)
	digest.Write(txBody.Amount)
	digest.Write(txBody.Payload)
	binary.Write(digest, binary.LittleEndian, txBody.GasLimit)
	digest.Write(txBody.GasPrice)
	binary.Write(digest, binary.LittleEndian, txBody.Type)
	digest.Write(txBody.ChainIdHash)
	digest.Write(txBody.Sign)
	return digest.Sum(nil)
}

func (tx *Tx) NeedNameVerify() bool {
	return tx.HasNameAccount()
}

func (tx *Tx) HasNameAccount() bool {
	return len(tx.Body.Account) <= NameLength
}

func (tx *Tx) HasNameRecipient() bool {
	return tx.Body.Recipient != nil && len(tx.Body.Recipient) <= NameLength
}

func (tx *Tx) Clone() *Tx {
	if tx == nil {
		return nil
	}
	if tx.Body == nil {
		return &Tx{}
	}
	body := &TxBody{
		Nonce:       tx.Body.Nonce,
		Account:     tx.Body.Account,
		Recipient:   tx.Body.Recipient,
		Amount:      tx.Body.Amount,
		Payload:     tx.Body.Payload,
		GasLimit:    tx.Body.GasLimit,
		GasPrice:    tx.Body.GasPrice,
		Type:        tx.Body.Type,
		ChainIdHash: tx.Body.ChainIdHash,
		Sign:        tx.Body.Sign,
	}
	res := &Tx{
		Body: body,
	}
	res.Hash = res.CalculateTxHash()
	return res
}

func (b *TxBody) GetAmountBigInt() *big.Int {
	return new(big.Int).SetBytes(b.GetAmount())
}

func (b *TxBody) GetGasPriceBigInt() *big.Int {
	return new(big.Int).SetBytes(b.GetGasPrice())
}

type MovingAverage struct {
	values []int64
	size   int
	count  int

	sum        int64
	removedVal int64
	curPos     int
}

func NewMovingAverage(size int) *MovingAverage {
	return &MovingAverage{
		values:     make([]int64, size),
		size:       size,
		removedVal: 0,
		sum:        0,
		curPos:     -1,
		count:      0,
	}
}

func (ma *MovingAverage) Add(val int64) int64 {
	ma.curPos = (ma.curPos + 1) % ma.size
	ma.removedVal = ma.values[ma.curPos]
	ma.values[ma.curPos] = val

	if ma.count != ma.size {
		ma.count++
	}

	return ma.calculateAvg()
}

func (ma *MovingAverage) calculateAvg() int64 {
	//values is empty
	if ma.count == 0 {
		return 0
	}

	ma.sum = ma.sum - ma.removedVal + ma.values[ma.curPos]

	// Finalize average and return
	avg := ma.sum / int64(ma.count)
	return avg
}

type BlockHeaderInfo struct {
	No            BlockNo
	Ts            int64
	PrevBlockHash []byte
	ChainId       []byte
	ForkVersion   int32
}

var EmptyBlockHeaderInfo = &BlockHeaderInfo{}

func NewBlockHeaderInfo(b *Block) *BlockHeaderInfo {
	cid := b.GetHeader().GetChainID()
	v := DecodeChainIdVersion(cid)
	return &BlockHeaderInfo{
		b.BlockNo(),
		b.GetHeader().GetTimestamp(),
		b.GetHeader().GetPrevBlockHash(),
		cid,
		v,
	}
}

func MakeChainId(cid []byte, v int32) []byte {
	nv := ChainIdVersion(v)
	if bytes.Equal(cid[:4], nv) {
		return cid
	}
	newCid := make([]byte, len(cid))
	copy(newCid, nv)
	copy(newCid[4:], cid[4:])
	return newCid
}

func NewBlockHeaderInfoFromPrevBlock(prev *Block, ts int64, bv BlockVersionner) *BlockHeaderInfo {
	no := prev.GetHeader().GetBlockNo() + 1
	cid := prev.GetHeader().GetChainID()
	v := bv.Version(no)
	return &BlockHeaderInfo{
		no,
		ts,
		prev.GetHash(),
		MakeChainId(cid, v),
		v,
	}
}

func (b *BlockHeaderInfo) ChainIdHash() []byte {
	return common.Hasher(b.ChainId)
}

func (bh *BlockHeader) BPID() (id PeerID, err error) {
	var pubKey crypto.PubKey
	if pubKey, err = crypto.UnmarshalPublicKey(bh.PubKey); err != nil {
		return PeerID(""), err
	}
	if id, err = IDFromPublicKey(pubKey); err != nil {
		return PeerID(""), err
	}
	return
}

// HasFunction returns if a function with the given name exists in the ABI definition
func (abi *ABI) HasFunction(name string) bool {
	for _, fn := range abi.Functions {
		if fn.GetName() == name {
			return true
		}
	}
	return false
}
