import { debug as createDebugLogger } from 'debug'
import {
  Account,
  Address,
  BN,
  generateAddress,
  generateAddress2,
  KECCAK256_NULL,
  MAX_INTEGER,
} from 'ethereumjs-util'
import { Block } from '@ethereumjs/block'
import { ERROR, VmError } from '../exceptions'
import { StateManager } from '../state/index'
import { getPrecompile, PrecompileFunc } from './precompiles'
import TxContext from './txContext'
import Message from './message'
import EEI from './eei'
// eslint-disable-next-line
import { short } from './opcodes/util'
import { Log } from './types'
import {
  default as Interpreter,
  InterpreterOpts,
  RunState,
  SimpleInterpreterStep,
} from './interpreter'

const debug = createDebugLogger('vm:evm')
const debugGas = createDebugLogger('vm:evm:gas')

/**
 * Result of executing a message via the {@link EVM}.
 */
export interface EVMResult {
  /**
   * Amount of gas used by the transaction
   */
  gasUsed: BN
  /**
   * Address of created account durint transaction, if any
   */
  createdAddress?: Address
  /**
   * Contains the results from running the code, if any, as described in {@link runCode}
   */
  execResult: ExecResult
  /**
   * Array of evm steps to process the tx bytecode
   */
  evmSteps?: SimpleInterpreterStep[]
}

/**
 * Result of executing a call via the {@link EVM}.
 */
export interface ExecResult {
  runState?: RunState
  /**
   * Description of the exception, if any occured
   */
  exceptionError?: VmError
  /**
   * Amount of gas left
   */
  gas?: BN
  /**
   * Amount of gas the code used to run
   */
  gasUsed: BN
  /**
   * Return value from the contract
   */
  returnValue: Buffer
  /**
   * Array of logs that the contract emitted
   */
  logs?: Log[]
  /**
   * A map from the accounts that have self-destructed to the addresses to send their funds to
   */
  selfdestruct?: { [k: string]: Buffer }
  /**
   * Total amount of gas to be refunded from all nested calls.
   */
  gasRefund?: BN
  /**
   * Array of evm steps to process the tx bytecode
   */
  evmSteps?: SimpleInterpreterStep[]
}

export interface NewContractEvent {
  address: Address
  // The deployment code
  code: Buffer
}

export function OOGResult(gasLimit: BN): ExecResult {
  return {
    returnValue: Buffer.alloc(0),
    gasUsed: gasLimit,
    exceptionError: new VmError(ERROR.OUT_OF_GAS),
  }
}
// CodeDeposit OOG Result
export function COOGResult(gasUsedCreateCode: BN): ExecResult {
  return {
    returnValue: Buffer.alloc(0),
    gasUsed: gasUsedCreateCode,
    exceptionError: new VmError(ERROR.CODESTORE_OUT_OF_GAS),
  }
}

export function INVALID_BYTECODE_RESULT(gasLimit: BN): ExecResult {
  return {
    returnValue: Buffer.alloc(0),
    gasUsed: gasLimit,
    exceptionError: new VmError(ERROR.INVALID_BYTECODE_RESULT),
  }
}

export function VmErrorResult(error: VmError, gasUsed: BN): ExecResult {
  return {
    returnValue: Buffer.alloc(0),
    gasUsed: gasUsed,
    exceptionError: error,
  }
}

/**
 * EVM is responsible for executing an EVM message fully
 * (including any nested calls and creates), processing the results
 * and storing them to state (or discarding changes in case of exceptions).
 * @ignore
 */
export default class EVM {
  _vm: any
  _state: StateManager
  _tx: TxContext
  _block: Block
  /**
   * Amount of gas to refund from deleting storage values
   */
  _refund: BN

  constructor(vm: any, txContext: TxContext, block: Block) {
    this._vm = vm
    this._state = this._vm.stateManager
    this._tx = txContext
    this._block = block
    this._refund = new BN(0)
  }

  /**
   * Executes an EVM message, determining whether it's a call or create
   * based on the `to` address. It checkpoints the state and reverts changes
   * if an exception happens during the message execution.
   */
  async executeMessage(message: Message): Promise<EVMResult> {
    await this._vm._emit('beforeMessage', message)

    if (!message.to && this._vm._common.isActivatedEIP(2929)) {
      message.code = message.data
      ;(<any>this._state).addWarmedAddress((await this._generateAddress(message)).buf)
    }

    const oldRefund = this._refund.clone()

    await this._state.checkpoint()
    if (this._vm.DEBUG) {
      debug('-'.repeat(100))
      debug(`message checkpoint`)
    }

    let result
    if (this._vm.DEBUG) {
      debug(
        `New message caller=${message.caller} gasLimit=${message.gasLimit} to=${
          message.to ? message.to.toString() : ''
        } value=${message.value} delegatecall=${message.delegatecall ? 'yes' : 'no'}`
      )
    }
    if (message.to) {
      if (this._vm.DEBUG) {
        debug(`Message CALL execution (to: ${message.to})`)
      }
      result = await this._executeCall(message)
    } else {
      if (this._vm.DEBUG) {
        debug(`Message CREATE execution (to undefined)`)
      }
      result = await this._executeCreate(message)
    }
    if (this._vm.DEBUG) {
      debug(
        `Received message results gasUsed=${result.gasUsed} execResult: [ gasUsed=${
          result.gasUsed
        } exceptionError=${
          result.execResult.exceptionError ? result.execResult.exceptionError.toString() : ''
        } returnValue=${short(result.execResult.returnValue)} gasRefund=${
          result.execResult.gasRefund
        } ]`
      )
    }
    const err = result.execResult.exceptionError

    // This clause captures any error which happened during execution
    // If that is the case, then set the _refund tracker to the old refund value
    if (err) {
      // TODO: Move `gasRefund` to a tx-level result object
      // instead of `ExecResult`.
      this._refund = oldRefund
      result.execResult.selfdestruct = {}
    }
    result.execResult.gasRefund = this._refund.clone()

    if (err) {
      if (this._vm._common.gteHardfork('homestead') || err.error != ERROR.CODESTORE_OUT_OF_GAS) {
        result.execResult.logs = []
        await this._state.revert()
        if (this._vm.DEBUG) {
          debug(`message checkpoint reverted`)
        }
      } else {
        // we are in chainstart and the error was the code deposit error
        // we do like nothing happened.
        await this._state.commit()
        if (this._vm.DEBUG) {
          debug(`message checkpoint committed`)
        }
      }
    } else {
      await this._state.commit()
      if (this._vm.DEBUG) {
        debug(`message checkpoint committed`)
      }
    }

    await this._vm._emit('afterMessage', result)

    return result
  }

  async _executeCall(message: Message): Promise<EVMResult> {
    const account = await this._state.getAccount(message.caller)
    // Reduce tx value from sender
    if (!message.delegatecall) {
      await this._reduceSenderBalance(account, message)
    }
    // Load `to` account
    const toAccount = await this._state.getAccount(message.to)
    // Add tx value to the `to` account
    let errorMessage
    if (!message.delegatecall) {
      try {
        await this._addToBalance(toAccount, message)
      } catch (e: any) {
        errorMessage = e
      }
    }

    // Load code
    await this._loadCode(message)
    let exit = false
    if (!message.code || message.code.length === 0) {
      exit = true
      if (this._vm.DEBUG) {
        debug(`Exit early on no code`)
      }
    }
    if (errorMessage) {
      exit = true
      if (this._vm.DEBUG) {
        debug(`Exit early on value tranfer overflowed`)
      }
    }
    if (exit) {
      return {
        gasUsed: new BN(0),
        execResult: {
          gasUsed: new BN(0),
          exceptionError: errorMessage, // Only defined if addToBalance failed
          returnValue: Buffer.alloc(0),
        },
      }
    }

    let result: ExecResult
    if (message.isCompiled) {
      if (this._vm.DEBUG) {
        debug(`Run precompile`)
      }
      const unsupportedPrecompileds = [
        '0x0000000000000000000000000000000000000003', // RIPEMD-160
        '0x0000000000000000000000000000000000000009', // blake
    ];
    if (unsupportedPrecompileds.includes(message.to.toString())) {
        result = {
          returnValue: Buffer.alloc(0),
          gasUsed: new BN(0),
          exceptionError: new VmError(ERROR.REVERT),
        }
      } else {
        result = await this.runPrecompile(
          message.code as PrecompileFunc,
          message.data,
          message.outLength,
          message.gasLimit
        )
      }
    } else {
      if (this._vm.DEBUG) {
        debug(`Start bytecode processing...`)
      }
      result = await this.runInterpreter(message)
    }

    return {
      evmSteps: result.evmSteps,
      gasUsed: result.gasUsed,
      execResult: result,
    }
  }

  async _executeCreate(message: Message): Promise<EVMResult> {
    const account = await this._state.getAccount(message.caller)
    // Reduce tx value from sender
    await this._reduceSenderBalance(account, message)

    message.code = message.data
    message.data = Buffer.alloc(0)
    message.to = await this._generateAddress(message)
    message.isDeploy = true
    if (this._vm.DEBUG) {
      debug(`Generated CREATE contract address ${message.to}`)
    }
    let toAccount = await this._state.getAccount(message.to)

    // Check for collision
    if ((toAccount.nonce && toAccount.nonce.gtn(0)) || !toAccount.codeHash.equals(KECCAK256_NULL)) {
      if (this._vm.DEBUG) {
        debug(`Returning on address collision`)
      }
      return {
        gasUsed: message.gasLimit,
        createdAddress: message.to,
        execResult: {
          returnValue: Buffer.alloc(0),
          exceptionError: new VmError(ERROR.CREATE_COLLISION),
          gasUsed: message.gasLimit,
        },
      }
    }

    await this._state.clearContractStorage(message.to)

    const newContractEvent: NewContractEvent = {
      address: message.to,
      code: message.code,
    }

    await this._vm._emit('newContract', newContractEvent)

    toAccount = await this._state.getAccount(message.to)
    // EIP-161 on account creation and CREATE execution
    if (this._vm._common.gteHardfork('spuriousDragon')) {
      toAccount.nonce.iaddn(1)
    }

    // Add tx value to the `to` account
    let errorMessage
    try {
      await this._addToBalance(toAccount, message)
    } catch (e: any) {
      errorMessage = e
    }

    let exit = false
    if (!message.code || message.code.length === 0) {
      exit = true
      if (this._vm.DEBUG) {
        debug(`Exit early on no code`)
      }
    }
    if (errorMessage) {
      exit = true
      if (this._vm.DEBUG) {
        debug(`Exit early on value tranfer overflowed`)
      }
    }
    if (exit) {
      return {
        gasUsed: new BN(0),
        createdAddress: message.to,
        execResult: {
          gasUsed: new BN(0),
          exceptionError: errorMessage, // only defined if addToBalance failed
          returnValue: Buffer.alloc(0),
        },
      }
    }

    if (this._vm.DEBUG) {
      debug(`Start bytecode processing...`)
    }
    let result = await this.runInterpreter(message)

    // fee for size of the return value
    let totalGas = result.gasUsed
    let returnFee = new BN(0)
    if (!result.exceptionError) {
      returnFee = new BN(result.returnValue.length).imuln(
        this._vm._common.param('gasPrices', 'createData')
      )
      totalGas = totalGas.add(returnFee)
      if (this._vm.DEBUG) {
        debugGas(`Add return value size fee (${returnFee} to gas used (-> ${totalGas}))`)
      }
    }

    // Check for SpuriousDragon EIP-170 code size limit
    let allowedCodeSize = true
    if (
      this._vm._common.gteHardfork('spuriousDragon') &&
      result.returnValue.length > this._vm._common.param('vm', 'maxCodeSize')
    ) {
      allowedCodeSize = false
    }

    // If enough gas and allowed code size
    let CodestoreOOG = false
    if (
      totalGas.lte(message.gasLimit) &&
      (this._vm._allowUnlimitedContractSize || allowedCodeSize)
    ) {
      if (
        this._vm._common.isActivatedEIP(3541) &&
        result.returnValue.slice(0, 1).equals(Buffer.from('EF', 'hex'))
      ) {
        result = { ...result, ...INVALID_BYTECODE_RESULT(message.gasLimit) }
      } else {
        result.gasUsed = totalGas
      }
    } else {
      if (this._vm._common.gteHardfork('homestead')) {
        if (this._vm.DEBUG) {
          debug(`Not enough gas or code size not allowed (>= Homestead)`)
        }
        result = { ...result, ...OOGResult(message.gasLimit) }
      } else {
        // we are in Frontier
        if (this._vm.DEBUG) {
          debug(`Not enough gas or code size not allowed (Frontier)`)
        }
        if (totalGas.sub(returnFee).lte(message.gasLimit)) {
          // we cannot pay the code deposit fee (but the deposit code actually did run)
          result = { ...result, ...COOGResult(totalGas.sub(returnFee)) }
          CodestoreOOG = true
        } else {
          result = { ...result, ...OOGResult(message.gasLimit) }
        }
      }
    }

    // Save code if a new contract was created
    if (!result.exceptionError && result.returnValue && result.returnValue.toString() !== '') {
      await this._state.putContractCode(message.to, result.returnValue)
      if (this._vm.DEBUG) {
        debug(`Code saved on new contract creation`)
      }
    } else if (CodestoreOOG) {
      // This only happens at Frontier. But, let's do a sanity check;
      if (!this._vm._common.gteHardfork('homestead')) {
        // Pre-Homestead behavior; put an empty contract.
        // This contract would be considered "DEAD" in later hard forks.
        // It is thus an unecessary default item, which we have to save to dik
        // It does change the state root, but it only wastes storage.
        //await this._state.putContractCode(message.to, result.returnValue)
        const account = await this._state.getAccount(message.to)
        await this._state.putAccount(message.to, account)
      }
    }

    return {
      evmSteps: result.evmSteps,
      gasUsed: result.gasUsed,
      createdAddress: message.to,
      execResult: result,
    }
  }

  /**
   * Starts the actual bytecode processing for a CALL or CREATE, providing
   * it with the {@link EEI}.
   */
  async runInterpreter(message: Message, opts: InterpreterOpts = {}): Promise<ExecResult> {
    const env = {
      blockchain: this._vm.blockchain, // Only used in BLOCKHASH
      address: message.to || Address.zero(),
      caller: message.caller || Address.zero(),
      callData: message.data || Buffer.from([0]),
      callValue: message.value || new BN(0),
      code: message.code as Buffer,
      isStatic: message.isStatic || false,
      depth: message.depth || 0,
      gasPrice: this._tx.gasPrice,
      origin: this._tx.origin || message.caller || Address.zero(),
      block: this._block || new Block(),
      contract: await this._state.getAccount(message.to || Address.zero()),
      codeAddress: message.codeAddress,
      isCreate: message.isCreate,
      isDeploy: message.isDeploy,
    }
    const eei = new EEI(env, this._state, this, this._vm._common, message.gasLimit.clone())
    if (message.selfdestruct) {
      eei._result.selfdestruct = message.selfdestruct
    }

    const interpreter = new Interpreter(this._vm, eei)
    const interpreterRes = await interpreter.run(message.code as Buffer, opts)

    let result = eei._result
    let gasUsed = message.gasLimit.sub(eei._gasLeft)
    if (interpreterRes.exceptionError) {
      if (interpreterRes.exceptionError.error !== ERROR.REVERT) {
        gasUsed = message.gasLimit
      }

      // Clear the result on error
      result = {
        ...result,
        logs: [],
        selfdestruct: {},
      }
    }

    return {
      ...result,
      runState: {
        ...interpreterRes.runState!,
        ...result,
        ...eei._env,
      },
      evmSteps: interpreterRes.evmSteps,
      exceptionError: interpreterRes.exceptionError,
      gas: eei._gasLeft,
      gasUsed,
      returnValue: result.returnValue ? result.returnValue : Buffer.alloc(0),
    }
  }

  /**
   * Returns code for precompile at the given address, or undefined
   * if no such precompile exists.
   */
  getPrecompile(address: Address): PrecompileFunc {
    return getPrecompile(address, this._vm._common)
  }

  /**
   * Executes a precompiled contract with given data and gas limit.
   */
  runPrecompile(
    code: PrecompileFunc,
    data: Buffer,
    outLength: BN,
    gasLimit: BN
  ): Promise<ExecResult> | ExecResult {
    if (typeof code !== 'function') {
      throw new Error('Invalid precompile')
    }

    const opts = {
      data,
      gasLimit,
      outLength,
      _common: this._vm._common,
      _VM: this._vm,
    }

    return code(opts)
  }

  async _loadCode(message: Message): Promise<void> {
    if (!message.code) {
      const precompile = this.getPrecompile(message.codeAddress)
      if (precompile) {
        message.code = precompile
        message.isCompiled = true
      } else {
        message.code = await this._state.getContractCode(message.codeAddress)
        message.isCompiled = false
      }
    }
  }

  async _generateAddress(message: Message): Promise<Address> {
    let addr
    if (message.salt) {
      addr = generateAddress2(message.caller.buf, message.salt, message.code as Buffer)
    } else {
      const acc = await this._state.getAccount(message.caller)
      const newNonce = acc.nonce.subn(1)
      addr = generateAddress(message.caller.buf, newNonce.toArrayLike(Buffer))
    }
    return new Address(addr)
  }

  async _reduceSenderBalance(account: Account, message: Message): Promise<void> {
    account.balance.isub(message.value)
    const result = this._state.putAccount(message.caller, account)
    if (this._vm.DEBUG) {
      debug(`Reduced sender (${message.caller}) balance (-> ${account.balance})`)
    }
    return result
  }

  async _addToBalance(toAccount: Account, message: Message): Promise<void> {
    const newBalance = toAccount.balance.add(message.value)
    if (newBalance.gt(MAX_INTEGER)) {
      throw new VmError(ERROR.VALUE_OVERFLOW)
    }
    toAccount.balance = newBalance
    // putAccount as the nonce may have changed for contract creation
    const result = this._state.putAccount(message.to, toAccount)
    if (this._vm.DEBUG) {
      debug(`Added toAccount (${message.to}) balance (-> ${toAccount.balance})`)
    }
    return result
  }

  async _touchAccount(address: Address): Promise<void> {
    const account = await this._state.getAccount(address)
    return this._state.putAccount(address, account)
  }
}
