# Nimbus
# Copyright (c) 2019 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.

proc hostReleaseResultImpl(res: var nimbus_result) {.cdecl, gcsafe.} =
  dealloc(res.output_data)

proc hostGetTxContextImpl(ctx: Computation): nimbus_tx_context {.cdecl.} =
  let vmstate = ctx.vmState
  result.tx_gas_price = toEvmc(vmstate.txGasPrice.u256)
  result.tx_origin = vmstate.txOrigin
  result.block_coinbase = vmstate.coinbase
  result.block_number = vmstate.blockNumber.truncate(int64)
  result.block_timestamp = vmstate.timestamp.toUnix()
  result.block_gas_limit = int64(vmstate.gasLimit)

  # EIP-4399
  # Transfer block randomness to difficulty OPCODE
  let difficulty = toEvmc(vmstate.difficulty)
  result.block_difficulty = difficulty

  result.chain_id = toEvmc(vmstate.chaindb.config.chainId.uint.u256)
  result.block_base_fee = toEvmc(vmstate.baseFee)

proc hostGetBlockHashImpl(ctx: Computation, number: int64): Hash256 {.cdecl.} =
  ctx.vmState.getAncestorHash(number.u256)

proc hostAccountExistsImpl(ctx: Computation, address: EthAddress): bool {.cdecl.} =
  let db = ctx.vmState.readOnlyStateDB
  if ctx.fork >= FkSpurious:
    not db.isDeadAccount(address)
  else:
    db.accountExists(address)

proc hostGetStorageImpl(ctx: Computation, address: EthAddress, key: var evmc_bytes32): evmc_bytes32 {.cdecl.} =
  ctx.vmState.stateDB.getStorage(address, UInt256.fromEvmc(key)).toEvmc()

proc sstoreNetGasMetering(ctx: Computation): bool {.inline.} =
  ctx.fork in {FkConstantinople, FkIstanbul, FkBerlin, FkLondon}

proc hostSetStorageImpl(ctx: Computation, address: EthAddress,
                        key, value: var evmc_bytes32): evmc_storage_status {.cdecl.} =
  let
    slot = UInt256.fromEvmc(key)
    newValue = UInt256.fromEvmc(value)
    statedb = ctx.vmState.readOnlyStateDB
    currValue = statedb.getStorage(address, slot)

  assert address == ctx.msg.contractAddress

  var
    status = EVMC_STORAGE_MODIFIED
    gasRefund = 0.GasInt
    origValue = 0.u256

  if newValue == currValue:
    status = EVMC_STORAGE_UNCHANGED
  else:
    origValue = statedb.getCommittedStorage(address, slot)
    if origValue == currValue or not ctx.sstoreNetGasMetering():
      if currValue == 0:
        status = EVMC_STORAGE_ADDED
      elif newValue == 0:
        status = EVMC_STORAGE_DELETED
    else:
      status = EVMC_STORAGE_MODIFIED_AGAIN
    ctx.vmState.mutateStateDB:
      db.setStorage(address, slot, newValue)

  let gasParam = GasParams(kind: Op.Sstore,
      s_status: status,
      s_currentValue: currValue,
      s_originalValue: origValue
    )
  gasRefund = ctx.gasCosts[Sstore].c_handler(newValue, gasParam)[1]

  if gasRefund != 0:
    ctx.gasMeter.refundGas(gasRefund)

  result = status

proc hostGetBalanceImpl(ctx: Computation, address: EthAddress): evmc_bytes32 {.cdecl.} =
  ctx.vmState.readOnlyStateDB.getBalance(address).toEvmc()

proc hostGetCodeSizeImpl(ctx: Computation, address: EthAddress): uint {.cdecl.} =
  ctx.vmState.readOnlyStateDB.getCode(address).len.uint

proc hostGetCodeHashImpl(ctx: Computation, address: EthAddress): Hash256 {.cdecl.} =
  let db = ctx.vmstate.readOnlyStateDB
  if not db.accountExists(address):
    return
  if db.isEmptyAccount(address):
    return
  db.getCodeHash(address)

proc hostCopyCodeImpl(ctx: Computation, address: EthAddress,
                      codeOffset: int, bufferData: ptr byte,
                      bufferSize: int): int {.cdecl.} =

  var code = ctx.vmState.readOnlyStateDB.getCode(address)

  # Handle "big offset" edge case.
  if codeOffset > code.len:
    return 0

  let maxToCopy = code.len - codeOffset
  let numToCopy = min(maxToCopy, bufferSize)
  if numToCopy > 0:
    copyMem(bufferData, code[codeOffset].addr, numToCopy)
  result = numToCopy

proc hostSelfdestructImpl(ctx: Computation, address, beneficiary: EthAddress) {.cdecl.} =
  assert address == ctx.msg.contractAddress
  ctx.execSelfDestruct(beneficiary)

proc hostEmitLogImpl(ctx: Computation, address: EthAddress,
                     data: ptr byte, dataSize: int,
                     topics: UncheckedArray[evmc_bytes32], topicsCount: int) {.cdecl.} =
  var log: Log
  if topicsCount > 0:
    log.topics = newSeq[Topic](topicsCount)
    for i in 0 ..< topicsCount:
      log.topics[i] = topics[i].bytes

  log.data = @(makeOpenArray(data, dataSize))
  log.address = address
  ctx.addLogEntry(log)

proc hostAccessAccountImpl(ctx: Computation, address: EthAddress): evmc_access_status {.cdecl.} =
  ctx.vmState.mutateStateDB:
    if not db.inAccessList(address):
      db.accessList(address)
      return EVMC_ACCESS_COLD
    else:
      return EVMC_ACCESS_WARM

proc hostAccessStorageImpl(ctx: Computation, address: EthAddress,
                           key: var evmc_bytes32): evmc_access_status {.cdecl.} =
  let slot = UInt256.fromEvmc(key)
  ctx.vmState.mutateStateDB:
    if not db.inAccessList(address, slot):
      db.accessList(address, slot)
      return EVMC_ACCESS_COLD
    else:
      return EVMC_ACCESS_WARM

proc enterCreateImpl(c: Computation, m: nimbus_message): Computation =
  # TODO: use evmc_message to evoid copy
  let childMsg = Message(
    kind: CallKind(m.kind),
    depth: m.depth,
    gas: m.gas,
    sender: m.sender,
    value: UInt256.fromEvmc(m.value),
    data: @(makeOpenArray(m.inputData, m.inputSize.int))
    )
  return newComputation(c.vmState, childMsg,
                        ContractSalt.fromEvmc(m.create2_salt))

template leaveCreateImpl(c, child: Computation, res: nimbus_result) =
  if not child.shouldBurnGas:
    res.gas_left = child.gasMeter.gasRemaining

  if child.isSuccess:
    c.merge(child)
    res.status_code = EVMC_SUCCESS
    res.create_address = child.msg.contractAddress
  else:
    res.status_code = if child.shouldBurnGas: EVMC_FAILURE else: EVMC_REVERT
    if child.output.len > 0:
      # TODO: can we move the ownership of seq to raw pointer?
      res.output_size = child.output.len.uint
      res.output_data = cast[ptr byte](alloc(child.output.len))
      copyMem(res.output_data, child.output[0].addr, child.output.len)
      res.release = hostReleaseResultImpl

template enterCallImpl(c: Computation, m: nimbus_message): Computation =
  let childMsg = Message(
    kind: CallKind(m.kind),
    depth: m.depth,
    gas: m.gas,
    sender: m.sender,
    codeAddress: m.destination,
    contractAddress: if m.kind == EVMC_CALL: m.destination else: c.msg.contractAddress,
    value: UInt256.fromEvmc(m.value),
    data: @(makeOpenArray(m.inputData, m.inputSize.int)),
    flags: MsgFlags(m.flags)
    )
  newComputation(c.vmState, childMsg)

template leaveCallImpl(c, child: Computation, res: nimbus_result) =
  if not child.shouldBurnGas:
    res.gas_left = child.gasMeter.gasRemaining

  if child.isSuccess:
    c.merge(child)
    res.status_code = EVMC_SUCCESS
  else:
    res.status_code = if child.shouldBurnGas: EVMC_FAILURE else: EVMC_REVERT

  if child.output.len > 0:
    # TODO: can we move the ownership of seq to raw pointer?
    res.output_size = child.output.len.uint
    res.output_data = cast[ptr byte](alloc(child.output.len))
    copyMem(res.output_data, child.output[0].addr, child.output.len)
    res.release = hostReleaseResultImpl

proc enterHostCall(c: Computation, msg: var nimbus_message): Computation {.noinline.} =
  if msg.kind == EVMC_CREATE or msg.kind == EVMC_CREATE2:
    enterCreateImpl(c, msg)
  else:
    enterCallImpl(c, msg)

proc leaveHostCall(c, child: Computation, kind: evmc_call_kind): nimbus_result {.noinline.} =
  if kind == EVMC_CREATE or kind == EVMC_CREATE2:
    leaveCreateImpl(c, child, result)
  else:
    leaveCallImpl(c, child, result)

proc hostCallImpl(ctx: Computation, msg: var nimbus_message): nimbus_result {.cdecl.} =
  let child = enterHostCall(ctx, msg)
  child.execCallOrCreate()
  leaveHostCall(ctx, child, msg.kind)

proc initHostInterface(): evmc_host_interface =
  result.account_exists = cast[evmc_account_exists_fn](hostAccountExistsImpl)
  result.get_storage = cast[evmc_get_storage_fn](hostGetStorageImpl)
  result.set_storage = cast[evmc_set_storage_fn](hostSetStorageImpl)
  result.get_balance = cast[evmc_get_balance_fn](hostGetBalanceImpl)
  result.get_code_size = cast[evmc_get_code_size_fn](hostGetCodeSizeImpl)
  result.get_code_hash = cast[evmc_get_code_hash_fn](hostGetCodeHashImpl)
  result.copy_code = cast[evmc_copy_code_fn](hostCopyCodeImpl)
  result.selfdestruct = cast[evmc_selfdestruct_fn](hostSelfdestructImpl)
  result.call = cast[evmc_call_fn](hostCallImpl)
  result.get_tx_context = cast[evmc_get_tx_context_fn](hostGetTxContextImpl)
  result.get_block_hash = cast[evmc_get_block_hash_fn](hostGetBlockHashImpl)
  result.emit_log = cast[evmc_emit_log_fn](hostEmitLogImpl)
  result.access_account = cast[evmc_access_account_fn](hostAccessAccountImpl)
  result.access_storage = cast[evmc_access_storage_fn](hostAccessStorageImpl)

proc vmSetOptionImpl(vm: ptr evmc_vm, name, value: cstring): evmc_set_option_result {.cdecl.} =
  return EVMC_SET_OPTION_INVALID_NAME

proc vmExecuteImpl(vm: ptr evmc_vm, host: ptr evmc_host_interface,
                   ctx: Computation, rev: evmc_revision,
                   msg: evmc_message, code: ptr byte, code_size: uint): evmc_result {.cdecl.} =
  discard

proc vmGetCapabilitiesImpl(vm: ptr evmc_vm): evmc_capabilities {.cdecl.} =
  result.incl(EVMC_CAPABILITY_EVM1)

proc vmDestroyImpl(vm: ptr evmc_vm) {.cdecl.} =
  dealloc(vm)

const
  EVMC_HOST_NAME = "nimbus_vm"
  EVMC_VM_VERSION = "0.0.1"

proc init(vm: var evmc_vm) =
  vm.abi_version = EVMC_ABI_VERSION
  vm.name = EVMC_HOST_NAME
  vm.version = EVMC_VM_VERSION
  vm.destroy = vmDestroyImpl
  vm.execute = cast[evmc_execute_fn](vmExecuteImpl)
  vm.get_capabilities = vmGetCapabilitiesImpl
  vm.set_option = vmSetOptionImpl

let gHost = initHostInterface()
proc nim_host_get_interface(): ptr nimbus_host_interface {.exportc, cdecl.} =
  result = cast[ptr nimbus_host_interface](gHost.unsafeAddr)

proc nim_host_create_context(vmstate: BaseVmState, msg: ptr evmc_message): Computation {.exportc, cdecl.} =
  #result = HostContext(
  #  vmState: vmstate,
  #  gasPrice: GasInt(gasPrice),
  #  origin: fromEvmc(origin)
  #)
  GC_ref(result)

proc nim_host_destroy_context(ctx: Computation) {.exportc, cdecl.} =
  GC_unref(ctx)

proc nim_create_nimbus_vm(): ptr evmc_vm {.exportc, cdecl.} =
  result = create(evmc_vm)
  init(result[])