import { debug as createDebugLogger } from 'debug'
import { Account, Address, BN } from 'ethereumjs-util'
import { StateManager } from '../state/index'
import { ERROR, VmError } from '../exceptions'
import Memory from './memory'
import Stack from './stack'
import EEI from './eei'
import { Opcode, handlers as opHandlers, OpHandler, AsyncOpHandler } from './opcodes'
import { dynamicGasHandlers } from './opcodes/gas'
import cloneDeep from 'lodash/cloneDeep'

export interface InterpreterOpts {
  pc?: number
}

export interface RunState {
  programCounter: number
  opCode: number
  memory: Memory
  memoryWordCount: BN
  highestMemCost: BN
  stack: Stack
  returnStack: Stack
  code: Buffer
  shouldDoJumpAnalysis: boolean
  validJumps: Uint8Array // array of values where validJumps[index] has value 0 (default), 1 (jumpdest), 2 (beginsub)
  stateManager: StateManager
  eei: EEI
  messageGasLimit?: BN // Cache value from `gas.ts` to save gas limit for a message call
  vcm?: any
}

export interface InterpreterResult {
  runState?: RunState
  exceptionError?: VmError
  evmSteps?: SimpleInterpreterStep[]
}

export interface InterpreterStep {
  pc: number
  opcode: {
    name: string
    fee: number
    dynamicFee?: BN
    isAsync: boolean
  }
  gasLeft: BN
  gasRefund: BN
  stateManager: StateManager
  stack: BN[]
  returnStack: BN[]
  account: Account
  address: Address
  depth: number
  memory: Buffer
  memoryWordCount: BN
  codeAddress: Address
}

export interface SimpleInterpreterStep {
  pc: number
  opcode: {
    name: string
    fee: number
    dynamicFee?: BN
    isAsync: boolean
  }
  gasLeft: BN
  gasRefund: BN
  stack: BN[]
  returnStack: BN[]
  depth: number
  memory: Buffer
  memoryWordCount: BN
  codeAddress: Address
  callOpcodes?: SimpleInterpreterStep[]
  counters?: any
}

/**
 * Parses and executes EVM bytecode.
 */
export default class Interpreter {
  _vm: any
  _state: StateManager
  _runState: RunState
  _eei: EEI
  _evmStepAux: SimpleInterpreterStep | null = null

  // Opcode debuggers (e.g. { 'push': [debug Object], 'sstore': [debug Object], ...})
  private opDebuggers: { [key: string]: (debug: string) => void } = {}

  constructor(vm: any, eei: EEI) {
    this._vm = vm
    this._state = vm.stateManager
    this._eei = eei
    this._runState = {
      programCounter: 0,
      opCode: 0xfe, // INVALID opcode
      memory: new Memory(),
      memoryWordCount: new BN(0),
      highestMemCost: new BN(0),
      stack: new Stack(),
      returnStack: new Stack(1023), // 1023 return stack height limit per EIP 2315 spec
      code: Buffer.alloc(0),
      validJumps: Uint8Array.from([]),
      stateManager: this._state,
      eei: this._eei,
      shouldDoJumpAnalysis: true,
      vcm: vm.vcm,
    }
    this._evmStepAux = null
  }

  async run(code: Buffer, opts: InterpreterOpts = {}): Promise<InterpreterResult> {
    this._runState.code = code
    this._runState.programCounter = opts.pc ?? this._runState.programCounter
    let evmSteps: any[] = []

    // Check that the programCounter is in range
    const pc = this._runState.programCounter
    if (pc !== 0 && (pc < 0 || pc >= this._runState.code.length)) {
      throw new Error('Internal error: program counter not in range')
    }

    let err
    // Iterate through the given ops until something breaks or we hit STOP
    while (this._runState.programCounter < this._runState.code.length) {
      const opCode = this._runState.code[this._runState.programCounter]
      if (
        this._runState.shouldDoJumpAnalysis &&
        (opCode === 0x56 || opCode === 0x57 || opCode === 0x5e)
      ) {
        // Only run the jump destination analysis if `code` actually contains a JUMP/JUMPI/JUMPSUB opcode
        this._runState.validJumps = this._getValidJumpDests(code)
        this._runState.shouldDoJumpAnalysis = false
      }
      this._runState.opCode = opCode

      try {
        const interpreterStep = await this.runStep()
        //Store a copy of the object
        const callOpcodes = interpreterStep.callOpcodes
        delete interpreterStep.callOpcodes
        evmSteps.push(cloneDeep(interpreterStep))
        //If has extra steps from a call, add them to the array
        if (callOpcodes) {
          evmSteps = evmSteps.concat(callOpcodes)
        }
      } catch (e: any) {
        //Add evmStepAux to steps array
        if (this._evmStepAux) {
          // evmSteps.push(JSON.parse(JSON.stringify(this._evmStepAux)))
          evmSteps.push(cloneDeep(this._evmStepAux))
          this._evmStepAux = null
        }
        // re-throw on non-VM errors
        if (!('errorType' in e && e.errorType === 'VmError')) {
          throw e
        }
        // STOP is not an exception
        if (e.error !== ERROR.STOP) {
          err = e
        }
        break
      }
    }

    return {
      runState: this._runState,
      exceptionError: err,
      evmSteps,
    }
  }

  /**
   * Executes the opcode to which the program counter is pointing,
   * reducing its base gas cost, and increments the program counter.
   */
  async runStep(): Promise<SimpleInterpreterStep> {
    const opInfo = this.lookupOpInfo(this._runState.opCode)

    const gas = new BN(opInfo.fee)
    // clone the gas limit; call opcodes can add stipend,
    // which makes it seem like the gas left increases
    const gasLimitClone = this._eei.getGasLeft()

    if (opInfo.dynamicGas) {
      const dynamicGasHandler = dynamicGasHandlers.get(this._runState.opCode)!
      // This function updates the gas BN in-place using `i*` methods
      // It needs the base fee, for correct gas limit calculation for the CALL opcodes
      await dynamicGasHandler(this._runState, gas, this._vm._common)
    }

    const simpleInterpreterStep = await this._runStepHook(gas, gasLimitClone)

    // Add aux evm step for revert/invalid opcodes
    if (
      opInfo.name === 'STOP' ||
      opInfo.name === 'INVALID' ||
      opInfo.name === 'SELFDESTRUCT' ||
      opInfo.name === 'REVERT'
    ) {
      // Copy step to aux to use it in case of reverted tx
      this._evmStepAux = simpleInterpreterStep
    }

    // Check for invalid opcode
    if (opInfo.name === 'INVALID') {
      throw new VmError(ERROR.INVALID_OPCODE)
    }

    // Reduce opcode's base fee
    this._eei.useGas(gas, `${opInfo.name} fee`)
    // Advance program counter
    this._runState.programCounter++

    // Execute opcode handler
    const opFn = this.getOpHandler(opInfo)
    let fnRes: any
    if (opInfo.isAsync) {
      fnRes = await (opFn as AsyncOpHandler).apply(null, [this._runState, this._vm._common])
    } else {
      fnRes = opFn.apply(null, [this._runState, this._vm._common])
    }
    // If is a CALL, append the call opcodes to the interpreter object
    if (
      ['CALL', 'STATICCALL', 'DELEGATECALL', 'CALLCODE', 'CREATE', 'CREATE2'].includes(
        opInfo.name
      ) &&
      fnRes
    ) {
      simpleInterpreterStep.callOpcodes = fnRes.evmSteps
    }
    // Set counters consumption at interpreter step
    simpleInterpreterStep.counters = this._runState.vcm.currentCountersSnapshot
    return simpleInterpreterStep
  }

  /**
   * Get the handler function for an opcode.
   */
  getOpHandler(opInfo: Opcode): OpHandler {
    return opHandlers.get(opInfo.code)!
  }

  /**
   * Get info for an opcode from VM's list of opcodes.
   */
  lookupOpInfo(op: number): Opcode {
    // if not found, return 0xfe: INVALID
    return this._vm._opcodes.get(op) ?? this._vm._opcodes.get(0xfe)
  }

  async _runStepHook(dynamicFee: BN, gasLeft: BN): Promise<SimpleInterpreterStep> {
    const opcode = this.lookupOpInfo(this._runState.opCode)
    const eventObj: InterpreterStep = {
      pc: this._runState.programCounter,
      gasLeft,
      gasRefund: this._eei._evm._refund,
      opcode: {
        name: opcode.fullName,
        fee: opcode.fee,
        dynamicFee,
        isAsync: opcode.isAsync,
      },
      stack: this._runState.stack._store,
      returnStack: this._runState.returnStack._store,
      depth: this._eei._env.depth,
      address: this._eei._env.address,
      account: this._eei._env.contract,
      stateManager: this._runState.stateManager,
      memory: this._runState.memory._store, // Return underlying array for backwards-compatibility
      memoryWordCount: this._runState.memoryWordCount,
      codeAddress: this._eei._env.codeAddress,
    }

    const simpleInterpreterStep: SimpleInterpreterStep = {
      pc: this._runState.programCounter,
      gasLeft,
      gasRefund: this._eei._evm._refund,
      opcode: {
        name: opcode.fullName,
        fee: opcode.fee,
        dynamicFee,
        isAsync: opcode.isAsync,
      },
      stack: this._runState.stack._store,
      returnStack: this._runState.returnStack._store,
      depth: this._eei._env.depth,
      memory: this._runState.memory._store, // Return underlying array for backwards-compatibility
      memoryWordCount: this._runState.memoryWordCount,
      codeAddress: this._eei._env.codeAddress,
    }

    if (this._vm.DEBUG) {
      // Create opTrace for debug functionality
      let hexStack = []
      hexStack = eventObj.stack.map((item: any) => {
        return '0x' + new BN(item).toString(16, 0)
      })

      const name = eventObj.opcode.name

      const opTrace = {
        pc: eventObj.pc,
        op: name,
        gas: '0x' + eventObj.gasLeft.toString('hex'),
        gasCost: '0x' + eventObj.opcode.fee.toString(16),
        stack: hexStack,
        depth: eventObj.depth,
      }

      if (!(name in this.opDebuggers)) {
        this.opDebuggers[name] = createDebugLogger(`vm:ops:${name}`)
      }
      this.opDebuggers[name](JSON.stringify(opTrace))
    }

    /**
     * The `step` event for trace output
     *
     * @event Event: step
     * @type {Object}
     * @property {Number} pc representing the program counter
     * @property {Object} opcode the next opcode to be ran
     * @property {string}     opcode.name
     * @property {fee}        opcode.number Base fee of the opcode
     * @property {dynamicFee} opcode.dynamicFee Dynamic opcode fee
     * @property {boolean}    opcode.isAsync opcode is async
     * @property {BN} gasLeft amount of gasLeft
     * @property {BN} gasRefund gas refund
     * @property {StateManager} stateManager a {@link StateManager} instance
     * @property {Array} stack an `Array` of `Buffers` containing the stack
     * @property {Array} returnStack the return stack
     * @property {Account} account the Account which owns the code running
     * @property {Address} address the address of the `account`
     * @property {Number} depth the current number of calls deep the contract is
     * @property {Buffer} memory the memory of the VM as a `buffer`
     * @property {BN} memoryWordCount current size of memory in words
     * @property {Address} codeAddress the address of the code which is currently being ran (this differs from `address` in a `DELEGATECALL` and `CALLCODE` call)
     */
    if (this._vm.listenerCount('step') > 0 || this._vm.DEBUG) {
      // Only run this stepHook function if there is an event listener (e.g. test runner)
      // or if the vm is running in debug mode (to display opcode debug logs)
      this._vm._emit('step', eventObj)
    }
    return simpleInterpreterStep
  }

  // Returns all valid jump and jumpsub destinations.
  _getValidJumpDests(code: Buffer) {
    const jumps = new Uint8Array(code.length).fill(0)

    for (let i = 0; i < code.length; i++) {
      const opcode = code[i]
      // skip over PUSH0-32 since no jump destinations in the middle of a push block
      if (opcode <= 0x7f) {
        if (opcode >= 0x60) {
          i += opcode - 0x5f
        } else if (opcode === 0x5b) {
          // Define a JUMPDEST as a 1 in the valid jumps array
          jumps[i] = 1
        } else if (opcode === 0x5c) {
          // Define a BEGINSUB as a 2 in the valid jumps array
          jumps[i] = 2
        }
      }
    }
    return jumps
  }
}
