// Copyright 2014 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.

// evm executes EVM code snippets.
package main

import (
	"fmt"
	"io/fs"
	"os"
	"path/filepath"

	"github.com/ethereum/go-ethereum/cmd/evm/internal/t8ntool"
	"github.com/ethereum/go-ethereum/core/state"
	"github.com/ethereum/go-ethereum/core/tracing"
	"github.com/ethereum/go-ethereum/eth/tracers/logger"
	"github.com/ethereum/go-ethereum/internal/debug"
	"github.com/ethereum/go-ethereum/internal/flags"
	"github.com/urfave/cli/v2"

	// Force-load the tracer engines to trigger registration
	_ "github.com/ethereum/go-ethereum/eth/tracers/js"
	_ "github.com/ethereum/go-ethereum/eth/tracers/native"
)

// Some other nice-to-haves:
// * accumulate traces into an object to bundle with test
// * write tx identifier for trace before hand (blocktest only)
// * combine blocktest and statetest runner logic using unified test interface

const traceCategory = "TRACING"

var (
	// Test running flags.
	RunFlag = &cli.StringFlag{
		Name:  "run",
		Value: ".*",
		Usage: "Run only those tests matching the regular expression.",
	}
	BenchFlag = &cli.BoolFlag{
		Name:     "bench",
		Usage:    "benchmark the execution",
		Category: flags.VMCategory,
	}
	FuzzFlag = &cli.BoolFlag{
		Name:     "fuzz",
		Usage:    "adapts output format for fuzzing",
		Category: flags.VMCategory,
	}
	WitnessCrossCheckFlag = &cli.BoolFlag{
		Name:    "cross-check",
		Aliases: []string{"xc"},
		Usage:   "Cross-check stateful execution against stateless, verifying the witness generation.",
	}

	// Debugging flags.
	DumpFlag = &cli.BoolFlag{
		Name:  "dump",
		Usage: "dumps the state after the run",
	}
	HumanReadableFlag = &cli.BoolFlag{
		Name:  "human",
		Usage: "\"Human-readable\" output",
	}
	StatDumpFlag = &cli.BoolFlag{
		Name:  "statdump",
		Usage: "displays stack and heap memory information",
	}

	// Tracing flags.
	TraceFlag = &cli.BoolFlag{
		Name:     "trace",
		Usage:    "Enable tracing and output trace log.",
		Category: traceCategory,
	}
	TraceFormatFlag = &cli.StringFlag{
		Name:     "trace.format",
		Usage:    "Trace output format to use (json|struct|md)",
		Value:    "json",
		Category: traceCategory,
	}
	TraceDisableMemoryFlag = &cli.BoolFlag{
		Name:     "trace.nomemory",
		Aliases:  []string{"nomemory"},
		Value:    true,
		Usage:    "disable memory output",
		Category: traceCategory,
	}
	TraceDisableStackFlag = &cli.BoolFlag{
		Name:     "trace.nostack",
		Aliases:  []string{"nostack"},
		Usage:    "disable stack output",
		Category: traceCategory,
	}
	TraceDisableStorageFlag = &cli.BoolFlag{
		Name:     "trace.nostorage",
		Aliases:  []string{"nostorage"},
		Usage:    "disable storage output",
		Category: traceCategory,
	}
	TraceDisableReturnDataFlag = &cli.BoolFlag{
		Name:     "trace.noreturndata",
		Aliases:  []string{"noreturndata"},
		Value:    true,
		Usage:    "enable return data output",
		Category: traceCategory,
	}

	// Deprecated flags.
	DebugFlag = &cli.BoolFlag{
		Name:     "debug",
		Usage:    "output full trace logs (deprecated)",
		Hidden:   true,
		Category: traceCategory,
	}
	MachineFlag = &cli.BoolFlag{
		Name:     "json",
		Usage:    "output trace logs in machine readable format, json (deprecated)",
		Hidden:   true,
		Category: traceCategory,
	}
)

// Command definitions.
var (
	stateTransitionCommand = &cli.Command{
		Name:    "transition",
		Aliases: []string{"t8n"},
		Usage:   "Executes a full state transition",
		Action:  t8ntool.Transition,
		Flags: []cli.Flag{
			t8ntool.TraceFlag,
			t8ntool.TraceTracerFlag,
			t8ntool.TraceTracerConfigFlag,
			t8ntool.TraceEnableMemoryFlag,
			t8ntool.TraceDisableStackFlag,
			t8ntool.TraceEnableReturnDataFlag,
			t8ntool.TraceEnableCallFramesFlag,
			t8ntool.OutputBasedir,
			t8ntool.OutputAllocFlag,
			t8ntool.OutputBTFlag,
			t8ntool.OutputResultFlag,
			t8ntool.OutputBodyFlag,
			t8ntool.InputAllocFlag,
			t8ntool.InputEnvFlag,
			t8ntool.InputBTFlag,
			t8ntool.InputTxsFlag,
			t8ntool.ForknameFlag,
			t8ntool.ChainIDFlag,
			t8ntool.RewardFlag,
			t8ntool.OpcodeCountFlag,
		},
	}

	verkleCommand = &cli.Command{
		Name:    "verkle",
		Aliases: []string{"vkt"},
		Usage:   "Binary Trie helpers",
		Subcommands: []*cli.Command{
			{
				Name:    "tree-keys",
				Aliases: []string{"v"},
				Usage:   "compute a set of binary trie keys, given their source addresses and optional slot numbers",
				Action:  t8ntool.BinKeys,
				Flags: []cli.Flag{
					t8ntool.InputAllocFlag,
				},
			},
			{
				Name:    "single-key",
				Aliases: []string{"vk"},
				Usage:   "compute the binary trie key given an address and optional slot number",
				Action:  t8ntool.BinKey,
			},
			{
				Name:    "code-chunk-key",
				Aliases: []string{"vck"},
				Usage:   "compute the binary trie key given an address and chunk number",
				Action:  t8ntool.BinaryCodeChunkKey,
			},
			{
				Name:    "chunkify-code",
				Aliases: []string{"vcc"},
				Usage:   "chunkify a given bytecode for a binary trie",
				Action:  t8ntool.BinaryCodeChunkCode,
			},
			{
				Name:    "state-root",
				Aliases: []string{"vsr"},
				Usage:   "compute the state-root of a binary trie for the given alloc",
				Action:  t8ntool.BinTrieRoot,
				Flags: []cli.Flag{
					t8ntool.InputAllocFlag,
				},
			},
		},
	}

	transactionCommand = &cli.Command{
		Name:    "transaction",
		Aliases: []string{"t9n"},
		Usage:   "Performs transaction validation",
		Action:  t8ntool.Transaction,
		Flags: []cli.Flag{
			t8ntool.InputTxsFlag,
			t8ntool.ChainIDFlag,
			t8ntool.ForknameFlag,
		},
	}

	blockBuilderCommand = &cli.Command{
		Name:    "block-builder",
		Aliases: []string{"b11r"},
		Usage:   "Builds a block",
		Action:  t8ntool.BuildBlock,
		Flags: []cli.Flag{
			t8ntool.OutputBasedir,
			t8ntool.OutputBlockFlag,
			t8ntool.InputHeaderFlag,
			t8ntool.InputOmmersFlag,
			t8ntool.InputWithdrawalsFlag,
			t8ntool.InputTxsRlpFlag,
			t8ntool.SealCliqueFlag,
		},
	}
)

// traceFlags contains flags that configure tracing output.
var traceFlags = []cli.Flag{
	TraceFlag,
	TraceFormatFlag,
	TraceDisableStackFlag,
	TraceDisableMemoryFlag,
	TraceDisableStorageFlag,
	TraceDisableReturnDataFlag,

	// deprecated
	DebugFlag,
	MachineFlag,
}

var app = flags.NewApp("the evm command line interface")

func init() {
	app.Flags = debug.Flags
	app.Commands = []*cli.Command{
		runCommand,
		blockTestCommand,
		stateTestCommand,
		stateTransitionCommand,
		transactionCommand,
		blockBuilderCommand,
		verkleCommand,
	}
	app.Before = func(ctx *cli.Context) error {
		flags.MigrateGlobalFlags(ctx)
		return debug.Setup(ctx)
	}
	app.After = func(ctx *cli.Context) error {
		debug.Exit()
		return nil
	}
}

func main() {
	if err := app.Run(os.Args); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

// tracerFromFlags parses the cli flags and returns the specified tracer.
func tracerFromFlags(ctx *cli.Context) *tracing.Hooks {
	config := &logger.Config{
		EnableMemory:     !ctx.Bool(TraceDisableMemoryFlag.Name),
		DisableStack:     ctx.Bool(TraceDisableStackFlag.Name),
		DisableStorage:   ctx.Bool(TraceDisableStorageFlag.Name),
		EnableReturnData: !ctx.Bool(TraceDisableReturnDataFlag.Name),
	}
	switch {
	case ctx.Bool(TraceFlag.Name):
		switch format := ctx.String(TraceFormatFlag.Name); format {
		case "struct":
			return logger.NewStreamingStructLogger(config, os.Stderr).Hooks()
		case "json":
			return logger.NewJSONLogger(config, os.Stderr)
		case "md", "markdown":
			return logger.NewMarkdownLogger(config, os.Stderr).Hooks()
		default:
			fmt.Fprintf(os.Stderr, "unknown trace format: %q\n", format)
			os.Exit(1)
			return nil
		}
	// Deprecated ways of configuring tracing.
	case ctx.Bool(MachineFlag.Name):
		return logger.NewJSONLogger(config, os.Stderr)
	case ctx.Bool(DebugFlag.Name):
		return logger.NewStreamingStructLogger(config, os.Stderr).Hooks()
	default:
		return nil
	}
}

// collectFiles walks the given path. If the path is a directory, it will
// return a list of all accumulates all files with json extension.
// Otherwise (if path points to a file), it will return the path.
func collectFiles(path string) []string {
	var out []string
	if info, err := os.Stat(path); err == nil && !info.IsDir() {
		// User explicitly pointed out a file, ignore extension.
		return []string{path}
	}
	err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if !info.IsDir() && filepath.Ext(info.Name()) == ".json" {
			out = append(out, path)
		}
		return nil
	})
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
	}
	return out
}

// dump returns a state dump for the most current trie.
func dump(s *state.StateDB) *state.Dump {
	root := s.IntermediateRoot(false)
	cpy, _ := state.New(root, s.Database())
	dump := cpy.RawDump(nil)
	return &dump
}
