// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package tracers

import (
	"github.com/ethereum/go-ethereum/crypto"
	"code.wuban.net.cn/cmpchain/ethtracer/evm/evmtypes"
	"code.wuban.net.cn/cmpchain/ethtracer/evm/vm"
	"code.wuban.net.cn/cmpchain/ethtracer/params"
	"code.wuban.net.cn/cmpchain/ethtracer/state"
	"code.wuban.net.cn/cmpchain/ethtracer/stateless"
	"code.wuban.net.cn/cmpchain/ethtracer/tracing"
	"code.wuban.net.cn/cmpchain/ethtracer/types"
	"code.wuban.net.cn/cmpchain/ethtracer/utils"
	metatypes "github.com/CaduceusMetaverseProtocol/MetaTypes/types"
	"math/big"

	"github.com/holiman/uint256"
)

// hookedStateDB represents a statedb which emits calls to tracing-hooks
// on state operations.
type hookedStateDB struct {
	inner vm.StateDB
	hooks *tracing.Hooks
}

func (s *hookedStateDB) SetBalance(address metatypes.Address, u *uint256.Int) uint256.Int {
	return s.inner.SetBalance(address, u)
}

func (s *hookedStateDB) SetStorage(addr metatypes.Address, storage map[metatypes.Hash]metatypes.Hash) {
	s.inner.SetStorage(addr, storage)
}

func (s *hookedStateDB) SetTxContext(hash metatypes.Hash, i int) {
	s.inner.SetTxContext(hash, i)
}

// NewHookedState wraps the given stateDb with the given hooks
func NewHookedState(stateDb vm.StateDB, hooks *tracing.Hooks) *hookedStateDB {
	s := &hookedStateDB{stateDb, hooks}
	if s.hooks == nil {
		s.hooks = new(tracing.Hooks)
	}
	return s
}

func (s *hookedStateDB) CreateAccount(addr metatypes.Address) {
	s.inner.CreateAccount(addr)
}

func (s *hookedStateDB) CreateContract(addr metatypes.Address) {
	s.inner.CreateContract(addr)
}

func (s *hookedStateDB) GetBalance(addr metatypes.Address) *uint256.Int {
	return s.inner.GetBalance(addr)
}

func (s *hookedStateDB) GetNonce(addr metatypes.Address) uint64 {
	return s.inner.GetNonce(addr)
}

func (s *hookedStateDB) GetCodeHash(addr metatypes.Address) metatypes.Hash {
	return s.inner.GetCodeHash(addr)
}

func (s *hookedStateDB) GetCode(addr metatypes.Address) []byte {
	return s.inner.GetCode(addr)
}

func (s *hookedStateDB) GetCodeSize(addr metatypes.Address) int {
	return s.inner.GetCodeSize(addr)
}

func (s *hookedStateDB) AddRefund(u uint64) {
	s.inner.AddRefund(u)
}

func (s *hookedStateDB) SubRefund(u uint64) {
	s.inner.SubRefund(u)
}

func (s *hookedStateDB) GetRefund() uint64 {
	return s.inner.GetRefund()
}

func (s *hookedStateDB) GetCommittedState(addr metatypes.Address, hash metatypes.Hash) metatypes.Hash {
	return s.inner.GetCommittedState(addr, hash)
}

func (s *hookedStateDB) GetState(addr metatypes.Address, hash metatypes.Hash) metatypes.Hash {
	return s.inner.GetState(addr, hash)
}

func (s *hookedStateDB) GetStorageRoot(addr metatypes.Address) metatypes.Hash {
	return s.inner.GetStorageRoot(addr)
}

func (s *hookedStateDB) GetTransientState(addr metatypes.Address, key metatypes.Hash) metatypes.Hash {
	return s.inner.GetTransientState(addr, key)
}

func (s *hookedStateDB) SetTransientState(addr metatypes.Address, key, value metatypes.Hash) {
	s.inner.SetTransientState(addr, key, value)
}

func (s *hookedStateDB) HasSelfDestructed(addr metatypes.Address) bool {
	return s.inner.HasSelfDestructed(addr)
}

func (s *hookedStateDB) Exist(addr metatypes.Address) bool {
	return s.inner.Exist(addr)
}

func (s *hookedStateDB) Empty(addr metatypes.Address) bool {
	return s.inner.Empty(addr)
}

func (s *hookedStateDB) AddressInAccessList(addr metatypes.Address) bool {
	return s.inner.AddressInAccessList(addr)
}

func (s *hookedStateDB) SlotInAccessList(addr metatypes.Address, slot metatypes.Hash) (addressOk bool, slotOk bool) {
	return s.inner.SlotInAccessList(addr, slot)
}

func (s *hookedStateDB) AddAddressToAccessList(addr metatypes.Address) {
	s.inner.AddAddressToAccessList(addr)
}

func (s *hookedStateDB) AddSlotToAccessList(addr metatypes.Address, slot metatypes.Hash) {
	s.inner.AddSlotToAccessList(addr, slot)
}

func (s *hookedStateDB) PointCache() *utils.PointCache {
	return s.inner.PointCache()
}

func (s *hookedStateDB) Prepare(rules params.Rules, sender, coinbase metatypes.Address, dest *metatypes.Address, precompiles []metatypes.Address, txAccesses evmtypes.AccessList) {
	s.inner.Prepare(rules, sender, coinbase, dest, precompiles, txAccesses)
}

func (s *hookedStateDB) RevertToSnapshot(i int) {
	s.inner.RevertToSnapshot(i)
}

func (s *hookedStateDB) Snapshot() int {
	return s.inner.Snapshot()
}

func (s *hookedStateDB) AddPreimage(hash metatypes.Hash, bytes []byte) {
	s.inner.AddPreimage(hash, bytes)
}

func (s *hookedStateDB) Witness() *stateless.Witness {
	return s.inner.Witness()
}

func (s *hookedStateDB) AccessEvents() *state.AccessEvents {
	return s.inner.AccessEvents()
}

func (s *hookedStateDB) SubBalance(addr metatypes.Address, amount *uint256.Int, reason tracing.BalanceChangeReason) uint256.Int {
	prev := s.inner.SubBalance(addr, amount, reason)
	if s.hooks.OnBalanceChange != nil && !amount.IsZero() {
		newBalance := new(uint256.Int).Sub(&prev, amount)
		s.hooks.OnBalanceChange(addr, prev.ToBig(), newBalance.ToBig(), reason)
	}
	return prev
}

func (s *hookedStateDB) AddBalance(addr metatypes.Address, amount *uint256.Int, reason tracing.BalanceChangeReason) uint256.Int {
	prev := s.inner.AddBalance(addr, amount, reason)
	if s.hooks.OnBalanceChange != nil && !amount.IsZero() {
		newBalance := new(uint256.Int).Add(&prev, amount)
		s.hooks.OnBalanceChange(addr, prev.ToBig(), newBalance.ToBig(), reason)
	}
	return prev
}

func (s *hookedStateDB) SetNonce(address metatypes.Address, nonce uint64, reason tracing.NonceChangeReason) {
	prev := s.inner.GetNonce(address)
	s.inner.SetNonce(address, nonce, reason)
	if s.hooks.OnNonceChangeV2 != nil {
		s.hooks.OnNonceChangeV2(address, prev, nonce, reason)
	} else if s.hooks.OnNonceChange != nil {
		s.hooks.OnNonceChange(address, prev, nonce)
	}
}

func (s *hookedStateDB) SetCode(address metatypes.Address, code []byte) []byte {
	prev := s.inner.SetCode(address, code)
	if s.hooks.OnCodeChange != nil {
		prevHash := types.EmptyCodeHash
		if len(prev) != 0 {
			prevHash = metatypes.BytesToHash(crypto.Keccak256Hash(prev).Bytes())
		}
		s.hooks.OnCodeChange(address, prevHash, prev, metatypes.BytesToHash(crypto.Keccak256Hash(code).Bytes()), code)
	}
	return prev
}

func (s *hookedStateDB) SetState(address metatypes.Address, key metatypes.Hash, value metatypes.Hash) metatypes.Hash {
	prev := s.inner.SetState(address, key, value)
	if s.hooks.OnStorageChange != nil && prev != value {
		s.hooks.OnStorageChange(address, key, prev, value)
	}
	return prev
}

func (s *hookedStateDB) SelfDestruct(address metatypes.Address) uint256.Int {
	var prevCode []byte
	var prevCodeHash metatypes.Hash

	if s.hooks.OnCodeChange != nil {
		prevCode = s.inner.GetCode(address)
		prevCodeHash = s.inner.GetCodeHash(address)
	}

	prev := s.inner.SelfDestruct(address)

	if s.hooks.OnBalanceChange != nil && !prev.IsZero() {
		s.hooks.OnBalanceChange(address, prev.ToBig(), new(big.Int), tracing.BalanceDecreaseSelfdestruct)
	}

	if s.hooks.OnCodeChange != nil && len(prevCode) > 0 {
		s.hooks.OnCodeChange(address, prevCodeHash, prevCode, types.EmptyCodeHash, nil)
	}

	return prev
}

func (s *hookedStateDB) SelfDestruct6780(address metatypes.Address) (uint256.Int, bool) {
	var prevCode []byte
	var prevCodeHash metatypes.Hash

	if s.hooks.OnCodeChange != nil {
		prevCodeHash = s.inner.GetCodeHash(address)
		prevCode = s.inner.GetCode(address)
	}

	prev, changed := s.inner.SelfDestruct6780(address)

	if s.hooks.OnBalanceChange != nil && changed && !prev.IsZero() {
		s.hooks.OnBalanceChange(address, prev.ToBig(), new(big.Int), tracing.BalanceDecreaseSelfdestruct)
	}

	if s.hooks.OnCodeChange != nil && changed && len(prevCode) > 0 {
		s.hooks.OnCodeChange(address, prevCodeHash, prevCode, types.EmptyCodeHash, nil)
	}

	return prev, changed
}

func (s *hookedStateDB) AddLog(log *evmtypes.Log) {
	// The inner will modify the log (add fields), so invoke that first
	s.inner.AddLog(log)
	if s.hooks.OnLog != nil {
		s.hooks.OnLog(log)
	}
}

func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) {
	defer s.inner.Finalise(deleteEmptyObjects)
	if s.hooks.OnBalanceChange == nil {
		return
	}
	// todo: vicotor implement it.
	//for addr := range s.inner.journal.dirties {
	//	obj := s.inner.stateObjects[addr]
	//	if obj != nil && obj.selfDestructed {
	//		// If ether was sent to account post-selfdestruct it is burnt.
	//		if bal := obj.Balance(); bal.Sign() != 0 {
	//			s.hooks.OnBalanceChange(addr, bal.ToBig(), new(big.Int), tracing.BalanceDecreaseSelfdestructBurn)
	//		}
	//	}
	//}
}
