/* global ethereum */
'use strict'
import Web3 from 'web3'
import { rlp, keccak, bufferToHex } from 'ethereumjs-util'
import { vm as remixLibVm, execution } from '@remix-project/remix-lib'
import VM from '@ethereumjs/vm'
import Common from '@ethereumjs/common'
import StateManager from '@ethereumjs/vm/dist/state/stateManager'
import { StorageDump } from '@ethereumjs/vm/dist/state/interface'

/*
  extend vm state manager and instanciate VM
*/

class StateManagerCommonStorageDump extends StateManager {
  keyHashes: { [key: string]: string }
  constructor () {
    super()
    this.keyHashes = {}
  }

  putContractStorage (address, key, value) {
    this.keyHashes[keccak(key).toString('hex')] = bufferToHex(key)
    return super.putContractStorage(address, key, value)
  }

  async dumpStorage (address) {
    let trie
    try {
      trie = await this._getStorageTrie(address)
    } catch (e) {
      console.log(e)
      throw e
    }
    return new Promise<StorageDump>((resolve, reject) => {
      try {
        const storage = {}
        const stream = trie.createReadStream()
        stream.on('data', (val) => {
          const value = rlp.decode(val.value)
          storage['0x' + val.key.toString('hex')] = {
            key: this.keyHashes[val.key.toString('hex')],
            value: '0x' + value.toString('hex')
          }
        })
        stream.on('end', function () {
          resolve(storage)
        })
      } catch (e) {
        reject(e)
      }
    })
  }

  async getStateRoot (force = false) {
    await this._cache.flush()

    const stateRoot = this._trie.root
    return stateRoot
  }

  async setStateRoot (stateRoot) {
    await this._cache.flush()

    if (stateRoot === this._trie.EMPTY_TRIE_ROOT) {
      this._trie.root = stateRoot
      this._cache.clear()
      this._storageTries = {}
      return
    }

    const hasRoot = await this._trie.checkRoot(stateRoot)
    if (!hasRoot) {
      throw new Error('State trie does not contain state root')
    }

    this._trie.root = stateRoot
    this._cache.clear()
    this._storageTries = {}
  }
}

/*
  trigger contextChanged, web3EndpointChanged
*/
export class VMContext {
  currentFork: string
  blockGasLimitDefault: number
  blockGasLimit: number
  customNetWorks
  blocks
  latestBlockNumber
  txs
  currentVm
  web3vm
  logsManager
  exeResults

  constructor (fork?) {
    this.blockGasLimitDefault = 4300000
    this.blockGasLimit = this.blockGasLimitDefault
    this.currentFork = fork || 'berlin'
    this.currentVm = this.createVm(this.currentFork)
    this.blocks = {}
    this.latestBlockNumber = 0
    this.txs = {}
    this.exeResults = {}
    this.logsManager = new execution.LogsManager()
  }

  createVm (hardfork) {
    const stateManager = new StateManagerCommonStorageDump()
    const common = new Common({ chain: 'mainnet', hardfork })
    const vm = new VM({
      common,
      activatePrecompiles: true,
      stateManager,
      allowUnlimitedContractSize: true
    })

    const web3vm = new remixLibVm.Web3VMProvider()
    web3vm.setVM(vm)
    return { vm, web3vm, stateManager, common }
  }

  getCurrentFork () {
    return this.currentFork
  }

  web3 () {
    return this.currentVm.web3vm
  }

  blankWeb3 () {
    return new Web3()
  }

  vm () {
    return this.currentVm.vm
  }

  vmObject () {
    return this.currentVm
  }

  addBlock (block) {
    let blockNumber = '0x' + block.header.number.toString('hex')
    if (blockNumber === '0x') {
      blockNumber = '0x0'
    }

    this.blocks['0x' + block.hash().toString('hex')] = block
    this.blocks[blockNumber] = block
    this.latestBlockNumber = blockNumber

    this.logsManager.checkBlock(blockNumber, block, this.web3())
  }

  trackTx (tx, block) {
    this.txs[tx] = block
  }

  trackExecResult (tx, execReult) {
    this.exeResults[tx] = execReult
  }
}
