package cachekv_test

import (
	fmt "fmt"
	"testing"

	dbm "github.com/cosmos/cosmos-db"
	"github.com/stretchr/testify/require"

	"cosmossdk.io/store/cachekv"
	"cosmossdk.io/store/dbadapter"
	"cosmossdk.io/store/types"
)

func DoBenchmarkDeepCacheStack(b *testing.B, depth int) {
	b.Helper()
	db := dbm.NewMemDB()
	initialStore := cachekv.NewStore(dbadapter.Store{DB: db})

	nItems := 20
	for i := 0; i < nItems; i++ {
		initialStore.Set([]byte(fmt.Sprintf("hello%03d", i)), []byte{0})
	}

	var stack CacheStack
	stack.Reset(initialStore)

	for i := 0; i < depth; i++ {
		stack.Snapshot()

		store := stack.CurrentStore()
		store.Set([]byte(fmt.Sprintf("hello%03d", i)), []byte{byte(i)})
	}

	store := stack.CurrentStore()

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		it := store.Iterator(nil, nil)
		items := make([][]byte, 0, nItems)
		for ; it.Valid(); it.Next() {
			items = append(items, it.Key())
			it.Value()
		}
		it.Close()
		require.Equal(b, nItems, len(items))
	}
}

func BenchmarkDeepCacheStack1(b *testing.B) {
	DoBenchmarkDeepCacheStack(b, 1)
}

func BenchmarkDeepCacheStack3(b *testing.B) {
	DoBenchmarkDeepCacheStack(b, 3)
}

func BenchmarkDeepCacheStack10(b *testing.B) {
	DoBenchmarkDeepCacheStack(b, 10)
}

func BenchmarkDeepCacheStack13(b *testing.B) {
	DoBenchmarkDeepCacheStack(b, 13)
}

// CacheStack manages a stack of nested cache store to
// support the evm `StateDB`'s `Snapshot` and `RevertToSnapshot` methods.
type CacheStack struct {
	initialStore types.CacheKVStore
	// Context of the initial state before transaction execution.
	// It's the context used by `StateDB.CommitedState`.
	cacheStores []types.CacheKVStore
}

// CurrentContext returns the top context of cached stack,
// if the stack is empty, returns the initial context.
func (cs *CacheStack) CurrentStore() types.CacheKVStore {
	l := len(cs.cacheStores)
	if l == 0 {
		return cs.initialStore
	}
	return cs.cacheStores[l-1]
}

// Reset sets the initial context and clear the cache context stack.
func (cs *CacheStack) Reset(initialStore types.CacheKVStore) {
	cs.initialStore = initialStore
	cs.cacheStores = nil
}

// IsEmpty returns true if the cache context stack is empty.
func (cs *CacheStack) IsEmpty() bool {
	return len(cs.cacheStores) == 0
}

// Commit commits all the cached contexts from top to bottom in order and clears the stack by setting an empty slice of cache contexts.
func (cs *CacheStack) Commit() {
	// commit in order from top to bottom
	for i := len(cs.cacheStores) - 1; i >= 0; i-- {
		cs.cacheStores[i].Write()
	}
	cs.cacheStores = nil
}

// CommitToRevision commit the cache after the target revision,
// to improve efficiency of db operations.
func (cs *CacheStack) CommitToRevision(target int) error {
	if target < 0 || target >= len(cs.cacheStores) {
		return fmt.Errorf("snapshot index %d out of bound [%d..%d)", target, 0, len(cs.cacheStores))
	}

	// commit in order from top to bottom
	for i := len(cs.cacheStores) - 1; i > target; i-- {
		cs.cacheStores[i].Write()
	}
	cs.cacheStores = cs.cacheStores[0 : target+1]

	return nil
}

// Snapshot pushes a new cached context to the stack,
// and returns the index of it.
func (cs *CacheStack) Snapshot() int {
	cs.cacheStores = append(cs.cacheStores, cachekv.NewStore(cs.CurrentStore()))
	return len(cs.cacheStores) - 1
}

// RevertToSnapshot pops all the cached contexts after the target index (inclusive).
// the target should be snapshot index returned by `Snapshot`.
// This function panics if the index is out of bounds.
func (cs *CacheStack) RevertToSnapshot(target int) {
	if target < 0 || target >= len(cs.cacheStores) {
		panic(fmt.Errorf("snapshot index %d out of bound [%d..%d)", target, 0, len(cs.cacheStores)))
	}
	cs.cacheStores = cs.cacheStores[:target]
}
