package database

import (
	"encoding/binary"
	"encoding/json"
	"errors"
	"math"

	"github.com/vapor/application/mov/common"
	dbm "github.com/vapor/database/leveldb"
	"github.com/vapor/protocol/bc"
	"github.com/vapor/protocol/bc/types"
)

const (
	order byte = iota
	tradePair
	matchStatus

	tradePairsNum = 1024
	ordersNum     = 10240
	assetIDLen    = 32
	rateByteLen   = 8
)

var (
	movStore         = []byte("MOV:")
	ordersPrefix     = append(movStore, order)
	tradePairsPrefix = append(movStore, tradePair)
	bestMatchStore   = append(movStore, matchStatus)
)

func calcOrderKey(fromAssetID, toAssetID *bc.AssetID, utxoHash *bc.Hash, rate float64) []byte {
	buf := make([]byte, 8)
	binary.BigEndian.PutUint64(buf, math.Float64bits(rate))
	key := append(ordersPrefix, fromAssetID.Bytes()...)
	key = append(key, toAssetID.Bytes()...)
	key = append(key, buf...)
	return append(key, utxoHash.Bytes()...)
}

func calcTradePairKey(fromAssetID, toAssetID *bc.AssetID) []byte {
	key := append(tradePairsPrefix, fromAssetID.Bytes()...)
	return append(key, toAssetID.Bytes()...)
}

func calcUTXOHash(order *common.Order) *bc.Hash {
	prog := &bc.Program{VmVersion: 1, Code: order.Utxo.ControlProgram}
	src := &bc.ValueSource{
		Ref:      order.Utxo.SourceID,
		Value:    &bc.AssetAmount{AssetId: order.FromAssetID, Amount: order.Utxo.Amount},
		Position: order.Utxo.SourcePos,
	}
	hash := bc.EntryID(bc.NewIntraChainOutput(src, prog, 0))
	return &hash
}

func getAssetIDFromTradePairKey(key []byte, prefix []byte, posIndex int) *bc.AssetID {
	b := [32]byte{}
	pos := len(prefix) + assetIDLen*posIndex
	copy(b[:], key[pos:pos+assetIDLen])
	assetID := bc.NewAssetID(b)
	return &assetID
}

func getRateFromOrderKey(key []byte, prefix []byte) float64 {
	ratePos := len(prefix) + assetIDLen*2
	return math.Float64frombits(binary.BigEndian.Uint64(key[ratePos : ratePos+rateByteLen]))
}

type tradePairData struct {
	Count int
}

type MovStore struct {
	db dbm.DB
}

func NewMovStore(db dbm.DB, height uint64, hash *bc.Hash) (*MovStore, error) {
	if value := db.Get(bestMatchStore); value == nil {
		state := &common.MovDatabaseState{Height: height, Hash: hash}
		value, err := json.Marshal(state)
		if err != nil {
			return nil, err
		}

		db.Set(bestMatchStore, value)
	}
	return &MovStore{db: db}, nil
}

func (m *MovStore) ListOrders(orderAfter *common.Order) ([]*common.Order, error) {
	if orderAfter.FromAssetID == nil || orderAfter.ToAssetID == nil {
		return nil, errors.New("assetID is nil")
	}

	orderPrefix := append(ordersPrefix, orderAfter.FromAssetID.Bytes()...)
	orderPrefix = append(orderPrefix, orderAfter.ToAssetID.Bytes()...)

	var startKey []byte
	if orderAfter.Rate > 0 {
		startKey = calcOrderKey(orderAfter.FromAssetID, orderAfter.ToAssetID, calcUTXOHash(orderAfter), orderAfter.Rate)
	}

	itr := m.db.IteratorPrefixWithStart(orderPrefix, startKey, false)
	defer itr.Release()

	var orders []*common.Order
	for txNum := 0; txNum < ordersNum && itr.Next(); txNum++ {
		movUtxo := &common.MovUtxo{}
		if err := json.Unmarshal(itr.Value(), movUtxo); err != nil {
			return nil, err
		}

		order := &common.Order{
			FromAssetID: orderAfter.FromAssetID,
			ToAssetID:   orderAfter.ToAssetID,
			Rate:        getRateFromOrderKey(itr.Key(), ordersPrefix),
			Utxo:        movUtxo,
		}
		orders = append(orders, order)
	}
	return orders, nil
}

func (m *MovStore) ProcessOrders(addOrders []*common.Order, delOreders []*common.Order, blockHeader *types.BlockHeader) error {
	if err := m.checkMovDatabaseState(blockHeader); err != nil {
		return err
	}

	batch := m.db.NewBatch()
	tradePairsCnt := make(map[common.TradePair]int)
	if err := m.addOrders(batch, addOrders, tradePairsCnt); err != nil {
		return err
	}

	m.deleteOrders(batch, delOreders, tradePairsCnt)

	if err := m.updateTradePairs(batch, tradePairsCnt); err != nil {
		return err
	}

	hash := blockHeader.Hash()
	if err := m.saveMovDatabaseState(batch, &common.MovDatabaseState{Height: blockHeader.Height, Hash: &hash}); err != nil {
		return err
	}

	batch.Write()
	return nil
}

func (m *MovStore) addOrders(batch dbm.Batch, orders []*common.Order, tradePairsCnt map[common.TradePair]int) error {
	for _, order := range orders {
		data, err := json.Marshal(order.Utxo)
		if err != nil {
			return err
		}

		key := calcOrderKey(order.FromAssetID, order.ToAssetID, calcUTXOHash(order), order.Rate)
		batch.Set(key, data)

		tradePair := common.TradePair{
			FromAssetID: order.FromAssetID,
			ToAssetID:   order.ToAssetID,
		}
		tradePairsCnt[tradePair] += 1
	}
	return nil
}

func (m *MovStore) deleteOrders(batch dbm.Batch, orders []*common.Order, tradePairsCnt map[common.TradePair]int) {
	for _, order := range orders {
		key := calcOrderKey(order.FromAssetID, order.ToAssetID, calcUTXOHash(order), order.Rate)
		batch.Delete(key)

		tradePair := common.TradePair{
			FromAssetID: order.FromAssetID,
			ToAssetID:   order.ToAssetID,
		}
		tradePairsCnt[tradePair] -= 1
	}
}

func (m *MovStore) GetMovDatabaseState() (*common.MovDatabaseState, error) {
	if value := m.db.Get(bestMatchStore); value != nil {
		state := &common.MovDatabaseState{}
		return state, json.Unmarshal(value, state)
	}

	return nil, errors.New("don't find state of mov-database")
}

func (m *MovStore) ListTradePairsWithStart(fromAssetIDAfter, toAssetIDAfter *bc.AssetID) ([]*common.TradePair, error) {
	var startKey []byte
	if fromAssetIDAfter != nil && toAssetIDAfter != nil {
		startKey = calcTradePairKey(fromAssetIDAfter, toAssetIDAfter)
	}

	itr := m.db.IteratorPrefixWithStart(tradePairsPrefix, startKey, false)
	defer itr.Release()

	var tradePairs []*common.TradePair
	for txNum := 0; txNum < tradePairsNum && itr.Next(); txNum++ {
		key := itr.Key()
		fromAssetID := getAssetIDFromTradePairKey(key, tradePairsPrefix, 0)
		toAssetID := getAssetIDFromTradePairKey(key, tradePairsPrefix, 1)

		tradePairData := &tradePairData{}
		if err := json.Unmarshal(itr.Value(), tradePairData); err != nil {
			return nil, err
		}

		tradePairs = append(tradePairs, &common.TradePair{FromAssetID: fromAssetID, ToAssetID: toAssetID, Count: tradePairData.Count})
	}
	return tradePairs, nil
}

func (m *MovStore) updateTradePairs(batch dbm.Batch, tradePairs map[common.TradePair]int) error {
	for k, v := range tradePairs {
		key := calcTradePairKey(k.FromAssetID, k.ToAssetID)
		tradePairData := &tradePairData{}
		if value := m.db.Get(key); value != nil {
			if err := json.Unmarshal(value, tradePairData); err != nil {
				return err
			}
		} else if v < 0 {
			return errors.New("don't find trade pair")
		}

		tradePairData.Count += v
		if tradePairData.Count > 0 {
			value, err := json.Marshal(tradePairData)
			if err != nil {
				return err
			}

			batch.Set(key, value)
		} else {
			batch.Delete(key)
		}
	}
	return nil
}

func (m *MovStore) checkMovDatabaseState(header *types.BlockHeader) error {
	state, err := m.GetMovDatabaseState()
	if err != nil {
		return err
	}

	blockHash := header.Hash()
	if (state.Hash.String() == header.PreviousBlockHash.String() && (state.Height+1) == header.Height) || state.Hash.String() == blockHash.String() {
		return nil
	}

	return errors.New("the status of the block is inconsistent with that of mov-database")
}

func (m *MovStore) saveMovDatabaseState(batch dbm.Batch, state *common.MovDatabaseState) error {
	value, err := json.Marshal(state)
	if err != nil {
		return err
	}

	batch.Set(bestMatchStore, value)
	return nil
}
