# Nimbus
# Copyright (c) 2018-2024 Status Research & Development GmbH
# Licensed under either of
#  * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
#    http://www.apache.org/licenses/LICENSE-2.0)
#  * MIT license ([LICENSE-MIT](LICENSE-MIT) or
#    http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except
# according to those terms.

const
  # debugging flag, dump macro info when asked for
  noisy {.intdefine.}: int = 0
  # isNoisy {.used.} = noisy > 0
  isChatty {.used.} = noisy > 1

import
  ../computation,
  ../evm_errors,
  ../../common/evmforks,
  ./gas_costs,
  ./gas_meter,
  ./op_codes,
  ./op_handlers,
  ./op_handlers/oph_defs,
  macros

export EVMFork, Op, oph_defs, gas_meter

# ------------------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------------------

template handleStopDirective(cpt: VmCpt, tracingEnabled: bool) =
  #trace "op: Stop"
  when tracingEnabled:
    if not cpt.code.atEnd():
      # we only trace `REAL STOP` and ignore `FAKE STOP`
      cpt.opIndex = cpt.traceOpCodeStarted(Stop)
      cpt.traceOpCodeEnded(Stop, cpt.opIndex)

template handleFixedGasCostsDirective(
    fork: EVMFork, op: Op, cost: GasInt, cpt: VmCpt, tracingEnabled: bool
) =
  when tracingEnabled:
    cpt.opIndex = cpt.traceOpCodeStarted(op)

  ?cpt.opcodeGasCost(op, cost, tracingEnabled, reason = $op)
  ?vmOpHandlers[fork][op].run(cpt)

  # If continuation is not nil, traceOpCodeEnded will be called in executeOpcodes.
  when tracingEnabled:
    if cpt.continuation.isNil:
      cpt.traceOpCodeEnded(op, cpt.opIndex)

template handleOtherDirective(fork: EVMFork, op: Op, cpt: VmCpt, tracingEnabled: bool) =
  when tracingEnabled:
    cpt.opIndex = cpt.traceOpCodeStarted(op)

  ?vmOpHandlers[fork][op].run(cpt)

  # If continuation is not nil, traceOpCodeEnded will be called in executeOpcodes.
  when tracingEnabled:
    if cpt.continuation.isNil:
      cpt.traceOpCodeEnded(op, cpt.opIndex)

proc makeCaseDispatcher(forkArg: EVMFork, tracingEnabled: bool, opArg, cpt: NimNode): NimNode =
  # Create a case statement for dispatching opcode to handler for the given
  # fork, taking care to record the gas cost
  # TODO there are several forks for which neither opcodes nor gas costs
  #      changed - these could use the same dispatcher thus saving some space
  #      and compile time
  let gasCosts = forkToSchedule(forkArg)

  result = nnkCaseStmt.newTree(opArg)
  for op in Op:
    let
      asOp = quote: `op`
      handler =
        if op == Stop:
          quote:
            handleStopDirective(`cpt`, `tracingEnabled`)
        elif gasCosts[op].kind == GckFixed:
          let cost = gasCosts[op].cost
          quote:
            handleFixedGasCostsDirective(
              `forkArg`, `op`, `cost`, `cpt`, `tracingEnabled`
            )
        else:
          quote:
            handleOtherDirective(`forkArg`, `op`, `cpt`, `tracingEnabled`)
      branch =
        case op
        of Create, Create2, Call, CallCode, DelegateCall, StaticCall:
          # These opcodes use `chainTo` to create a continuation call which must
          # be handled separately
          quote:
            `handler`
            if not `cpt`.continuation.isNil:
              break

        of Stop, Return, Revert, SelfDestruct:
          quote:
            `handler`
            break
        else:
          handler

    result.add nnkOfBranch.newTree(asOp, branch)

  when isChatty:
    echo ">>> ", result.repr

# ------------------------------------------------------------------------------
# Public macros/functions
# ------------------------------------------------------------------------------

macro dispatchInstr*(
    fork: static EVMFork, tracingEnabled: static bool, op: Op, cpt: VmCpt
): untyped =
  makeCaseDispatcher(fork, tracingEnabled, op, cpt)

# ------------------------------------------------------------------------------
# Debugging ...
# ------------------------------------------------------------------------------

when isMainModule and isChatty:
  import ../types

  proc optimised(cpt: VmCpt): EvmResultVoid {.compileTime.} =
    while true:
      dispatchInstr(FkFrontier, false, cpt.instr, cpt)

# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------