package vm

import (
	"bytes"
	"crypto/elliptic"
	"encoding/base64"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"math/big"
	"os"
	"slices"
	"strconv"
	"strings"
	"text/tabwriter"

	"github.com/chzyer/readline"
	"github.com/kballard/go-shellquote"
	"git.marketally.com/tutus-one/tutus-chain/cli/cmdargs"
	"git.marketally.com/tutus-one/tutus-chain/cli/flags"
	"git.marketally.com/tutus-one/tutus-chain/cli/options"
	"git.marketally.com/tutus-one/tutus-chain/cli/paramcontext"
	"git.marketally.com/tutus-one/tutus-chain/pkg/compiler"
	"git.marketally.com/tutus-one/tutus-chain/pkg/config"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/interop"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/interop/runtime"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/state"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/storage"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/storage/dbconfig"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/transaction"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/keys"
	"git.marketally.com/tutus-one/tutus-chain/pkg/encoding/address"
	"git.marketally.com/tutus-one/tutus-chain/pkg/encoding/bigint"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/callflag"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/manifest"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/nef"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/trigger"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
	"github.com/urfave/cli/v2"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

const (
	chainKey            = "chain"
	chainCfgKey         = "chainCfg"
	icKey               = "ic"
	contractStateKey    = "contractState"
	exitFuncKey         = "exitFunc"
	readlineInstanceKey = "readlineKey"
	printLogoKey        = "printLogoKey"
)

// Various flag names.
const (
	verboseFlagFullName   = "verbose"
	historicFlagFullName  = "historic"
	gasFlagFullName       = "gas"
	backwardsFlagFullName = "backwards"
	diffFlagFullName      = "diff"
	hashFlagFullName      = "hash"
)

var (
	historicFlag = &cli.IntFlag{
		Name: historicFlagFullName,
		Usage: "Height for historic script invocation (for MPT-enabled blockchain configuration with KeepOnlyLatestState setting disabled). " +
			"Assuming that block N-th is specified as an argument, the historic invocation is based on the storage state of height N and fake currently-accepting block with index N+1.",
	}
	gasFlag = &cli.Int64Flag{
		Name:  gasFlagFullName,
		Usage: "GAS limit for this execution (integer number, satoshi).",
	}
	hashFlag = &flags.AddressFlag{
		Name:  hashFlagFullName,
		Usage: "Smart-contract hash in LE form or address",
	}
)

var commands = []*cli.Command{
	{
		Name:        "exit",
		Usage:       "Exit the VM prompt",
		UsageText:   "exit",
		Description: "Exit the VM prompt.",
		Action:      handleExit,
	},
	{
		Name:        "ip",
		Usage:       "Show current instruction",
		UsageText:   "ip",
		Description: "Show current instruction.",
		Action:      handleIP,
	},
	{
		Name:      "break",
		Usage:     "Place a breakpoint",
		UsageText: `break <ip>`,
		Description: `<ip> is mandatory parameter.

Example:
> break 12`,
		Action: handleBreak,
	},
	{
		Name:      "delete",
		Usage:     "Remove a breakpoint",
		UsageText: `delete <ip>`,
		Description: `<ip> is mandatory parameter.

Example:
> delete 12`,
		Action: handleRemoveBreak,
	},
	{
		Name:      "ib",
		Usage:     "List breakpoints",
		UsageText: `ib`,
		Description: `List breakpoints. 

Example:
> ib`,
		Action: handleListBreak,
	},
	{
		Name:      "jump",
		Usage:     "Jump to the specified instruction (absolute IP value)",
		UsageText: `jump <ip>`,
		Description: `<ip> is mandatory parameter (absolute IP value).

Example:
> jump 12`,
		Action: handleJump,
	},
	{
		Name:        "estack",
		Usage:       "Show evaluation stack contents",
		UsageText:   "estack",
		Description: "Show evaluation stack contents.",
		Action:      handleXStack,
	},
	{
		Name:        "istack",
		Usage:       "Show invocation stack contents",
		UsageText:   "istack",
		Description: "Show invocation stack contents.",
		Action:      handleXStack,
	},
	{
		Name:        "sslot",
		Usage:       "Show static slot contents",
		UsageText:   "sslot",
		Description: "Show static slot contents.",
		Action:      handleSlots,
	},
	{
		Name:        "lslot",
		Usage:       "Show local slot contents",
		UsageText:   "lslot",
		Description: "Show local slot contents",
		Action:      handleSlots,
	},
	{
		Name:        "aslot",
		Usage:       "Show arguments slot contents",
		UsageText:   "aslot",
		Description: "Show arguments slot contents.",
		Action:      handleSlots,
	},
	{
		Name:      "loadnef",
		Usage:     "Load a NEF (possibly with a contract hash) into the VM optionally using provided scoped signers in the context",
		UsageText: `loadnef [--historic <height>] [--gas <int>] [--hash <hash-or-address>] <file> [<manifest>] [-- <signer-with-scope>, ...]`,
		Flags:     []cli.Flag{historicFlag, gasFlag, hashFlag},
		Description: `<file> parameter is mandatory, <manifest> parameter (if omitted) will
   be guessed from the <file> parameter by replacing '.nef' suffix with '.manifest.json'
   suffix.

` + cmdargs.SignersParsingDoc + `

Example:
> loadnef /path/to/script.nef /path/to/manifest.json`,
		Action: handleLoadNEF,
	},
	{
		Name:      "loadbase64",
		Usage:     "Load a base64-encoded script string into the VM optionally attaching to it provided signers with scopes",
		UsageText: `loadbase64 [--historic <height>] [--gas <int>] <string> [-- <signer-with-scope>, ...]`,
		Flags:     []cli.Flag{historicFlag, gasFlag},
		Description: `<string> is mandatory parameter.

` + cmdargs.SignersParsingDoc + `

Example:
> loadbase64 AwAQpdToAAAADBQV9ehtQR1OrVZVhtHtoUHRfoE+agwUzmFvf3Rhfg/EuAVYOvJgKiON9j8TwAwIdHJhbnNmZXIMFDt9NxHG8Mz5sdypA9G/odiW8SOMQWJ9W1I4`,
		Action: handleLoadBase64,
	},
	{
		Name:      "loadhex",
		Usage:     "Load a hex-encoded script string into the VM optionally attaching to it provided signers with scopes",
		UsageText: `loadhex [--historic <height>] [--gas <int>] <string> [-- <signer-with-scope>, ...]`,
		Flags:     []cli.Flag{historicFlag, gasFlag},
		Description: `<string> is mandatory parameter.

` + cmdargs.SignersParsingDoc + `

Example:
> loadhex 0c0c48656c6c6f20776f726c6421`,
		Action: handleLoadHex,
	},
	{
		Name:      "loadgo",
		Usage:     "Compile and load a Go file with the manifest into the VM optionally attaching to it provided signers with scopes and setting provided hash",
		UsageText: `loadgo [--historic <height>] [--gas <int>] [--hash <hash-or-address>] <file> [-- <signer-with-scope>, ...]`,
		Flags:     []cli.Flag{historicFlag, gasFlag, hashFlag},
		Description: `<file> is mandatory parameter.

` + cmdargs.SignersParsingDoc + `

Example:
> loadgo /path/to/file.go`,
		Action: handleLoadGo,
	},
	{
		Name:      "loadtx",
		Usage:     "Load transaction into the VM from chain or from parameter context file",
		UsageText: `loadtx [--historic <height>] [--gas <int>] <file-or-hash>`,
		Flags:     []cli.Flag{historicFlag, gasFlag},
		Description: `Load transaction into the VM from chain or from parameter context file.
   The transaction script will be loaded into VM; the resulting execution context
   will use the provided transaction as script container including its signers,
   hash and nonce. It'll also use transaction's system fee value as GAS limit if
   --gas option is not used.

<file-or-hash> is mandatory parameter.

Example:
> loadtx /path/to/file`,
		Action: handleLoadTx,
	},
	{
		Name:      "loaddeployed",
		Usage:     "Load deployed contract into the VM from chain optionally attaching to it provided signers with scopes",
		UsageText: `loaddeployed [--historic <height>] [--gas <int>] <hash-or-address-or-id>  [-- <signer-with-scope>, ...]`,
		Flags:     []cli.Flag{historicFlag, gasFlag},
		Description: `Load deployed contract into the VM from chain optionally attaching to it provided signers with scopes.
If '--historic' flag specified, then the historic contract state (historic script and manifest) will be loaded.

<hash-or-address-or-id> is mandatory parameter.

` + cmdargs.SignersParsingDoc + `

Example:
> loaddeployed 0x0000000009070e030d0f0e020d0c06050e030c02`,
		Action: handleLoadDeployed,
	},
	{
		Name:        "reset",
		Usage:       "Unload compiled script from the VM and reset context to proper (possibly, historic) state",
		UsageText:   "reset",
		Flags:       []cli.Flag{historicFlag},
		Description: "Unload compiled script from the VM and reset context to proper (possibly, historic) state.",
		Action:      handleReset,
	},
	{
		Name:      "parse",
		Usage:     "Parse provided argument and convert it into other possible formats",
		UsageText: `parse <arg>`,
		Description: `<arg> is an argument which is tried to be interpreted as an item of different types
and converted to other formats. Strings are escaped and output in quotes.`,
		Action: handleParse,
	},
	{
		Name:      "run",
		Usage:     "Usage Execute the current loaded script",
		UsageText: `run [<method> [<parameter>...]]`,
		Description: `<method> is a contract method, specified in manifest. It can be '_' which will push
        parameters onto the stack and execute from the current offset.
<parameter> is a parameter (can be repeated multiple times) that can be specified
        using the same rules as for 'contract testinvokefunction' command:

` + cmdargs.ParamsParsingDoc + `

Example:
> run put int:5 string:some_string_value`,
		Action: handleRun,
	},
	{
		Name:        "cont",
		Usage:       "Continue execution of the current loaded script",
		UsageText:   "cont",
		Description: "Continue execution of the current loaded script.",
		Action:      handleCont,
	},
	{
		Name:      "step",
		Usage:     "Step (n) instruction in the program",
		UsageText: `step [<n>]`,
		Description: `<n> is optional parameter to specify number of instructions to run.

Example:
> step 10`,
		Action: handleStep,
	},
	{
		Name:      "stepinto",
		Usage:     "Stepinto instruction to take in the debugger",
		UsageText: "stepinto",
		Description: `Stepinto instruction to take in the debugger.

Example:
> stepinto`,
		Action: handleStepInto,
	},
	{
		Name:      "stepout",
		Usage:     "Stepout instruction to take in the debugger",
		UsageText: "stepout",
		Description: `Stepout instruction to take in the debugger.

Example:
> stepout`,
		Action: handleStepOut,
	},
	{
		Name:      "stepover",
		Usage:     "Stepover instruction to take in the debugger",
		UsageText: "stepover",
		Description: `Stepover instruction to take in the debugger.

Example:
> stepover`,
		Action: handleStepOver,
	},
	{
		Name:        "ops",
		Usage:       "Dump opcodes of the current loaded program",
		UsageText:   "ops",
		Description: "Dump opcodes of the current loaded program",
		Action:      handleOps,
	},
	{
		Name:        "events",
		Usage:       "Dump events emitted by the current loaded program",
		UsageText:   "events",
		Description: "Dump events emitted by the current loaded program",
		Action:      handleEvents,
	},
	{
		Name:      "env",
		Usage:     "Dump state of the chain that is used for VM CLI invocations (use -v for verbose node configuration)",
		UsageText: `env [-v]`,
		Flags: []cli.Flag{
			&cli.BoolFlag{
				Name:    verboseFlagFullName,
				Aliases: []string{"v"},
				Usage:   "Print the whole blockchain node configuration.",
			},
		},
		Description: `Dump state of the chain that is used for VM CLI invocations (use -v for verbose node configuration).

Example:
> env -v`,
		Action: handleEnv,
	},
	{
		Name:      "storage",
		Usage:     "Dump storage of the contract with the specified hash, address or ID as is at the current stage of script invocation",
		UsageText: `storage [--backwards] [--diff] <hash-or-address-or-id> [<prefix>]`,
		Flags: []cli.Flag{
			&cli.BoolFlag{
				Name:    backwardsFlagFullName,
				Aliases: []string{"b"},
				Usage:   "Backwards traversal direction",
			},
			&cli.BoolFlag{
				Name:    diffFlagFullName,
				Aliases: []string{"d"},
				Usage:   "Dump only those storage items that were added or changed during the current script invocation. Note that this call won't show removed storage items, use 'changes' command for that.",
			},
		},
		Description: `Dump storage of the contract with the specified hash, address or ID as is at the current stage of script invocation.
Can be used if no script is loaded.
Hex-encoded storage items prefix may be specified (empty by default to return the whole set of storage items).
If seek prefix is not empty, then it's trimmed from the resulting keys.
Items are sorted. Backwards seek direction may be specified (false by default, which means forwards storage seek direction).
It is possible to dump only those storage items that were added or changed during current script invocation (use --diff flag for it).
To dump the whole set of storage changes including removed items use 'changes' command.

Example:
> storage 0x0000000009070e030d0f0e020d0c06050e030c02 030e --backwards --diff`,
		Action: handleStorage,
	},
	{
		Name:      "changes",
		Usage:     "Dump storage changes as is at the current stage of loaded script invocation",
		UsageText: `changes [<hash-or-address-or-id> [<prefix>]]`,
		Description: `Dump storage changes as is at the current stage of loaded script invocation.
If no script is loaded or executed, then no changes are present.
The contract hash, address or ID may be specified as the first parameter to dump the specified contract storage changes.
Hex-encoded search prefix (without contract ID) may be specified to dump matching storage changes.
Resulting values are not sorted.

Example:
> changes 0x0000000009070e030d0f0e020d0c06050e030c02 030e`,
		Action: handleChanges,
	},
}

var completer *readline.PrefixCompleter

func init() {
	var pcItems []readline.PrefixCompleterInterface
	for _, c := range commands {
		if !c.Hidden {
			var flagsItems []readline.PrefixCompleterInterface
			for _, f := range c.Flags {
				names := strings.SplitN(f.Names()[0], ", ", 2) // only long name will be offered
				flagsItems = append(flagsItems, readline.PcItem("--"+names[0]))
			}
			pcItems = append(pcItems, readline.PcItem(c.Name, flagsItems...))
		}
	}
	completer = readline.NewPrefixCompleter(pcItems...)
}

// Various errors.
var (
	ErrMissingParameter = errors.New("missing argument")
	ErrInvalidParameter = errors.New("can't parse argument")
)

// CLI object for interacting with the VM.
type CLI struct {
	chain *core.Blockchain
	shell *cli.App
}

// NewWithConfig returns new CLI instance using provided config and (optionally)
// provided node config for state-backed VM.
func NewWithConfig(printLogotype bool, onExit func(int), c *readline.Config, cfg config.Config) (*CLI, error) {
	if c.AutoComplete == nil {
		// Autocomplete commands/flags on TAB.
		c.AutoComplete = completer
	}
	l, err := readline.NewEx(c)
	if err != nil {
		return nil, fmt.Errorf("failed to create readline instance: %w", err)
	}
	ctl := cli.NewApp()
	ctl.Name = "VM CLI"

	// Note: need to set empty `ctl.HelpName` and `ctl.UsageText`, otherwise
	// `filepath.Base(os.Args[0])` will be used which is `neo-go`.
	ctl.HelpName = ""
	ctl.UsageText = ""

	ctl.Writer = l.Stdout()
	ctl.ErrWriter = l.Stderr()
	ctl.Version = config.Version
	ctl.Usage = "Official VM CLI for Tutus"

	// Override default error handler in order not to exit on error.
	ctl.ExitErrHandler = func(context *cli.Context, err error) {}

	ctl.Commands = commands

	store, err := storage.NewStore(cfg.ApplicationConfiguration.DBConfiguration)
	if err != nil {
		writeErr(ctl.ErrWriter, fmt.Errorf("failed to open DB, clean in-memory storage will be used: %w", err))
		cfg.ApplicationConfiguration.DBConfiguration.Type = dbconfig.InMemoryDB
		store = storage.NewMemoryStore()
	}

	log, _, logCloser, err := options.HandleLoggingParams(nil, cfg.ApplicationConfiguration)
	if err != nil {
		return nil, cli.Exit(fmt.Errorf("failed to init logger: %w", err), 1)
	}
	filter := zap.WrapCore(func(z zapcore.Core) zapcore.Core {
		return options.NewFilteringCore(z, func(entry zapcore.Entry) bool {
			// Log only Runtime.Notify messages.
			return entry.Level == zapcore.InfoLevel && entry.Message == runtime.SystemRuntimeLogMessage
		})
	})
	fLog := log.WithOptions(filter)

	exitF := func(i int) {
		_ = store.Close()
		if logCloser != nil {
			_ = logCloser()
		}
		onExit(i)
	}

	chain, err := core.NewBlockchain(store, cfg.Blockchain(), fLog)
	if err != nil {
		return nil, cli.Exit(fmt.Errorf("could not initialize blockchain: %w", err), 1)
	}
	// Do not run chain, we need only state-related functionality from it.
	ic, err := chain.GetTestVM(trigger.Application, nil, nil)
	if err != nil {
		return nil, cli.Exit(fmt.Errorf("failed to create test VM: %w", err), 1)
	}

	vmcli := CLI{
		chain: chain,
		shell: ctl,
	}

	vmcli.shell.Metadata = map[string]any{
		chainKey:            chain,
		chainCfgKey:         cfg,
		icKey:               ic,
		contractStateKey:    new(state.ContractBase),
		exitFuncKey:         exitF,
		readlineInstanceKey: l,
		printLogoKey:        printLogotype,
	}
	changePrompt(vmcli.shell)
	return &vmcli, nil
}

func getExitFuncFromContext(app *cli.App) func(int) {
	return app.Metadata[exitFuncKey].(func(int))
}

func getReadlineInstanceFromContext(app *cli.App) *readline.Instance {
	return app.Metadata[readlineInstanceKey].(*readline.Instance)
}

func getVMFromContext(app *cli.App) *vm.VM {
	return getInteropContextFromContext(app).VM
}

func getChainFromContext(app *cli.App) *core.Blockchain {
	return app.Metadata[chainKey].(*core.Blockchain)
}

func getChainConfigFromContext(app *cli.App) config.Config {
	return app.Metadata[chainCfgKey].(config.Config)
}

func getInteropContextFromContext(app *cli.App) *interop.Context {
	return app.Metadata[icKey].(*interop.Context)
}

func getContractStateFromContext(app *cli.App) *state.ContractBase {
	return app.Metadata[contractStateKey].(*state.ContractBase)
}

func getPrintLogoFromContext(app *cli.App) bool {
	return app.Metadata[printLogoKey].(bool)
}

func setInteropContextInContext(app *cli.App, ic *interop.Context) {
	app.Metadata[icKey] = ic
}

func setContractStateInContext(app *cli.App, cs *state.ContractBase) {
	app.Metadata[contractStateKey] = cs
}

func checkVMIsReady(app *cli.App) bool {
	v := getVMFromContext(app)
	if v == nil || !v.Ready() {
		writeErr(app.Writer, errors.New("VM is not ready: no program loaded"))
		return false
	}
	return true
}

func handleExit(c *cli.Context) error {
	finalizeInteropContext(c.App)
	l := getReadlineInstanceFromContext(c.App)
	_ = l.Close()
	exit := getExitFuncFromContext(c.App)
	fmt.Fprintln(c.App.Writer, "Bye!")
	exit(0)
	return nil
}

func handleIP(c *cli.Context) error {
	if !checkVMIsReady(c.App) {
		return nil
	}
	v := getVMFromContext(c.App)
	ctx := v.Context()
	if ctx.NextIP() < ctx.LenInstr() {
		ip, opcode := v.Context().NextInstr()
		fmt.Fprintf(c.App.Writer, "instruction pointer at %d (%s)\n", ip, opcode)
	} else {
		fmt.Fprintln(c.App.Writer, "execution has finished")
	}
	return nil
}

func handleBreak(c *cli.Context) error {
	if !checkVMIsReady(c.App) {
		return nil
	}
	n, err := getInstructionParameter(c)
	if err != nil {
		return err
	}

	v := getVMFromContext(c.App)
	v.AddBreakPoint(n)
	fmt.Fprintf(c.App.Writer, "breakpoint added at instruction %d\n", n)
	return nil
}

func handleRemoveBreak(c *cli.Context) error {
	if !checkVMIsReady(c.App) {
		return nil
	}
	n, err := getInstructionParameter(c)
	if err != nil {
		return err
	}

	v := getVMFromContext(c.App)
	v.RemoveBreakPoint(n)
	fmt.Fprintf(c.App.Writer, "breakpoint removed at instruction %d\n", n)
	return nil
}

func handleListBreak(c *cli.Context) error {
	if !checkVMIsReady(c.App) {
		return nil
	}

	v := getVMFromContext(c.App)
	for _, bp := range v.Context().BreakPoints() {
		fmt.Fprintf(c.App.Writer, "%d\n", bp)
	}
	return nil
}

func handleJump(c *cli.Context) error {
	if !checkVMIsReady(c.App) {
		return nil
	}
	n, err := getInstructionParameter(c)
	if err != nil {
		return err
	}

	v := getVMFromContext(c.App)
	v.Context().Jump(n)
	fmt.Fprintf(c.App.Writer, "jumped to instruction %d\n", n)
	return nil
}

func getInstructionParameter(c *cli.Context) (int, error) {
	args := c.Args().Slice()
	if len(args) != 1 {
		return 0, fmt.Errorf("%w: <ip>", ErrMissingParameter)
	}
	n, err := strconv.Atoi(args[0])
	if err != nil {
		return 0, fmt.Errorf("%w: %w", ErrInvalidParameter, err)
	}
	return n, nil
}

func handleXStack(c *cli.Context) error {
	v := getVMFromContext(c.App)
	var stackDump string
	switch c.Command.Name {
	case "estack":
		stackDump = v.DumpEStack()
	case "istack":
		stackDump = v.DumpIStack()
	default:
		return errors.New("unknown stack")
	}
	fmt.Fprintln(c.App.Writer, stackDump)
	return nil
}

func handleSlots(c *cli.Context) error {
	v := getVMFromContext(c.App)
	vmCtx := v.Context()
	if vmCtx == nil {
		return errors.New("no program loaded")
	}
	var rawSlot string
	switch c.Command.Name {
	case "sslot":
		rawSlot = dumpSlot(vmCtx.StaticsSlot())
	case "lslot":
		rawSlot = dumpSlot(vmCtx.LocalsSlot())
	case "aslot":
		rawSlot = dumpSlot(vmCtx.ArgumentsSlot())
	default:
		return errors.New("unknown slot")
	}
	fmt.Fprintln(c.App.Writer, rawSlot)
	return nil
}

func dumpSlot(s *vm.Slot) string {
	if s == nil {
		return "[]"
	}
	b, _ := json.MarshalIndent(s, "", "    ")
	return string(b)
}

// prepareVM retrieves --historic flag from context (if set) and resets app state
// (to the specified historic height if given).
func prepareVM(c *cli.Context, tx *transaction.Transaction) error {
	var err error
	if c.IsSet(historicFlagFullName) {
		height := c.Int(historicFlagFullName)
		err = resetState(c.App, tx, uint32(height))
	} else {
		err = resetState(c.App, tx)
	}
	if err != nil {
		return err
	}
	if c.IsSet(gasFlagFullName) {
		gas := c.Int64(gasFlagFullName)
		v := getVMFromContext(c.App)
		v.SetGasLimit(gas)
	}
	return nil
}

func getHashFlag(c *cli.Context) (util.Uint160, error) {
	if !c.IsSet(hashFlagFullName) {
		return util.Uint160{}, nil
	}
	h := c.Generic(hashFlagFullName).(*flags.Address)
	return h.Uint160(), nil
}

func handleLoadNEF(c *cli.Context) error {
	args := c.Args().Slice()
	if len(args) < 1 {
		return fmt.Errorf("%w: <nef> is required", ErrMissingParameter)
	}
	nefFile := args[0]
	var (
		manifestFile       string
		signersStartOffset int
	)
	if len(args) == 2 {
		manifestFile = args[1]
	} else if len(args) == 3 {
		if args[1] != cmdargs.CosignersSeparator {
			return fmt.Errorf("%w: `%s` was expected as the second parameter, got %s", ErrInvalidParameter, cmdargs.CosignersSeparator, args[1])
		}
		signersStartOffset = 2
	} else if len(args) > 3 {
		if args[1] == cmdargs.CosignersSeparator {
			signersStartOffset = 2
		} else {
			manifestFile = args[1]
			if args[2] != cmdargs.CosignersSeparator {
				return fmt.Errorf("%w: `%s` was expected as the third parameter, got %s", ErrInvalidParameter, cmdargs.CosignersSeparator, args[2])
			}
			signersStartOffset = 3
		}
	}
	if len(manifestFile) == 0 {
		manifestFile = strings.TrimSuffix(nefFile, ".nef") + ".manifest.json"
	}
	b, err := os.ReadFile(nefFile)
	if err != nil {
		return err
	}
	nef, err := nef.FileFromBytes(b)
	if err != nil {
		return fmt.Errorf("failed to decode NEF file: %w", err)
	}
	m, err := getManifestFromFile(manifestFile)
	if err != nil {
		return fmt.Errorf("failed to read manifest: %w", err)
	}
	var signers []transaction.Signer
	if signersStartOffset != 0 && len(args) > signersStartOffset {
		signers, err = cmdargs.ParseSigners(args[signersStartOffset:])
		if err != nil {
			return fmt.Errorf("%w: failed to parse signers: %w", ErrInvalidParameter, err)
		}
	}
	err = prepareVM(c, createFakeTransaction(nef.Script, signers))
	if err != nil {
		return err
	}
	h, err := getHashFlag(c)
	if err != nil {
		return err
	}
	cs := &state.ContractBase{
		Hash:     h,
		NEF:      nef,
		Manifest: *m,
	}
	setContractStateInContext(c.App, cs)

	v := getVMFromContext(c.App)
	fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr())
	changePrompt(c.App)
	return nil
}

func handleLoadBase64(c *cli.Context) error {
	args := c.Args().Slice()
	if len(args) < 1 {
		return fmt.Errorf("%w: <string>", ErrMissingParameter)
	}
	b, err := base64.StdEncoding.DecodeString(args[0])
	if err != nil {
		return fmt.Errorf("%w: %w", ErrInvalidParameter, err)
	}
	var signers []transaction.Signer
	if len(args) > 1 {
		if args[1] != cmdargs.CosignersSeparator {
			return fmt.Errorf("%w: `%s` was expected as the second parameter, got %s", ErrInvalidParameter, cmdargs.CosignersSeparator, args[1])
		}
		if len(args) < 3 {
			return fmt.Errorf("%w: signers expected after `%s`, got none", ErrInvalidParameter, cmdargs.CosignersSeparator)
		}
		signers, err = cmdargs.ParseSigners(args[2:])
		if err != nil {
			return fmt.Errorf("%w: %w", ErrInvalidParameter, err)
		}
	}
	err = prepareVM(c, createFakeTransaction(b, signers))
	if err != nil {
		return err
	}
	v := getVMFromContext(c.App)
	fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr())
	changePrompt(c.App)
	return nil
}

// createFakeTransaction creates fake transaction with prefilled script, VUB and signers.
func createFakeTransaction(script []byte, signers []transaction.Signer) *transaction.Transaction {
	return &transaction.Transaction{
		Script:  script,
		Signers: signers,
	}
}

func handleLoadHex(c *cli.Context) error {
	args := c.Args().Slice()
	if len(args) < 1 {
		return fmt.Errorf("%w: <string>", ErrMissingParameter)
	}
	b, err := hex.DecodeString(args[0])
	if err != nil {
		return fmt.Errorf("%w: %w", ErrInvalidParameter, err)
	}
	var signers []transaction.Signer
	if len(args) > 1 {
		if args[1] != cmdargs.CosignersSeparator {
			return fmt.Errorf("%w: `%s` was expected as the second parameter, got %s", ErrInvalidParameter, cmdargs.CosignersSeparator, args[1])
		}
		if len(args) < 3 {
			return fmt.Errorf("%w: signers expected after `%s`, got none", ErrInvalidParameter, cmdargs.CosignersSeparator)
		}
		signers, err = cmdargs.ParseSigners(args[2:])
		if err != nil {
			return fmt.Errorf("%w: %w", ErrInvalidParameter, err)
		}
	}
	err = prepareVM(c, createFakeTransaction(b, signers))
	if err != nil {
		return err
	}
	v := getVMFromContext(c.App)
	fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr())
	changePrompt(c.App)
	return nil
}

func handleLoadGo(c *cli.Context) error {
	args := c.Args().Slice()
	if len(args) < 1 {
		return fmt.Errorf("%w: <file>", ErrMissingParameter)
	}

	name := strings.TrimSuffix(args[0], ".go")
	ne, di, err := compiler.CompileWithOptions(args[0], nil, &compiler.Options{Name: name})
	if err != nil {
		return fmt.Errorf("failed to compile: %w", err)
	}

	// Don't perform checks, just load.
	m, err := di.ConvertToManifest(&compiler.Options{})
	if err != nil {
		return fmt.Errorf("can't create manifest: %w", err)
	}
	var signers []transaction.Signer
	if len(args) > 1 {
		if args[1] != cmdargs.CosignersSeparator {
			return fmt.Errorf("%w: `%s` was expected as the second parameter, got %s", ErrInvalidParameter, cmdargs.CosignersSeparator, args[1])
		}
		if len(args) < 3 {
			return fmt.Errorf("%w: signers expected after `%s`, got none", ErrInvalidParameter, cmdargs.CosignersSeparator)
		}
		signers, err = cmdargs.ParseSigners(args[2:])
		if err != nil {
			return fmt.Errorf("%w: %w", ErrInvalidParameter, err)
		}
	}

	err = prepareVM(c, createFakeTransaction(ne.Script, signers))
	if err != nil {
		return err
	}
	h, err := getHashFlag(c)
	if err != nil {
		return err
	}
	cs := &state.ContractBase{
		Hash:     h,
		NEF:      *ne,
		Manifest: *m,
	}
	setContractStateInContext(c.App, cs)

	v := getVMFromContext(c.App)
	fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr())
	changePrompt(c.App)
	return nil
}

func handleLoadTx(c *cli.Context) error {
	args := c.Args().Slice()
	if len(args) < 1 {
		return fmt.Errorf("%w: <file-or-hash>", ErrMissingParameter)
	}

	var (
		tx  *transaction.Transaction
		err error
	)
	h, err := util.Uint256DecodeStringLE(strings.TrimPrefix(args[0], "0x"))
	if err != nil {
		pc, err := paramcontext.Read(args[0])
		if err != nil {
			return fmt.Errorf("invalid tx hash or path to parameter context: %w", err)
		}
		var ok bool
		tx, ok = pc.Verifiable.(*transaction.Transaction)
		if !ok {
			return errors.New("failed to retrieve transaction from parameter context: verifiable item is not a transaction")
		}
	} else {
		bc := getChainFromContext(c.App)
		tx, _, err = bc.GetTransaction(h)
		if err != nil {
			return fmt.Errorf("failed to get transaction from chain: %w", err)
		}
	}
	err = prepareVM(c, tx)
	if err != nil {
		return err
	}
	v := getVMFromContext(c.App)
	if v.GasLimit() == -1 {
		v.SetGasLimit(tx.SystemFee)
	}
	fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr())
	changePrompt(c.App)
	return nil
}

func handleLoadDeployed(c *cli.Context) error {
	err := prepareVM(c, nil) // prepare historic IC if needed (for further historic contract state retrieving).
	if err != nil {
		return err
	}
	if !c.Args().Present() {
		return errors.New("contract hash, address or ID is mandatory argument")
	}
	args := c.Args().Slice()
	hashOrID := args[0]
	ic := getInteropContextFromContext(c.App)
	h, err := flags.ParseAddress(hashOrID)
	if err != nil {
		i, err := strconv.ParseInt(hashOrID, 10, 32)
		if err != nil {
			return fmt.Errorf("failed to parse contract hash, address or ID: %w", err)
		}
		h, err = native.GetContractScriptHash(ic.DAO, ic.Chain.NativeManagementID(), int32(i))
		if err != nil {
			return fmt.Errorf("failed to retrieve contract hash by ID: %w", err)
		}
	}
	cs, err := ic.GetContract(h) // will return historic contract state.
	if err != nil {
		return fmt.Errorf("contract %s not found: %w", h.StringLE(), err)
	}

	var signers []transaction.Signer
	if len(args) > 1 {
		if args[1] != cmdargs.CosignersSeparator {
			return fmt.Errorf("%w: %s was expected as the second parameter, got %s", ErrInvalidParameter, cmdargs.CosignersSeparator, args[1])
		}
		if len(args) < 3 {
			return fmt.Errorf("%w: signers expected after `%s`, got none", ErrInvalidParameter, cmdargs.CosignersSeparator)
		}
		signers, err = cmdargs.ParseSigners(args[2:])
		if err != nil {
			return fmt.Errorf("%w: %w", ErrInvalidParameter, err)
		}
	}
	err = prepareVM(c, createFakeTransaction(cs.NEF.Script, signers)) // prepare VM one more time for proper IC initialization.
	if err != nil {
		return err
	}
	ic = getInteropContextFromContext(c.App) // fetch newly-created IC.
	gasLimit := ic.VM.GasLimit()
	ic.ReuseVM(ic.VM) // clear previously loaded program and context.
	ic.VM.SetGasLimit(gasLimit)
	ic.VM.LoadScriptWithHash(cs.NEF.Script, cs.Hash, callflag.All)
	fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", ic.VM.Context().LenInstr())
	setContractStateInContext(c.App, &cs.ContractBase)
	changePrompt(c.App)
	return nil
}

func handleReset(c *cli.Context) error {
	err := prepareVM(c, nil)
	if err != nil {
		return err
	}
	changePrompt(c.App)
	return nil
}

// finalizeInteropContext calls finalizer for the current interop context.
func finalizeInteropContext(app *cli.App) {
	ic := getInteropContextFromContext(app)
	ic.Finalize()
}

// resetInteropContext calls finalizer for current interop context and replaces
// it with the newly created one. If transaction is provided, then its script is
// loaded into bound VM.
func resetInteropContext(app *cli.App, tx *transaction.Transaction, height ...uint32) error {
	finalizeInteropContext(app)
	bc := getChainFromContext(app)
	var (
		newIc *interop.Context
		err   error
	)
	if len(height) != 0 {
		if tx != nil {
			tx.ValidUntilBlock = height[0] + 1
		}
		newIc, err = bc.GetTestHistoricVM(trigger.Application, tx, height[0]+1)
		if err != nil {
			return fmt.Errorf("failed to create historic VM for height %d: %w", height[0], err)
		}
	} else {
		if tx != nil {
			tx.ValidUntilBlock = bc.BlockHeight() + 1
		}
		newIc, err = bc.GetTestVM(trigger.Application, tx, nil)
		if err != nil {
			return fmt.Errorf("failed to create VM: %w", err)
		}
	}
	if tx != nil {
		newIc.VM.LoadWithFlags(tx.Script, callflag.All)
	}

	setInteropContextInContext(app, newIc)
	return nil
}

// resetContractState removes loaded contract state from app context.
func resetContractState(app *cli.App) {
	setContractStateInContext(app, nil)
}

// resetState resets state of the app (clear interop context and manifest) so that it's ready
// to load new program.
func resetState(app *cli.App, tx *transaction.Transaction, height ...uint32) error {
	err := resetInteropContext(app, tx, height...)
	if err != nil {
		return err
	}
	resetContractState(app)
	return nil
}

func getManifestFromFile(name string) (*manifest.Manifest, error) {
	bs, err := os.ReadFile(name)
	if err != nil {
		return nil, fmt.Errorf("%w: can't read manifest", ErrInvalidParameter)
	}

	var m manifest.Manifest
	if err := json.Unmarshal(bs, &m); err != nil {
		return nil, fmt.Errorf("%w: can't unmarshal manifest", ErrInvalidParameter)
	}
	return &m, nil
}

func handleRun(c *cli.Context) error {
	v := getVMFromContext(c.App)
	cs := getContractStateFromContext(c.App)
	args := c.Args().Slice()
	if len(args) != 0 {
		var (
			params     []stackitem.Item
			offset     int
			err        error
			runCurrent = args[0] != "_"
			hasRet     bool
		)

		_, scParams, err := cmdargs.ParseParams(args[1:], true)
		if err != nil {
			return fmt.Errorf("%w: %w", ErrInvalidParameter, err)
		}
		params = make([]stackitem.Item, len(scParams))
		for i := range scParams {
			params[i], err = scParams[i].ToStackItem()
			if err != nil {
				return fmt.Errorf("failed to convert parameter #%d to stackitem: %w", i, err)
			}
		}
		if runCurrent {
			if cs == nil {
				return fmt.Errorf("manifest is not loaded; either use 'run' command to run loaded script from the start or use 'loadgo', 'loadnef' or 'loaddeployed' commands to provide manifest")
			}
			md := cs.Manifest.ABI.GetMethod(args[0], len(params))
			if md == nil {
				return fmt.Errorf("%w: method not found", ErrInvalidParameter)
			}
			hasRet = md.ReturnType != smartcontract.VoidType
			offset = md.Offset
			var initOff = -1
			if initMD := cs.Manifest.ABI.GetMethod(manifest.MethodInit, 0); initMD != nil {
				initOff = initMD.Offset
			}

			// Clear context loaded by 'loadgo', 'loadnef' or 'loaddeployed' to properly handle LoadNEFMethod.
			// At the same time, preserve previously set gas limit and the set of breakpoints.
			ic := getInteropContextFromContext(c.App)
			gasLimit := v.GasLimit()
			breaks := v.Context().BreakPoints() // We ensure that there's a context loaded.
			ic.ReuseVM(v)
			v.SetGasLimit(gasLimit)
			v.LoadNEFMethod(&cs.NEF, &cs.Manifest, util.Uint160{}, cs.Hash, callflag.All, hasRet, offset, initOff, nil, false)
			for _, bp := range breaks {
				v.AddBreakPoint(bp)
			}
		}
		for i := len(params) - 1; i >= 0; i-- {
			v.Estack().PushVal(params[i])
		}
	}
	runVMWithHandling(c)
	changePrompt(c.App)
	return nil
}

// runVMWithHandling runs VM with handling errors and additional state messages.
func runVMWithHandling(c *cli.Context) {
	v := getVMFromContext(c.App)
	err := v.Run()
	if err != nil {
		writeErr(c.App.ErrWriter, err)
	}

	var (
		message string
		dumpNtf bool
	)
	switch {
	case v.HasFailed():
		message = "" // the error will be printed on return
		dumpNtf = true
	case v.HasHalted():
		message = v.DumpEStack()
		dumpNtf = true
	case v.AtBreakpoint():
		ctx := v.Context()
		if ctx.NextIP() < ctx.LenInstr() {
			i, op := ctx.NextInstr()
			message = fmt.Sprintf("at breakpoint %d (%s)", i, op)
		} else {
			message = "execution has finished"
		}
	}
	if dumpNtf {
		var e string
		e, err = dumpEvents(c.App)
		if err == nil && len(e) != 0 {
			if message != "" {
				message += "\n"
			}
			message += "Events:\n" + e
		}
	}
	if message != "" {
		fmt.Fprintln(c.App.Writer, message)
	}
}

func handleCont(c *cli.Context) error {
	if !checkVMIsReady(c.App) {
		return nil
	}
	runVMWithHandling(c)
	changePrompt(c.App)
	return nil
}

func handleStep(c *cli.Context) error {
	var (
		n   = 1
		err error
	)

	if !checkVMIsReady(c.App) {
		return nil
	}
	v := getVMFromContext(c.App)
	args := c.Args().Slice()
	if len(args) > 0 {
		n, err = strconv.Atoi(args[0])
		if err != nil {
			return fmt.Errorf("%w: %w", ErrInvalidParameter, err)
		}
	}
	v.AddBreakPointRel(n)
	runVMWithHandling(c)
	changePrompt(c.App)
	return nil
}

func handleStepInto(c *cli.Context) error {
	return handleStepType(c, "into")
}

func handleStepOut(c *cli.Context) error {
	return handleStepType(c, "out")
}

func handleStepOver(c *cli.Context) error {
	return handleStepType(c, "over")
}

func handleStepType(c *cli.Context, stepType string) error {
	if !checkVMIsReady(c.App) {
		return nil
	}
	v := getVMFromContext(c.App)
	var err error
	switch stepType {
	case "into":
		err = v.StepInto()
	case "out":
		err = v.StepOut()
	case "over":
		err = v.StepOver()
	}
	if err != nil {
		return err
	}
	_ = handleIP(c)
	changePrompt(c.App)
	return nil
}

func handleOps(c *cli.Context) error {
	if !checkVMIsReady(c.App) {
		return nil
	}
	v := getVMFromContext(c.App)
	out := bytes.NewBuffer(nil)
	v.PrintOps(out)
	fmt.Fprintln(c.App.Writer, out.String())
	return nil
}

func changePrompt(app *cli.App) {
	v := getVMFromContext(app)
	l := getReadlineInstanceFromContext(app)
	if v.Ready() && v.Context().NextIP() >= 0 && v.Context().NextIP() < v.Context().LenInstr() {
		l.SetPrompt(fmt.Sprintf("\033[32mNEO-GO-VM %d >\033[0m ", v.Context().NextIP()))
	} else {
		l.SetPrompt("\033[32mNEO-GO-VM >\033[0m ")
	}
}

func handleEvents(c *cli.Context) error {
	e, err := dumpEvents(c.App)
	if err != nil {
		writeErr(c.App.ErrWriter, err)
		return nil
	}
	fmt.Fprintln(c.App.Writer, e)
	return nil
}

func handleEnv(c *cli.Context) error {
	bc := getChainFromContext(c.App)
	cfg := getChainConfigFromContext(c.App)
	ic := getInteropContextFromContext(c.App)
	message := fmt.Sprintf("Chain height: %d\nVM height (may differ from chain height in case of historic call): %d\nNetwork magic: %d\nDB type: %s\n",
		bc.BlockHeight(), ic.BlockHeight(), bc.GetConfig().Magic, cfg.ApplicationConfiguration.DBConfiguration.Type)
	if c.Bool(verboseFlagFullName) {
		cfgBytes, err := json.MarshalIndent(cfg, "", "\t")
		if err != nil {
			return fmt.Errorf("failed to marshal node configuration: %w", err)
		}
		message += "Node config:\n" + string(cfgBytes) + "\n"
	}
	fmt.Fprint(c.App.Writer, message)
	return nil
}

func handleStorage(c *cli.Context) error {
	id, prefix, err := getDumpArgs(c)
	if err != nil {
		return err
	}
	var (
		backwards bool
		seekDepth int
		ic        = getInteropContextFromContext(c.App)
	)
	if c.Bool(backwardsFlagFullName) {
		backwards = true
	}
	if c.Bool(diffFlagFullName) {
		seekDepth = 1 // take only upper DAO layer which stores only added or updated items.
	}
	ic.DAO.Seek(id, storage.SeekRange{
		Prefix:      prefix,
		Backwards:   backwards,
		SearchDepth: seekDepth,
	}, func(k, v []byte) bool {
		fmt.Fprintf(c.App.Writer, "%s: %v\n", hex.EncodeToString(k), hex.EncodeToString(v))
		return true
	})
	return nil
}

func handleChanges(c *cli.Context) error {
	var (
		expectedID int32
		prefix     []byte
		err        error
		hasAgs     = c.Args().Present()
	)
	if hasAgs {
		expectedID, prefix, err = getDumpArgs(c)
		if err != nil {
			return err
		}
	}
	ic := getInteropContextFromContext(c.App)
	b := ic.DAO.GetBatch()
	if b == nil {
		return nil
	}
	ops := storage.BatchToOperations(b)
	var notFirst bool
	for _, op := range ops {
		id := int32(binary.LittleEndian.Uint32(op.Key))
		if hasAgs && (expectedID != id || (len(prefix) != 0 && !bytes.HasPrefix(op.Key[4:], prefix))) {
			continue
		}
		var message string
		if notFirst {
			message += "\n"
		}
		message += fmt.Sprintf("Contract ID: %d\nState: %s\nKey: %s\n", id, op.State, hex.EncodeToString(op.Key[4:]))
		if op.Value != nil {
			message += fmt.Sprintf("Value: %s\n", hex.EncodeToString(op.Value))
		}
		fmt.Fprint(c.App.Writer, message)
		notFirst = true
	}
	return nil
}

// getDumpArgs is a helper function that retrieves contract ID and search prefix (if given).
func getDumpArgs(c *cli.Context) (int32, []byte, error) {
	id, err := getContractID(c)
	if err != nil {
		return 0, nil, err
	}
	var prefix []byte
	if c.NArg() > 1 {
		prefix, err = hex.DecodeString(c.Args().Get(1))
		if err != nil {
			return 0, nil, fmt.Errorf("failed to decode prefix from hex: %w", err)
		}
	}
	return id, prefix, nil
}

// getContractID returns contract ID parsed from the first argument which can be ID,
// hash or address.
func getContractID(c *cli.Context) (int32, error) {
	if !c.Args().Present() {
		return 0, errors.New("contract hash, address or ID is mandatory argument")
	}
	hashOrID := c.Args().Get(0)
	var ic = getInteropContextFromContext(c.App)
	h, err := flags.ParseAddress(hashOrID)
	if err != nil {
		i, err := strconv.ParseInt(hashOrID, 10, 32)
		if err != nil {
			return 0, fmt.Errorf("failed to parse contract hash, address or ID: %w", err)
		}
		return int32(i), nil
	}
	cs, err := ic.GetContract(h)
	if err != nil {
		return 0, fmt.Errorf("contract %s not found: %w", h.StringLE(), err)
	}
	return cs.ID, nil
}

func dumpEvents(app *cli.App) (string, error) {
	ic := getInteropContextFromContext(app)
	if len(ic.Notifications) == 0 {
		return "", nil
	}
	b, err := json.MarshalIndent(ic.Notifications, "", "\t")
	if err != nil {
		return "", fmt.Errorf("failed to marshal notifications: %w", err)
	}
	return string(b), nil
}

// Run waits for user input from Stdin and executes the passed command.
func (c *CLI) Run() error {
	if getPrintLogoFromContext(c.shell) {
		printLogo(c.shell.Writer)
	}
	l := getReadlineInstanceFromContext(c.shell)
	for {
		line, err := l.Readline()
		if errors.Is(err, io.EOF) || errors.Is(err, readline.ErrInterrupt) {
			return nil // OK, stop execution.
		}
		if err != nil {
			return fmt.Errorf("failed to read input: %w", err) // Critical error, stop execution.
		}

		args, err := shellquote.Split(line)
		if err != nil {
			writeErr(c.shell.ErrWriter, fmt.Errorf("failed to parse arguments: %w", err))
			continue // Not a critical error, continue execution.
		}

		err = c.shell.Run(append([]string{"vm"}, args...))
		if err != nil {
			writeErr(c.shell.ErrWriter, err) // Various command/flags parsing errors and execution errors.
		}
	}
}

func handleParse(c *cli.Context) error {
	res, err := Parse(c.Args().Slice())
	if err != nil {
		return err
	}
	fmt.Fprintln(c.App.Writer, res)
	return nil
}

// Parse converts it's argument to other formats.
func Parse(args []string) (string, error) {
	if len(args) < 1 {
		return "", ErrMissingParameter
	}
	arg := args[0]
	var buf []byte
	if val, err := strconv.ParseInt(arg, 10, 64); err == nil {
		bs := bigint.ToBytes(big.NewInt(val))
		buf = fmt.Appendf(buf, "Integer to Hex\t%s\n", hex.EncodeToString(bs))
		buf = fmt.Appendf(buf, "Integer to Base64\t%s\n", base64.StdEncoding.EncodeToString(bs))
	}
	noX := strings.TrimPrefix(arg, "0x")
	if rawStr, err := hex.DecodeString(noX); err == nil {
		if val, err := util.Uint160DecodeBytesBE(rawStr); err == nil {
			buf = fmt.Appendf(buf, "BE ScriptHash to Address\t%s\n", address.Uint160ToString(val))
			buf = fmt.Appendf(buf, "LE ScriptHash to Address\t%s\n", address.Uint160ToString(val.Reverse()))
		}
		if pub, err := keys.NewPublicKeyFromBytes(rawStr, elliptic.P256()); err == nil {
			sh := pub.GetScriptHash()
			buf = fmt.Appendf(buf, "Public key to Base64\t%s\n", base64.StdEncoding.EncodeToString(rawStr))
			buf = fmt.Appendf(buf, "Public key to BE ScriptHash\t%s\n", sh)
			buf = fmt.Appendf(buf, "Public key to LE ScriptHash\t%s\n", sh.Reverse())
			buf = fmt.Appendf(buf, "Public key to Address\t%s\n", address.Uint160ToString(sh))
		}
		buf = fmt.Appendf(buf, "Hex to String\t%s\n", fmt.Sprintf("%q", string(rawStr)))
		buf = fmt.Appendf(buf, "Hex to Integer\t%s\n", bigint.FromBytes(rawStr))
		var clonedStr = slices.Clone(rawStr)
		slices.Reverse(clonedStr)
		buf = fmt.Appendf(buf, "Swap Endianness\t%s\n", hex.EncodeToString(clonedStr))
	}
	if addr, err := address.StringToUint160(arg); err == nil {
		buf = fmt.Appendf(buf, "Address to BE ScriptHash\t%s\n", addr)
		buf = fmt.Appendf(buf, "Address to LE ScriptHash\t%s\n", addr.Reverse())
		buf = fmt.Appendf(buf, "Address to Base64 (BE)\t%s\n", base64.StdEncoding.EncodeToString(addr.BytesBE()))
		buf = fmt.Appendf(buf, "Address to Base64 (LE)\t%s\n", base64.StdEncoding.EncodeToString(addr.BytesLE()))
	}
	if rawStr, err := base64.StdEncoding.DecodeString(arg); err == nil {
		buf = fmt.Appendf(buf, "Base64 to String\t%s\n", fmt.Sprintf("%q", string(rawStr)))
		buf = fmt.Appendf(buf, "Base64 to BigInteger\t%s\n", bigint.FromBytes(rawStr))
		if u, err := util.Uint160DecodeBytesBE(rawStr); err == nil {
			buf = fmt.Appendf(buf, "Base64 to BE ScriptHash\t%s\n", u.StringBE())
			buf = fmt.Appendf(buf, "Base64 to LE ScriptHash\t%s\n", u.StringLE())
			buf = fmt.Appendf(buf, "Base64 to Address (BE)\t%s\n", address.Uint160ToString(u))
			buf = fmt.Appendf(buf, "Base64 to Address (LE)\t%s\n", address.Uint160ToString(u.Reverse()))
		}
		if pub, err := keys.NewPublicKeyFromBytes(rawStr, elliptic.P256()); err == nil {
			sh := pub.GetScriptHash()
			buf = fmt.Appendf(buf, "Public key to Hex\t%s\n", pub.StringCompressed())
			buf = fmt.Appendf(buf, "Public key to BE ScriptHash\t%s\n", sh)
			buf = fmt.Appendf(buf, "Public key to LE ScriptHash\t%s\n", sh.Reverse())
			buf = fmt.Appendf(buf, "Public key to Address\t%s\n", address.Uint160ToString(sh))
		}
	}

	buf = fmt.Appendf(buf, "String to Hex\t%s\n", hex.EncodeToString([]byte(arg)))
	buf = fmt.Appendf(buf, "String to Base64\t%s\n", base64.StdEncoding.EncodeToString([]byte(arg)))

	res := bytes.NewBuffer(nil)
	w := tabwriter.NewWriter(res, 0, 4, 4, '\t', 0)
	if _, err := w.Write(buf); err != nil {
		return "", err
	}
	if err := w.Flush(); err != nil {
		return "", err
	}
	return res.String(), nil
}

const logo = `
    _   ____________        __________      _    ____  ___
   / | / / ____/ __ \      / ____/ __ \    | |  / /  |/  /
  /  |/ / __/ / / / /_____/ / __/ / / /____| | / / /|_/ / 
 / /|  / /___/ /_/ /_____/ /_/ / /_/ /_____/ |/ / /  / /  
/_/ |_/_____/\____/      \____/\____/      |___/_/  /_/   
`

func printLogo(w io.Writer) {
	fmt.Fprint(w, logo)
	fmt.Fprintln(w)
	fmt.Fprintln(w)
	fmt.Fprintln(w)
}

func writeErr(w io.Writer, err error) {
	fmt.Fprintf(w, "Error: %s\n", err)
}
