// 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 t8ntool

import (
	"github.com/holiman/uint256"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/empty"
	"github.com/erigontech/erigon/common/math"
	"github.com/erigontech/erigon/db/kv"
	"github.com/erigontech/erigon/db/state/execctx"
	"github.com/erigontech/erigon/execution/chain"
	"github.com/erigontech/erigon/execution/protocol/rules/ethash"
	"github.com/erigontech/erigon/execution/state"
	"github.com/erigontech/erigon/execution/tracing"
	"github.com/erigontech/erigon/execution/types"
	"github.com/erigontech/erigon/execution/types/accounts"
)

type Prestate struct {
	Env stEnv              `json:"env"`
	Pre types.GenesisAlloc `json:"pre"`
}

type ommer struct {
	Delta   uint64         `json:"delta"`
	Address common.Address `json:"address"`
}

//go:generate gencodec -type stEnv -field-override stEnvMarshaling -out gen_stenv.go
type stEnv struct {
	Coinbase         common.Address                      `json:"currentCoinbase"   gencodec:"required"`
	Difficulty       *uint256.Int                        `json:"currentDifficulty"`
	Random           *uint256.Int                        `json:"currentRandom"`
	MixDigest        common.Hash                         `json:"mixHash,omitempty"`
	ParentDifficulty *uint256.Int                        `json:"parentDifficulty"`
	GasLimit         uint64                              `json:"currentGasLimit"   gencodec:"required"`
	Number           uint64                              `json:"currentNumber"     gencodec:"required"`
	Timestamp        uint64                              `json:"currentTimestamp"  gencodec:"required"`
	ParentTimestamp  uint64                              `json:"parentTimestamp,omitempty"`
	BlockHashes      map[math.HexOrDecimal64]common.Hash `json:"blockHashes,omitempty"`
	Ommers           []ommer                             `json:"ommers,omitempty"`
	BaseFee          *uint256.Int                        `json:"currentBaseFee,omitempty"`
	ParentUncleHash  common.Hash                         `json:"parentUncleHash"`
	UncleHash        common.Hash                         `json:"uncleHash,omitempty"`
	Withdrawals      []*types.Withdrawal                 `json:"withdrawals,omitempty"`
	WithdrawalsHash  *common.Hash                        `json:"withdrawalsRoot,omitempty"`
	RequestsHash     *common.Hash                        `json:"requestsHash,omitempty"`
}

type stEnvMarshaling struct {
	Coinbase         common.UnprefixedAddress
	Difficulty       *math.HexOrDecimal256
	Random           *math.HexOrDecimal256
	ParentDifficulty *math.HexOrDecimal256
	GasLimit         math.HexOrDecimal64
	Number           math.HexOrDecimal64
	Timestamp        math.HexOrDecimal64
	ParentTimestamp  math.HexOrDecimal64
	BaseFee          *math.HexOrDecimal256
}

func MakePreState(chainRules *chain.Rules, tx kv.TemporalRwTx, sd *execctx.SharedDomains, alloc types.GenesisAlloc, blockNum, txNum uint64) (state.StateReader, state.StateWriter) {
	stateReader, stateWriter := state.NewReaderV3(sd.AsGetter(tx)), state.NewWriter(sd.AsPutDel(tx), nil, txNum)
	statedb := state.New(stateReader) //ibs
	for address, account := range alloc {
		addr := accounts.InternAddress(address)
		statedb.SetCode(addr, account.Code, tracing.CodeChangeGenesis)
		statedb.SetNonce(addr, account.Nonce, tracing.NonceChangeGenesis)
		var balance uint256.Int
		_ = balance.SetFromBig(account.Balance)
		statedb.SetBalance(addr, balance, tracing.BalanceIncreaseGenesisBalance)
		for k, v := range account.Storage {
			key := accounts.InternKey(k)
			val := uint256.NewInt(0).SetBytes(v[:])
			statedb.SetState(addr, key, *val)
		}

		if len(account.Code) > 0 || len(account.Storage) > 0 {
			statedb.SetIncarnation(addr, state.FirstContractIncarnation)
		}
	}
	// Commit and re-open to start with a clean state.
	if err := statedb.FinalizeTx(chainRules, stateWriter); err != nil {
		panic(err)
	}
	if err := statedb.CommitBlock(chainRules, stateWriter); err != nil {
		panic(err)
	}
	return stateReader, stateWriter
}

// calcDifficulty is based on ethash.CalcDifficulty. This method is used in case
// the caller does not provide an explicit difficulty, but instead provides only
// parent timestamp + difficulty.
// Note: this method only works for ethash engine.
func calcDifficulty(config *chain.Config, number, currentTime, parentTime uint64,
	parentDifficulty uint256.Int, parentUncleHash common.Hash) *uint256.Int {
	uncleHash := parentUncleHash
	if uncleHash == (common.Hash{}) {
		uncleHash = empty.UncleHash
	}
	parent := &types.Header{
		ParentHash: common.Hash{},
		UncleHash:  uncleHash,
		Difficulty: parentDifficulty,
		Time:       parentTime,
	}
	parent.Number.SetUint64(number - 1)
	diff := ethash.CalcDifficulty(config, currentTime, parent.Time, parent.Difficulty, number-1, parent.UncleHash)
	return &diff
}
