package state

import (
	"encoding/binary"

	"github.com/cosmos/cosmos-sdk/store"
	ethcommon "github.com/ethereum/go-ethereum/common"
	ethdb "github.com/ethereum/go-ethereum/ethdb"
	ethtrie "github.com/ethereum/go-ethereum/trie"
)

const (
	versionLen = 8
)

// Trie implements the Ethereum state.Trie interface.
type Trie struct {
	// accountsCache contains all the accounts in memory to persit when
	// committing the trie. A CacheKVStore is used to provide deterministic
	// ordering.
	accountsCache store.CacheKVStore
	// storageCache contains all the contract storage in memory to persit when
	// committing the trie. A CacheKVStore is used to provide deterministic
	// ordering.
	storageCache store.CacheKVStore

	// Store is an IAVL KV store that is part of a larger store except it used
	// for a specific prefix. It will either be an accountsCache or a
	// storageCache.
	store store.KVStore

	// prefix is a static prefix used for persistence operations where the
	// storage data is a contract state. This is to prevent key collisions
	// since the IAVL tree is used for all contract state.
	prefix []byte

	// empty reflects if there exists any data in the tree
	empty bool

	// root is the encoding of an IAVL tree root (version)
	root ethcommon.Hash

	ethTrieDB *ethtrie.Database
}

// prefixKey returns a composite key composed of a static prefix and a given
// key. This is used in situations where the storage data is contract state and
// the underlying structure to store said state is a single IAVL tree. To
// prevent collision, a static prefix is used.
func (t *Trie) prefixKey(key []byte) []byte {
	compositeKey := make([]byte, len(t.prefix)+len(key))

	copy(compositeKey, t.prefix)
	copy(compositeKey[len(t.prefix):], key)

	return compositeKey
}

// TryGet implements the Ethereum state.Trie interface. It returns the value
// for key stored in the trie. The value bytes must not be modified by the
// caller.
func (t *Trie) TryGet(key []byte) ([]byte, error) {
	if t.prefix != nil {
		key = t.prefixKey(key)
	}

	return t.store.Get(key), nil
}

// TryUpdate implements the Ethereum state.Trie interface. It associates a
// given key with a value in the trie. Subsequent calls to Get will return a
// value. It also marks the tree as not empty.
//
// CONTRACT: The order of insertions must be deterministic due to the nature of
// the IAVL tree. Since a CacheKVStore is used as the storage type, the keys
// will be sorted giving us a deterministic ordering.
func (t *Trie) TryUpdate(key, value []byte) error {
	t.empty = false

	if t.prefix != nil {
		key = t.prefixKey(key)
	}

	t.store.Set(key, value)
	return nil
}

// TryDelete implements the Ethereum state.Trie interface. It removes any
// existing value for a given key from the trie.
//
// CONTRACT: The order of deletions must be deterministic due to the nature of
// the IAVL tree. Since a CacheKVStore is used as the storage type, the keys
// will be sorted giving us a deterministic ordering.
func (t *Trie) TryDelete(key []byte) error {
	if t.prefix != nil {
		key = t.prefixKey(key)
	}

	t.store.Delete(key)
	return nil
}

// Commit implements the Ethereum state.Trie interface. It persists transient
// state. State is held by a merkelized multi-store IAVL tree. Commitment will
// only occur through an account trie, in other words, when the prefix of the
// trie is nil. In such a case, if either the accountCache or the storageCache
// are not nil, they are persisted. In addition, all the mappings of
// codeHash => code are also persisted. All these operations are performed in a
// deterministic order. Transient state is built up in a CacheKVStore. Finally,
// a root hash is returned or an error if any operation fails.
//
// CONTRACT: The root is an encoded IAVL tree version and each new commitment
// increments the version by one.
func (t *Trie) Commit(_ ethtrie.LeafCallback) (ethcommon.Hash, error) {
	if t.empty {
		return ethcommon.Hash{}, nil
	}

	newRoot := rootHashFromVersion(versionFromRootHash(t.root) + 1)

	if t.prefix == nil {
		if t.accountsCache != nil {
			t.accountsCache.Write()
			t.accountsCache = nil
		}

		if t.storageCache != nil {
			t.storageCache.Write()
			t.storageCache = nil
		}

		// persist the mappings of codeHash => code
		for _, n := range t.ethTrieDB.Nodes() {
			if err := t.ethTrieDB.Commit(n, false); err != nil {
				return ethcommon.Hash{}, err
			}
		}
	}

	t.root = newRoot
	return newRoot, nil
}

// Hash implements the Ethereum state.Trie interface. It returns the state root
// of the Trie which is an encoding of the underlying IAVL tree.
//
// CONTRACT: The root is an encoded IAVL tree version.
func (t *Trie) Hash() ethcommon.Hash {
	return t.root
}

// NodeIterator implements the Ethereum state.Trie interface. Such a node
// iterator is used primarily for the implementation of RPC API functions. It
// performs a no-op.
//
// TODO: Determine if we need to implement such functionality for an IAVL tree.
// This will ultimately be related to if we want to support web3.
func (t *Trie) NodeIterator(startKey []byte) ethtrie.NodeIterator {
	return nil
}

// GetKey implements the Ethereum state.Trie interface. Since the IAVL does not
// need to store preimages of keys, a simple identity can be returned.
func (t *Trie) GetKey(key []byte) []byte {
	return key
}

// Prove implements the Ethereum state.Trie interface. It writes a Merkle proof
// to a ethdb.Putter, proofDB, for a given key starting at fromLevel.
//
// TODO: Determine how to integrate this with Cosmos SDK to provide such
// proofs.
func (t *Trie) Prove(key []byte, fromLevel uint, proofDB ethdb.Putter) error {
	return nil
}

// versionFromRootHash returns a Cosmos SDK IAVL version from an Ethereum state
// root hash.
//
// CONTRACT: The encoded version is the eight MSB bytes of the root hash.
func versionFromRootHash(root ethcommon.Hash) int64 {
	return int64(binary.BigEndian.Uint64(root[:versionLen]))
}

// rootHashFromVersion returns a state root hash from a Cosmos SDK IAVL
// version.
func rootHashFromVersion(version int64) (root ethcommon.Hash) {
	binary.BigEndian.PutUint64(root[:versionLen], uint64(version))
	return
}
