package state

import (
	"math/big"

	"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
	"github.com/nspcc-dev/neo-go/pkg/io"
	"github.com/nspcc-dev/neo-go/pkg/util"
)

// NEP5Tracker contains info about a single account in a NEP5 contract.
type NEP5Tracker struct {
	// Balance is the current balance of the account.
	Balance big.Int
	// LastUpdatedBlock is a number of block when last `transfer` to or from the
	// account occured.
	LastUpdatedBlock uint32
}

// NEP5TransferLog is a log of NEP5 token transfers for the specific command.
type NEP5TransferLog struct {
	Raw []byte
	// size is the number of NEP5Transfers written into Raw
	size int
}

// NEP5Transfer represents a single NEP5 Transfer event.
type NEP5Transfer struct {
	// Asset is a NEP5 contract hash.
	Asset util.Uint160
	// Address is the address of the sender.
	From util.Uint160
	// To is the address of the receiver.
	To util.Uint160
	// Amount is the amount of tokens transferred.
	// It is negative when tokens are sent and positive if they are received.
	Amount big.Int
	// Block is a number of block when the event occured.
	Block uint32
	// Timestamp is the timestamp of the block where transfer occured.
	Timestamp uint64
	// Tx is a hash the transaction.
	Tx util.Uint256
}

// NEP5Balances is a map of the NEP5 contract hashes
// to the corresponding structures.
type NEP5Balances struct {
	Trackers map[util.Uint160]NEP5Tracker
	// NextTransferBatch stores an index of the next transfer batch.
	NextTransferBatch uint32
}

// NewNEP5Balances returns new NEP5Balances.
func NewNEP5Balances() *NEP5Balances {
	return &NEP5Balances{
		Trackers: make(map[util.Uint160]NEP5Tracker),
	}
}

// DecodeBinary implements io.Serializable interface.
func (bs *NEP5Balances) DecodeBinary(r *io.BinReader) {
	bs.NextTransferBatch = r.ReadU32LE()
	lenBalances := r.ReadVarUint()
	m := make(map[util.Uint160]NEP5Tracker, lenBalances)
	for i := 0; i < int(lenBalances); i++ {
		var key util.Uint160
		var tr NEP5Tracker
		r.ReadBytes(key[:])
		tr.DecodeBinary(r)
		m[key] = tr
	}
	bs.Trackers = m
}

// EncodeBinary implements io.Serializable interface.
func (bs *NEP5Balances) EncodeBinary(w *io.BinWriter) {
	w.WriteU32LE(bs.NextTransferBatch)
	w.WriteVarUint(uint64(len(bs.Trackers)))
	for k, v := range bs.Trackers {
		w.WriteBytes(k[:])
		v.EncodeBinary(w)
	}
}

// Append appends single transfer to a log.
func (lg *NEP5TransferLog) Append(tr *NEP5Transfer) error {
	w := io.NewBufBinWriter()
	tr.EncodeBinary(w.BinWriter)
	if w.Err != nil {
		return w.Err
	}
	lg.Raw = append(lg.Raw, w.Bytes()...)
	lg.size++
	return nil
}

// ForEach iterates over transfer log returning on first error.
func (lg *NEP5TransferLog) ForEach(f func(*NEP5Transfer) error) error {
	if lg == nil {
		return nil
	}
	tr := new(NEP5Transfer)
	var bytesRead int
	for i := 0; i < len(lg.Raw); i += bytesRead {
		r := io.NewBinReaderFromBuf(lg.Raw[i:])
		bytesRead = tr.DecodeBinaryReturnCount(r)
		if r.Err != nil {
			return r.Err
		} else if err := f(tr); err != nil {
			return nil
		}
	}
	return nil
}

// Size returns an amount of transfer written in log.
func (lg *NEP5TransferLog) Size() int {
	return lg.size
}

// EncodeBinary implements io.Serializable interface.
func (t *NEP5Tracker) EncodeBinary(w *io.BinWriter) {
	w.WriteVarBytes(bigint.ToBytes(&t.Balance))
	w.WriteU32LE(t.LastUpdatedBlock)
}

// DecodeBinary implements io.Serializable interface.
func (t *NEP5Tracker) DecodeBinary(r *io.BinReader) {
	t.Balance = *bigint.FromBytes(r.ReadVarBytes())
	t.LastUpdatedBlock = r.ReadU32LE()
}

// EncodeBinary implements io.Serializable interface.
func (t *NEP5Transfer) EncodeBinary(w *io.BinWriter) {
	w.WriteBytes(t.Asset[:])
	w.WriteBytes(t.Tx[:])
	w.WriteBytes(t.From[:])
	w.WriteBytes(t.To[:])
	w.WriteU32LE(t.Block)
	w.WriteU64LE(t.Timestamp)
	amountBytes := bigint.ToBytes(&t.Amount)
	w.WriteU64LE(uint64(len(amountBytes)))
	w.WriteBytes(amountBytes)
}

// DecodeBinary implements io.Serializable interface.
func (t *NEP5Transfer) DecodeBinary(r *io.BinReader) {
	_ = t.DecodeBinaryReturnCount(r)
}

// DecodeBinaryReturnCount decodes NEP5Transfer and returns the number of bytes read.
func (t *NEP5Transfer) DecodeBinaryReturnCount(r *io.BinReader) int {
	r.ReadBytes(t.Asset[:])
	r.ReadBytes(t.Tx[:])
	r.ReadBytes(t.From[:])
	r.ReadBytes(t.To[:])
	t.Block = r.ReadU32LE()
	t.Timestamp = r.ReadU64LE()
	amountLen := r.ReadU64LE()
	amountBytes := make([]byte, amountLen)
	r.ReadBytes(amountBytes)
	t.Amount = *bigint.FromBytes(amountBytes)
	return util.Uint160Size*3 + 8 + 4 + (8 + len(amountBytes)) + +util.Uint256Size
}