'use strict'
var yo = require('yo-yo')
var async = require('async')
var remixLib = require('remix-lib')
var EventManager = require('../lib/events')

var CompilerImport = require('../app/compiler/compiler-imports')
var toolTip = require('../app/ui/tooltip')
var globalRegistry = require('../global/registry')
var SourceHighlighter = require('../app/editor/sourceHighlighter')
var RemixDebug = require('remix-debug').EthDebugger
var TreeView = require('../app/ui/TreeView') // TODO setup a direct reference to the UI components
var solidityTypeFormatter = require('../app/tabs/debugger/debuggerUI/vmDebugger/utils/SolidityTypeFormatter')
var GistHandler = require('./gist-handler')

class CmdInterpreterAPI {
  constructor (terminal, localRegistry, blockchain) {
    const self = this
    self.event = new EventManager()
    self.blockchain = blockchain
    self._components = {}
    self._components.registry = localRegistry || globalRegistry
    self._components.terminal = terminal
    self._components.sourceHighlighter = new SourceHighlighter()
    self._components.fileImport = new CompilerImport()
    self._components.gistHandler = new GistHandler()
    self._deps = {
      fileManager: self._components.registry.get('filemanager').api,
      editor: self._components.registry.get('editor').api,
      compilersArtefacts: self._components.registry.get('compilersartefacts').api,
      offsetToLineColumnConverter: self._components.registry.get('offsettolinecolumnconverter').api
    }
    self.commandHelp = {
      'remix.call(message: {name, key, payload})': 'Call a registered plugins',
      'remix.getFile(path)': 'Returns the content of the file located at the given path',
      'remix.setFile(path, content)': 'set the content of the file located at the given path',
      'remix.debug(hash)': 'Start debugging a transaction.',
      'remix.loadgist(id)': 'Load a gist in the file explorer.',
      'remix.loadurl(url)': 'Load the given url in the file explorer. The url can be of type github, swarm, ipfs or raw http',
      'remix.setproviderurl(url)': 'Change the current provider to Web3 provider and set the url endpoint.',
      'remix.execute(filepath)': 'Run the script specified by file path. If filepath is empty, script currently displayed in the editor is executed.',
      'remix.exeCurrent()': 'Run the script currently displayed in the editor',
      'remix.help()': 'Display this help message',
      'remix.debugHelp()': 'Display help message for debugging'
    }
  }
  call (message) {
    return this._components.terminal.externalApi.request(message)
  }
  log () { arguments[0] != null ? this._components.terminal.commands.html(arguments[0]) : this._components.terminal.commands.html(arguments[1]) }
  highlight (rawLocation) {
    var self = this
    if (!rawLocation) {
      self._components.sourceHighlighter.currentSourceLocation(null)
      return
    }
    var lineColumnPos = self._deps.offsetToLineColumnConverter.offsetToLineColumn(rawLocation, rawLocation.file,
      self._deps.compilersArtefacts['__last'].getSourceCode().sources,
      self._deps.compilersArtefacts['__last'].getAsts())
    self._components.sourceHighlighter.currentSourceLocation(lineColumnPos, rawLocation)
  }
  debug (hash, cb) {
    var self = this
    delete self.d
    self.blockchain.web3().eth.getTransaction(hash, (error, tx) => {
      if (error) return cb(error)
      var debugSession = new RemixDebug({
        compilationResult: () => {
          return self._deps.compilersArtefacts['__last'].getData()
        }
      })
      debugSession.addProvider('web3', self.blockchain.web3())
      debugSession.switchProvider('web3')
      debugSession.debug(tx)
      self.d = debugSession
      this._components.terminal.commands.log('A new debugging session is available at remix.d')
      if (cb) cb(null, debugSession)
      // helpers
      self.d.highlight = (address, vmtraceIndex) => {
        if (!address) return self.highlight()
        self.d.sourceLocationFromVMTraceIndex(address, vmtraceIndex, (error, rawLocation) => {
          if (!error && rawLocation) {
            self.highlight(rawLocation)
          }
        })
      }
      self.d.stateAt = (vmTraceIndex) => {
        self.d.extractStateAt(vmTraceIndex, (error, state) => {
          if (error) return self.log(error)
          self.d.decodeStateAt(vmTraceIndex, state, (error, state) => {
            if (error) return this._components.terminal.commands.html(error)
            var treeView = new TreeView({
              json: true,
              formatSelf: solidityTypeFormatter.formatSelf,
              extractData: solidityTypeFormatter.extractData
            })
            self.log('State at ' + vmTraceIndex)
            self._components.terminal.commands.html(treeView.render(state, true))
          })
        })
      }
      self.d.localsAt = (contractAddress, vmTraceIndex) => {
        debugSession.sourceLocationFromVMTraceIndex(contractAddress, vmTraceIndex, (error, location) => {
          if (error) return self.log(error)
          debugSession.decodeLocalsAt(23, location, (error, locals) => {
            if (error) return this._components.terminal.commands.html(error)
            var treeView = new TreeView({
              json: true,
              formatSelf: solidityTypeFormatter.formatSelf,
              extractData: solidityTypeFormatter.extractData
            })
            self.log('Locals at ' + vmTraceIndex)
            self._components.terminal.commands.html(treeView.render(locals, true))
          })
        })
      }
      self.d.goTo = (row) => {
        if (self._deps.editor.current()) {
          var breakPoint = new remixLib.code.BreakpointManager(self.d, (sourceLocation) => {
            return self._deps.offsetToLineColumnConverter.offsetToLineColumn(sourceLocation, sourceLocation.file,
              self._deps.compilersArtefacts['__last'].getSourceCode().sources,
              self._deps.compilersArtefacts['__last'].getAsts())
          })
          breakPoint.event.register('breakpointHit', (sourceLocation, currentStep) => {
            self.log(null, 'step index ' + currentStep)
            self.highlight(sourceLocation)
            self.d.stateAt(currentStep)
            self.d.traceManager.getCurrentCalledAddressAt(currentStep, (error, address) => {
              if (error) return self.log(address)
              self.d.localsAt(address, currentStep)
            })
          })
          breakPoint.event.register('NoBreakpointHit', () => { self.log('line ' + row + ' is not part of the current execution') })
          breakPoint.add({fileName: self._deps.editor.current(), row: row - 1})
          breakPoint.jumpNextBreakpoint(0, true)
        }
      }
    })
  }
  loadgist (id, cb) {
    const self = this
    self._components.gistHandler.loadFromGist({gist: id}, this._deps.fileManager)
    if (cb) cb()
  }
  loadurl (url, cb) {
    const self = this
    self._components.fileImport.import(url,
      (loadingMsg) => { toolTip(loadingMsg) },
      (err, content, cleanUrl, type, url) => {
        if (err) {
          toolTip(`Unable to load ${url}: ${err}`)
          if (cb) cb(err)
        } else {
          self._deps.fileManager.setFile(type + '/' + cleanUrl, content)
          try {
            content = JSON.parse(content)
            async.eachOfSeries(content.sources, (value, file, callbackSource) => {
              var url = value.urls[0] // @TODO retrieve all other contents ?
              self._components.fileImport.import(url,
                (loadingMsg) => { toolTip(loadingMsg) },
                async (error, content, cleanUrl, type, url) => {
                  if (error) {
                    toolTip(`Cannot retrieve the content of ${url}: ${error}`)
                    return callbackSource(`Cannot retrieve the content of ${url}: ${error}`)
                  } else {
                    try {
                      await self._deps.fileManager.setFile(type + '/' + cleanUrl, content)
                      callbackSource()
                    } catch (e) {
                      callbackSource(e.message)
                    }
                  }
                })
            }, (error) => {
              if (cb) cb(error)
            })
          } catch (e) {}
          if (cb) cb()
        }
      })
  }
  setproviderurl (url, cb) {
    this.blockchain.setProviderFromEndpoint(url, 'web3', (error) => {
      if (error) toolTip(error)
      if (cb) cb()
    })
  }
  exeCurrent (cb) {
    return this.execute(undefined, cb)
  }
  getFile (path, cb) {
    var provider = this._deps.fileManager.fileProviderOf(path)
    if (provider) {
      provider.get(path, cb)
    } else {
      cb('file not found')
    }
  }
  setFile (path, content, cb) {
    cb = cb || function () {}
    var provider = this._deps.fileManager.fileProviderOf(path)
    if (provider) {
      provider.set(path, content, (error) => {
        if (error) return cb(error)
        this._deps.fileManager.syncEditor(path)
        cb()
      })
    } else {
      cb('file not found')
    }
  }
  execute (file, cb) {
    const self = this

    function _execute (content, cb) {
      if (!content) {
        toolTip('no content to execute')
        if (cb) cb()
        return
      }

      self._components.terminal.commands.script(content)
    }

    if (typeof file === 'undefined') {
      var content = self._deps.editor.currentContent()
      _execute(content, cb)
      return
    }

    var provider = self._deps.fileManager.fileProviderOf(file)

    if (!provider) {
      toolTip(`provider for path ${file} not found`)
      if (cb) cb()
      return
    }

    provider.get(file, (error, content) => {
      if (error) {
        toolTip(error)
        if (cb) cb()
        return
      }

      _execute(content, cb)
    })
  }
  help (cb) {
    const self = this
    var help = yo`<div></div>`
    for (var k in self.commandHelp) {
      help.appendChild(yo`<div>${k}: ${self.commandHelp[k]}</div>`)
      help.appendChild(yo`<br>`)
    }
    self._components.terminal.commands.html(help)
    if (cb) cb()
    return ''
  }
  debugHelp (cb) {
    const self = this
    var help = yo`<div>Here are some examples of scripts that can be run (using remix.exeCurrent() or directly from the console)</div>`
    help.appendChild(yo`<br>`)
    help.appendChild(yo`<br>`)
    help.appendChild(yo`<div>remix.debug('0x3c247ac268afb9a9c183feb9d4e83df51efbc8a2f4624c740789b788dac43029', function (error, debugSession) {
     remix.log = function () { arguments[0] != null ? console.log(arguments[0]) : console.log(arguments[1]) }

     remix.d.traceManager.getLength(remix.log)
     remix.storageView = remix.d.storageViewAt(97, '0x692a70d2e424a56d2c6c27aa97d1a86395877b3a')
     console.log('storage at 97 :')
     remix.storageView.storageRange(remix.log)
})<br/></div>`)
    help.appendChild(yo`<div>remix.log = function () { arguments[0] != null ? console.log(arguments[0]) : console.log(arguments[1]) }
    remix.d.extractStateAt(2, function (error, state) {
        remix.d.decodeStateAt(97, state, remix.log)
    })<br/></div>`)
    help.appendChild(yo`<br>`)
    help.appendChild(yo`<div>remix.highlight(contractAddress, vmTraceIndex)</div>`)
    help.appendChild(yo`<br>`)
    help.appendChild(yo`<div>remix.goTo(row) (this log the index in the vm trace, state and local variables)</div>`)
    help.appendChild(yo`<br>`)
    help.appendChild(yo`<div>remix.stateAt(vmTraceIndex)</div>`)
    help.appendChild(yo`<br>`)
    help.appendChild(yo`<div>remix.localsAt(vmTraceIndex)</div>`)
    help.appendChild(yo`<br>`)
    help.appendChild(yo`<div>Please see <a href="https://www.npmjs.com/package/remix-debug" target="_blank">https://www.npmjs.com/package/remix-debug</a> for more informations</div>`)
    self._components.terminal.commands.html(help)
    if (cb) cb()
    return ''
  }
}

module.exports = CmdInterpreterAPI
