package database

import (
	"strconv"

	"github.com/golang/groupcache/singleflight"

	"github.com/vapor/common"
	"github.com/vapor/protocol/bc"
	"github.com/vapor/protocol/bc/types"
	"github.com/vapor/protocol/state"
)

const (
	maxCachedBlockHeaders      = 4096
	maxCachedBlockTransactions = 1024
	maxCachedBlockHashes       = 8192
	maxCachedMainChainHashes   = 8192
	maxCachedConsensusResults  = 128
)

type fillBlockHeaderFn func(hash *bc.Hash) (*types.BlockHeader, error)
type fillBlockTransactionsFn func(hash *bc.Hash) ([]*types.Tx, error)
type fillBlockHashesFn func(height uint64) ([]*bc.Hash, error)
type fillMainChainHashFn func(height uint64) (*bc.Hash, error)
type fillConsensusResultFn func(seq uint64) (*state.ConsensusResult, error)

func newCache(fillBlockHeader fillBlockHeaderFn, fillBlockTxs fillBlockTransactionsFn, fillBlockHashes fillBlockHashesFn, fillMainChainHash fillMainChainHashFn, fillConsensusResult fillConsensusResultFn) cache {
	return cache{
		lruBlockHeaders:     common.NewCache(maxCachedBlockHeaders),
		lruBlockTxs:         common.NewCache(maxCachedBlockTransactions),
		lruBlockHashes:      common.NewCache(maxCachedBlockHashes),
		lruMainChainHashes:  common.NewCache(maxCachedMainChainHashes),
		lruConsensusResults: common.NewCache(maxCachedConsensusResults),

		fillBlockHeaderFn:      fillBlockHeader,
		fillBlockTransactionFn: fillBlockTxs,
		fillBlockHashesFn:      fillBlockHashes,
		fillMainChainHashFn:    fillMainChainHash,
		fillConsensusResultFn:  fillConsensusResult,
	}
}

type cache struct {
	lruBlockHeaders     *common.Cache
	lruBlockTxs         *common.Cache
	lruBlockHashes      *common.Cache
	lruMainChainHashes  *common.Cache
	lruConsensusResults *common.Cache

	fillBlockHeaderFn      func(hash *bc.Hash) (*types.BlockHeader, error)
	fillBlockTransactionFn func(hash *bc.Hash) ([]*types.Tx, error)
	fillBlockHashesFn      func(uint64) ([]*bc.Hash, error)
	fillMainChainHashFn    func(uint64) (*bc.Hash, error)
	fillConsensusResultFn  func(seq uint64) (*state.ConsensusResult, error)

	sf singleflight.Group
}

func (c *cache) lookupBlockHeader(hash *bc.Hash) (*types.BlockHeader, error) {
	if data, ok := c.lruBlockHeaders.Get(*hash); ok {
		return data.(*types.BlockHeader), nil
	}

	blockHeader, err := c.sf.Do("BlockHeader:"+hash.String(), func() (interface{}, error) {
		blockHeader, err := c.fillBlockHeaderFn(hash)
		if err != nil {
			return nil, err
		}

		c.lruBlockHeaders.Add(blockHeader.Hash(), blockHeader)
		return blockHeader, nil
	})
	if err != nil {
		return nil, err
	}
	return blockHeader.(*types.BlockHeader), nil
}

func (c *cache) lookupBlockTxs(hash *bc.Hash) ([]*types.Tx, error) {
	if data, ok := c.lruBlockTxs.Get(*hash); ok {
		return data.([]*types.Tx), nil
	}

	blockTxs, err := c.sf.Do("BlockTxs:"+hash.String(), func() (interface{}, error) {
		blockTxs, err := c.fillBlockTransactionFn(hash)
		if err != nil {
			return nil, err
		}

		c.lruBlockTxs.Add(*hash, blockTxs)
		return blockTxs, nil
	})
	if err != nil {
		return nil, err
	}
	return blockTxs.([]*types.Tx), nil
}

func (c *cache) lookupConsensusResult(seq uint64) (*state.ConsensusResult, error) {
	if data, ok := c.lruConsensusResults.Get(seq); ok {
		return data.(*state.ConsensusResult).Fork(), nil
	}

	seqStr := strconv.FormatUint(seq, 10)
	consensusResult, err := c.sf.Do("ConsensusResult:"+seqStr, func() (interface{}, error) {
		consensusResult, err := c.fillConsensusResultFn(seq)
		if err != nil {
			return nil, err
		}

		c.lruConsensusResults.Add(consensusResult.Seq, consensusResult)
		return consensusResult, nil
	})
	if err != nil {
		return nil, err
	}
	return consensusResult.(*state.ConsensusResult).Fork(), nil
}

func (c *cache) lookupMainChainHash(height uint64) (*bc.Hash, error) {
	if hash, ok := c.lruMainChainHashes.Get(height); ok {
		return hash.(*bc.Hash), nil
	}

	heightStr := strconv.FormatUint(height, 10)
	hash, err := c.sf.Do("BlockHashByHeight:"+heightStr, func() (interface{}, error) {
		hash, err := c.fillMainChainHashFn(height)
		if err != nil {
			return nil, err
		}

		c.lruMainChainHashes.Add(height, hash)
		return hash, nil
	})
	if err != nil {
		return nil, err
	}
	return hash.(*bc.Hash), nil
}

func (c *cache) lookupBlockHashesByHeight(height uint64) ([]*bc.Hash, error) {
	if hashes, ok := c.lruBlockHashes.Get(height); ok {
		return hashes.([]*bc.Hash), nil
	}

	heightStr := strconv.FormatUint(height, 10)
	hashes, err := c.sf.Do("BlockHashesByHeight:"+heightStr, func() (interface{}, error) {
		hashes, err := c.fillBlockHashesFn(height)
		if err != nil {
			return nil, err
		}

		c.lruBlockHashes.Add(height, hashes)
		return hashes, nil
	})
	if err != nil {
		return nil, err
	}
	return hashes.([]*bc.Hash), nil
}

func (c *cache) removeBlockHeader(blockHeader *types.BlockHeader) {
	c.lruBlockHeaders.Remove(blockHeader.Hash())
}

func (c *cache) removeBlockHashes(height uint64) {
	c.lruBlockHashes.Remove(height)
}

func (c *cache) removeMainChainHash(height uint64) {
	c.lruMainChainHashes.Remove(height)
}

func (c *cache) removeConsensusResult(consensusResult *state.ConsensusResult) {
	c.lruConsensusResults.Remove(consensusResult.Seq)
}
