// Copyright 2020 The go-ethereum Authors
// (original work)
// Copyright 2024 The Erigon Authors
// (modifications)
// This file is part of Erigon.
//
// Erigon 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.
//
// Erigon 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 Erigon. If not, see <http://www.gnu.org/licenses/>.

package vm

import (
	"errors"
	"fmt"

	"github.com/holiman/uint256"

	"github.com/erigontech/erigon/common/dbg"
	"github.com/erigontech/erigon/common/math"
	"github.com/erigontech/erigon/execution/protocol/params"
	"github.com/erigontech/erigon/execution/tracing"
	"github.com/erigontech/erigon/execution/types/accounts"
)

func makeGasSStoreFunc(clearingRefund uint64) gasFunc {
	return func(evm *EVM, callContext *CallContext, scopeGas uint64, memorySize uint64) (uint64, error) {
		if evm.readOnly {
			return 0, ErrWriteProtection
		}
		// If we fail the minimum gas availability invariant, fail (0)
		if scopeGas <= params.SstoreSentryGasEIP2200 {
			return 0, errors.New("not enough gas for reentrancy sentry")
		}
		// Gas sentry honoured, do the actual gas calculation based on the stored value
		var (
			y, x    = callContext.Stack.Back(1), callContext.Stack.peek()
			slot    = accounts.InternKey(x.Bytes32())
			current uint256.Int
			cost    = uint64(0)
		)

		current, _ = evm.IntraBlockState().GetState(callContext.Address(), slot)
		// If the caller cannot afford the cost, this change will be rolled back
		if _, slotMod := evm.IntraBlockState().AddSlotToAccessList(callContext.Address(), slot); slotMod {
			cost = params.ColdSloadCostEIP2929
		}
		var value uint256.Int
		value.Set(y)

		if current.Eq(&value) { // noop (1)
			// EIP 2200 original clause:
			//		return params.SloadGasEIP2200, nil
			return cost + params.WarmStorageReadCostEIP2929, nil // SLOAD_GAS
		}

		slotCommited := accounts.InternKey(x.Bytes32())
		var original, _ = evm.IntraBlockState().GetCommittedState(callContext.Address(), slotCommited)
		if original.Eq(&current) {
			if original.IsZero() { // create slot (2.1.1)
				return cost + params.SstoreSetGasEIP2200, nil
			}
			if value.IsZero() { // delete slot (2.1.2b)
				evm.IntraBlockState().AddRefund(clearingRefund)
			}
			// EIP-2200 original clause:
			//		return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2)
			return cost + (params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929), nil // write existing slot (2.1.2)
		}
		if !original.IsZero() {
			if current.IsZero() { // recreate slot (2.2.1.1)
				evm.IntraBlockState().SubRefund(clearingRefund)
			} else if value.IsZero() { // delete slot (2.2.1.2)
				evm.IntraBlockState().AddRefund(clearingRefund)
			}
		}
		if original.Eq(&value) {
			if original.IsZero() { // reset to original inexistent slot (2.2.2.1)
				// EIP 2200 Original clause:
				//evm.StateDB.AddRefund(params.SstoreSetGasEIP2200 - params.SloadGasEIP2200)
				evm.IntraBlockState().AddRefund(params.SstoreSetGasEIP2200 - params.WarmStorageReadCostEIP2929)
			} else { // reset to original existing slot (2.2.2.2)
				// EIP 2200 Original clause:
				//	evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.SloadGasEIP2200)
				// - SSTORE_RESET_GAS redefined as (5000 - COLD_SLOAD_COST)
				// - SLOAD_GAS redefined as WARM_STORAGE_READ_COST
				// Final: (5000 - COLD_SLOAD_COST) - WARM_STORAGE_READ_COST
				evm.IntraBlockState().AddRefund((params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929) - params.WarmStorageReadCostEIP2929)
			}
		}
		// EIP-2200 original clause:
		//return params.SloadGasEIP2200, nil // dirty update (2.2)
		return cost + params.WarmStorageReadCostEIP2929, nil // dirty update (2.2)
	}
}

// gasSLoadEIP2929 calculates dynamic gas for SLOAD according to EIP-2929
// For SLOAD, if the (address, storage_key) pair (where address is the address of the contract
// whose storage is being read) is not yet in accessed_storage_keys,
// charge 2100 gas and add the pair to accessed_storage_keys.
// If the pair is already in accessed_storage_keys, charge 100 gas.
func gasSLoadEIP2929(evm *EVM, callContext *CallContext, scopeGas uint64, memorySize uint64) (uint64, error) {
	loc := callContext.Stack.peek()
	// If the caller cannot afford the cost, this change will be rolled back
	// If he does afford it, we can skip checking the same thing later on, during execution
	if _, slotMod := evm.IntraBlockState().AddSlotToAccessList(callContext.Address(), accounts.InternKey(loc.Bytes32())); slotMod {
		return params.ColdSloadCostEIP2929, nil
	}
	return params.WarmStorageReadCostEIP2929, nil
}

// gasExtCodeCopyEIP2929 implements extcodecopy according to EIP-2929
// EIP spec:
// > If the target is not in accessed_addresses,
// > charge COLD_ACCOUNT_ACCESS_COST gas, and add the address to accessed_addresses.
// > Otherwise, charge WARM_STORAGE_READ_COST gas.
func gasExtCodeCopyEIP2929(evm *EVM, callContext *CallContext, scopeGas uint64, memorySize uint64) (uint64, error) {
	// memory expansion first (dynamic part of pre-2929 implementation)
	gas, err := gasExtCodeCopy(evm, callContext, scopeGas, memorySize)
	if err != nil {
		return 0, err
	}
	addr := accounts.InternAddress(callContext.Stack.peek().Bytes20())
	callContext.callAddrTmp = addr
	// Check slot presence in the access list
	if evm.IntraBlockState().AddAddressToAccessList(addr) {
		var overflow bool
		// We charge (cold-warm), since 'warm' is already charged as constantGas
		if gas, overflow = math.SafeAdd(gas, params.ColdAccountAccessCostEIP2929-params.WarmStorageReadCostEIP2929); overflow {
			return 0, ErrGasUintOverflow
		}
		return gas, nil
	}
	return gas, nil
}

// gasEip2929AccountCheck checks whether the first stack item (as address) is present in the access list.
// If it is, this method returns '0', otherwise 'cold-warm' gas, presuming that the opcode using it
// is also using 'warm' as constant factor.
// This method is used by:
// - extcodehash,
// - extcodesize,
// - (ext) balance
func gasEip2929AccountCheck(evm *EVM, callContext *CallContext, scopeGas uint64, memorySize uint64) (uint64, error) {
	addr := accounts.InternAddress(callContext.Stack.peek().Bytes20())
	callContext.callAddrTmp = addr
	// If the caller cannot afford the cost, this change will be rolled back
	if evm.IntraBlockState().AddAddressToAccessList(addr) {
		// The warm storage read cost is already charged as constantGas
		return params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929, nil
	}
	return 0, nil
}

func makeCallVariantGasCallEIP2929(oldCalculator gasFunc) gasFunc {
	return func(evm *EVM, callContext *CallContext, scopeGas uint64, memorySize uint64) (uint64, error) {
		addr := accounts.InternAddress(callContext.Stack.Back(1).Bytes20())
		callContext.callAddrTmp = addr
		// The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so
		// the cost to charge for cold access, if any, is Cold - Warm
		coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
		warmAccess := evm.IntraBlockState().AddressInAccessList(addr)
		if !warmAccess {
			// Charge the remaining difference here already, to correctly calculate available
			// gas for call
			if _, ok := useGas(scopeGas, coldCost, evm.Config().Tracer, tracing.GasChangeCallStorageColdAccess); !ok {
				return 0, ErrOutOfGas
			}
			evm.IntraBlockState().AddAddressToAccessList(addr)
			scopeGas -= coldCost
		}

		// Now call the old calculator, which takes into account
		// - create new account
		// - transfer value
		// - memory expansion
		// - 63/64ths rule
		gas, err := oldCalculator(evm, callContext, scopeGas, memorySize)
		if warmAccess || err != nil {
			return gas, err
		}
		// In case of a cold access, we temporarily add the cold charge back, and also
		// add it to the returned gas. By adding it to the return, it will be charged
		// outside of this function, as part of the dynamic gas, and that will make it
		// also become correctly reported to tracers.
		return gas + coldCost, nil
	}
}

var (
	gasCallEIP2929         = makeCallVariantGasCallEIP2929(gasCall)
	gasDelegateCallEIP2929 = makeCallVariantGasCallEIP2929(gasDelegateCall)
	gasStaticCallEIP2929   = makeCallVariantGasCallEIP2929(gasStaticCall)
	gasCallCodeEIP2929     = makeCallVariantGasCallEIP2929(gasCallCode)
	gasSelfdestructEIP2929 = makeSelfdestructGasFn(true)
	// gasSelfdestructEIP3529 implements the changes in EIP-2539 (no refunds)
	gasSelfdestructEIP3529 = makeSelfdestructGasFn(false)

	// gasSStoreEIP2929 implements gas cost for SSTORE according to EIP-2929
	//
	// When calling SSTORE, check if the (address, storage_key) pair is in accessed_storage_keys.
	// If it is not, charge an additional COLD_SLOAD_COST gas, and add the pair to accessed_storage_keys.
	// Additionally, modify the parameters defined in EIP 2200 as follows:
	//
	// Parameter 	Old value 	New value
	// SLOAD_GAS 	800 	= WARM_STORAGE_READ_COST
	// SSTORE_RESET_GAS 	5000 	5000 - COLD_SLOAD_COST
	//
	//The other parameters defined in EIP 2200 are unchanged.
	// see gasSStoreEIP2200(...) in core/vm/gas_table.go for more info about how EIP 2200 is specified
	gasSStoreEIP2929 = makeGasSStoreFunc(params.SstoreClearsScheduleRefundEIP2200)

	// gasSStoreEIP2539 implements gas cost for SSTORE according to EPI-2539
	// Replace `SSTORE_CLEARS_SCHEDULE` with `SSTORE_RESET_GAS + ACCESS_LIST_STORAGE_KEY_COST` (4,800)
	gasSStoreEIP3529 = makeGasSStoreFunc(params.SstoreClearsScheduleRefundEIP3529)
)

// makeSelfdestructGasFn can create the selfdestruct dynamic gas function for EIP-2929 and EIP-2539
func makeSelfdestructGasFn(refundsEnabled bool) gasFunc {
	gasFunc := func(evm *EVM, callContext *CallContext, scopeGas uint64, memorySize uint64) (uint64, error) {
		var (
			gas     uint64
			address = accounts.InternAddress(callContext.Stack.peek().Bytes20())
		)
		callContext.callAddrTmp = address
		// If the caller cannot afford the cost, this change will be rolled back
		if !evm.IntraBlockState().AddressInAccessList(address) {
			gas = params.ColdAccountAccessCostEIP2929
			if _, ok := useGas(scopeGas, gas, evm.Config().Tracer, tracing.GasChangeCallStorageColdAccess); !ok {
				return 0, ErrOutOfGas
			}
			evm.IntraBlockState().AddAddressToAccessList(address)
		}

		// if empty and transfers value
		empty, err := evm.IntraBlockState().Empty(address)
		if err != nil {
			return 0, err
		}
		balance, err := evm.IntraBlockState().GetBalance(callContext.Address())
		if err != nil {
			return 0, err
		}
		// Record the beneficiary address access for BAL tracking when the
		// contract has non-zero balance.  A zero-balance selfdestruct does
		// not transfer value, so the beneficiary should not appear in the
		// block access list.  Skip in read-only context (STATICCALL) where
		// SELFDESTRUCT will be rejected by ErrWriteProtection.
		if !evm.readOnly && !balance.IsZero() {
			evm.IntraBlockState().MarkAddressAccess(address, false)
		}
		// When balance is zero OR we're in a read-only (STATICCALL) context,
		// and the beneficiary differs from self, mark the beneficiary's reads
		// as internal.  In both cases the Empty() call above recorded versioned
		// reads for the beneficiary purely for gas calculation — no value is
		// actually transferred (zero balance) or SELFDESTRUCT will be rejected
		// (read-only).  Skip when beneficiary == self to avoid incorrectly
		// marking the contract's own legitimate reads.
		if (balance.IsZero() || evm.readOnly) && address != callContext.Address() {
			evm.IntraBlockState().MarkReadsInternal(address)
		}
		if empty && !balance.IsZero() {
			gas += params.CreateBySelfdestructGas
		}

		hasSelfdestructed, err := evm.IntraBlockState().HasSelfdestructed(callContext.Address())
		if err != nil {
			return 0, err
		}
		if refundsEnabled && !hasSelfdestructed {
			evm.IntraBlockState().AddRefund(params.SelfdestructRefundGas)
		}
		return gas, nil
	}
	return gasFunc
}

var (
	gasCallEIP7702         = makeCallVariantGasCallEIP7702(statelessGasCall, statefulGasCall)
	gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(statelessGasDelegateCall, statefulGasDelegateCall)
	gasStaticCallEIP7702   = makeCallVariantGasCallEIP7702(statelessGasStaticCall, statefulGasStaticCall)
	gasCallCodeEIP7702     = makeCallVariantGasCallEIP7702(statelessGasCallCode, statefulGasCallCode)
)

func makeCallVariantGasCallEIP7702(statelessCalculator statelessGasFunc, statefulCalculator statefulGasFunc) gasFunc {
	return func(evm *EVM, callContext *CallContext, availableGas uint64, memorySize uint64) (uint64, error) {
		addr := accounts.InternAddress(callContext.Stack.Back(1).Bytes20())
		callContext.callAddrTmp = addr
		// Check slot presence in the access list
		var gas uint64
		var accessGas uint64
		if !evm.intraBlockState.AddressInAccessList(addr) {
			// The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so
			// the cost to charge for cold access, if any, is Cold - Warm
			accessGas = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
			// Charge the remaining difference here already, to correctly calculate available
			// gas for call
			if availableGas < accessGas {
				return 0, ErrOutOfGas
			}

			evm.intraBlockState.AddAddressToAccessList(addr)
		}

		// Call the old calculator, which takes into account
		// - create new account
		// - transfer value
		// - memory expansion
		statelessBaseGas, transfersValue, err := statelessCalculator(evm, callContext, availableGas, memorySize, false)
		if err != nil {
			return 0, err
		}
		if statelessGas, overflow := math.SafeAdd(statelessBaseGas, accessGas); overflow {
			return 0, ErrGasUintOverflow
		} else if availableGas < statelessGas {
			return 0, ErrOutOfGas
		}

		statefulBaseGas, err := statefulCalculator(evm, callContext, statelessBaseGas, availableGas-accessGas, transfersValue)
		if err != nil {
			return 0, err
		}
		var overflow bool
		if gas, overflow = math.SafeAdd(gas, accessGas); overflow {
			return 0, ErrGasUintOverflow
		}
		if gas, overflow = math.SafeAdd(gas, statefulBaseGas); overflow {
			return 0, ErrGasUintOverflow
		}
		if availableGas < gas {
			return 0, ErrOutOfGas
		}

		// Check if code is a delegation and if so, charge for resolution.
		dd, ok, err := evm.intraBlockState.GetDelegatedDesignation(addr)
		if err != nil {
			return 0, err
		}

		var delegationGas uint64
		if ok {
			if !evm.intraBlockState.AddressInAccessList(dd) {
				delegationGas = params.ColdAccountAccessCostEIP2929
			} else {
				delegationGas = params.WarmStorageReadCostEIP2929
			}
			_, err := evm.intraBlockState.GetCode(addr)
			if err != nil {
				return 0, err
			}
			if gas, overflow = math.SafeAdd(gas, delegationGas); overflow {
				return 0, ErrGasUintOverflow
			}
			if availableGas < gas {
				return 0, ErrOutOfGas
			}
			evm.intraBlockState.AddAddressToAccessList(dd)
		}

		if availableGas-accessGas-delegationGas < statefulBaseGas {
			return 0, ErrOutOfGas
		}

		// Call the old calculator, which takes into account
		// - 63/64ths rule
		callGas, err := calcCallGas(evm, callContext, availableGas-accessGas-delegationGas, statefulBaseGas)
		if err != nil {
			return 0, err
		}

		if dbg.TraceDynamicGas && evm.intraBlockState.Trace() {
			fmt.Printf("%d (%d.%d) Variant Gas: base %d, access: %d, delegation: %d, call: %d\n",
				evm.intraBlockState.BlockNumber(), evm.intraBlockState.TxIndex(), evm.intraBlockState.Incarnation(),
				statefulBaseGas, accessGas, delegationGas, callGas)
		}
		if gas, overflow = math.SafeAdd(gas, callGas); overflow {
			return 0, ErrGasUintOverflow
		}

		return gas, nil
	}
}
