'use strict'

const StorageViewer = require('./storage/storageViewer')
const StorageResolver = require('./storage/storageResolver')

const TraceManager = require('./trace/traceManager')
const CodeManager = require('./code/codeManager')
const traceHelper = require('./trace/traceHelper')
const EventManager = require('./eventManager')

const {SolidityProxy, stateDecoder, localDecoder, InternalCallTree} = require('./solidity-decoder')

/**
  * Ethdebugger is a wrapper around a few classes that helps debugging a transaction
  *
  * - Web3Providers - define which environment (web3) the transaction will be retrieved from
  * - TraceManager - Load / Analyze the trace and retrieve details of specific test
  * - CodeManager - Retrieve loaded byte code and help to resolve AST item from vmtrace index
  * - SolidityProxy - Basically used to extract state variable from AST
  * - Breakpoint Manager - Used to add / remove / jumpto breakpoint
  * - InternalCallTree - Used to retrieved local variables
  * - StorageResolver - Help resolving the storage accross different steps
  *
  * @param {Map} opts  -  { function compilationResult } //
  */
function Ethdebugger (opts) {
  this.compilationResult = opts.compilationResult || function (contractAddress) { return null }
  this.web3 = opts.web3
  this.opts = opts

  this.event = new EventManager()

  this.tx

  this.traceManager = new TraceManager({web3: this.web3})
  this.codeManager = new CodeManager(this.traceManager)
  this.solidityProxy = new SolidityProxy({getCurrentCalledAddressAt: this.traceManager.getCurrentCalledAddressAt.bind(this.traceManager), getCode: this.codeManager.getCode.bind(this.codeManager)})
  this.storageResolver = null

  const includeLocalVariables = true
  this.callTree = new InternalCallTree(this.event, 
      this.traceManager,
      this.solidityProxy,
      this.codeManager, 
      { ...opts, includeLocalVariables})
}

Ethdebugger.prototype.setManagers = function () {
  this.traceManager = new TraceManager({web3: this.web3})
  this.codeManager = new CodeManager(this.traceManager)
  this.solidityProxy = new SolidityProxy({getCurrentCalledAddressAt: this.traceManager.getCurrentCalledAddressAt.bind(this.traceManager), getCode: this.codeManager.getCode.bind(this.codeManager)})
  this.storageResolver = null
  const includeLocalVariables = true

  this.callTree = new InternalCallTree(this.event, 
      this.traceManager, 
      this.solidityProxy, 
      this.codeManager, 
      { ...this.opts, includeLocalVariables})
  this.event.trigger('managersChanged')
}

Ethdebugger.prototype.resolveStep = function (index) {
  this.codeManager.resolveStep(index, this.tx)
}

Ethdebugger.prototype.setCompilationResult = function (compilationResult) {
  this.solidityProxy.reset((compilationResult && compilationResult.data) || {})
}

Ethdebugger.prototype.sourceLocationFromVMTraceIndex = async function (address, stepIndex) {
  return this.callTree.sourceLocationTracker.getSourceLocationFromVMTraceIndex(address, stepIndex, this.solidityProxy.contracts)
}

Ethdebugger.prototype.getValidSourceLocationFromVMTraceIndex = async function (address, stepIndex) {
  return this.callTree.sourceLocationTracker.getValidSourceLocationFromVMTraceIndex(address, stepIndex, this.solidityProxy.contracts)
}

Ethdebugger.prototype.sourceLocationFromInstructionIndex = async function (address, instIndex, callback) {
  return this.callTree.sourceLocationTracker.getSourceLocationFromInstructionIndex(address, instIndex, this.solidityProxy.contracts)
}

/* breakpoint */
Ethdebugger.prototype.setBreakpointManager = function (breakpointManager) {
  this.breakpointManager = breakpointManager
}

/* decode locals */
Ethdebugger.prototype.extractLocalsAt = function (step) {
  return this.callTree.findScope(step)
}

Ethdebugger.prototype.decodeLocalsAt = async function (step, sourceLocation, callback) {
  try {
    const stack = this.traceManager.getStackAt(step)
    const memory = this.traceManager.getMemoryAt(step)
    const address = this.traceManager.getCurrentCalledAddressAt(step)
    try {
      const storageViewer = new StorageViewer({ stepIndex: step, tx: this.tx, address: address }, this.storageResolver, this.traceManager)
      const locals = await localDecoder.solidityLocals(step, this.callTree, stack, memory, storageViewer, sourceLocation)
      if (locals.error) {
        return callback(locals.error)
      }
      return callback(null, locals)
    } catch (e) {
      callback(e.message)
    }
  } catch (error) {
    callback(error)
  }
}

/* decode state */
Ethdebugger.prototype.extractStateAt = async function (step) {
  return this.solidityProxy.extractStateVariablesAt(step)
}

Ethdebugger.prototype.decodeStateAt = async function (step, stateVars, callback) {
  try {
    const address = this.traceManager.getCurrentCalledAddressAt(step)
    const storageViewer = new StorageViewer({stepIndex: step, tx: this.tx, address: address}, this.storageResolver, this.traceManager)
    const result = await stateDecoder.decodeState(stateVars, storageViewer)
    return result
  } catch (error) {
    callback(error)
  }
}

Ethdebugger.prototype.storageViewAt = function (step, address) {
  return new StorageViewer({stepIndex: step, tx: this.tx, address: address}, this.storageResolver, this.traceManager)
}

Ethdebugger.prototype.updateWeb3 = function (web3) {
  this.web3 = web3
  this.setManagers()
}

Ethdebugger.prototype.unLoad = function () {
  this.traceManager.init()
  this.codeManager.clear()
  this.event.trigger('traceUnloaded')
}

Ethdebugger.prototype.debug = function (tx) {
  if (this.traceManager.isLoading) {
    return
  }
  tx.to = tx.to || traceHelper.contractCreationToken('0')
  this.tx = tx

  this.traceManager.resolveTrace(tx).then(async (result) => {
    this.setCompilationResult(await this.compilationResult(tx.to))
    this.event.trigger('newTraceLoaded', [this.traceManager.trace])
    if (this.breakpointManager && this.breakpointManager.hasBreakpoint()) {
      this.breakpointManager.jumpNextBreakpoint(false)
    }
    this.storageResolver = new StorageResolver({web3: this.traceManager.web3})
  }).catch((error) => {
    this.statusMessage = error ? error.message : 'Trace not loaded'
  })
}

module.exports = Ethdebugger
