'use strict'
import { ethers } from 'ethers'
import { getFunctionFragment } from './txHelper'

/**
  * deploy the given contract
  *
  * @param {String} from    - sender address
  * @param {String} data    - data to send with the transaction ( return of txFormat.buildData(...) ).
  * @param {String} value    - decimal representation of value.
  * @param {String} gasLimit    - decimal representation of gas limit.
  * @param {Object} txRunner    - TxRunner.js instance
  * @param {Object} callbacks    - { confirmationCb, gasEstimationForceSend, promptCb }
  *     [validate transaction] confirmationCb (network, tx, gasEstimation, continueTxExecution, cancelCb)
  *     [transaction failed, force send] gasEstimationForceSend (error, continueTxExecution, cancelCb)
  *     [personal mode enabled, need password to continue] promptCb (okCb, cancelCb)
  * @param {Function} finalCallback    - last callback.
  */
export function createContract (from, data, value, gasLimit, txRunner, callbacks, finalCallback) {
  if (!callbacks.confirmationCb || !callbacks.gasEstimationForceSend || !callbacks.promptCb) {
    return finalCallback('all the callbacks must have been defined')
  }
  const tx = { from: from, to: null, data: data, useCall: false, value: value, gasLimit: gasLimit }
  txRunner.rawRun(tx, callbacks.confirmationCb, callbacks.gasEstimationForceSend, callbacks.promptCb, (error, txResult) => {
    // see universaldapp.js line 660 => 700 to check possible values of txResult (error case)
    finalCallback(error, txResult)
  })
}

/**
  * call the current given contract ! that will create a transaction !
  *
  * @param {String} from    - sender address
  * @param {String} to    - recipient address
  * @param {String} data    - data to send with the transaction ( return of txFormat.buildData(...) ).
  * @param {String} value    - decimal representation of value.
  * @param {String} gasLimit    - decimal representation of gas limit.
  * @param {Object} txRunner    - TxRunner.js instance
  * @param {Object} callbacks    - { confirmationCb, gasEstimationForceSend, promptCb }
  *     [validate transaction] confirmationCb (network, tx, gasEstimation, continueTxExecution, cancelCb)
  *     [transaction failed, force send] gasEstimationForceSend (error, continueTxExecution, cancelCb)
  *     [personal mode enabled, need password to continue] promptCb (okCb, cancelCb)
  * @param {Function} finalCallback    - last callback.
  */
export function callFunction (from, to, data, value, gasLimit, funAbi, txRunner, callbacks, finalCallback) {
  const useCall = funAbi.stateMutability === 'view' || funAbi.stateMutability === 'pure' || funAbi.constant
  const tx = { from, to, data, useCall, value, gasLimit }
  txRunner.rawRun(tx, callbacks.confirmationCb, callbacks.gasEstimationForceSend, callbacks.promptCb, (error, txResult) => {
    // see universaldapp.js line 660 => 700 to check possible values of txResult (error case)
    finalCallback(error, txResult)
  })
}

/**
  * check if the vm has errored
  *
  * @param {Object} execResult    - execution result given by the VM
  * @return {Object} -  { error: true/false, message: DOMNode }
  */
export function checkVMError (execResult, abi, contract) {
  const errorCode = {
    OUT_OF_GAS: 'out of gas',
    STACK_UNDERFLOW: 'stack underflow',
    STACK_OVERFLOW: 'stack overflow',
    INVALID_JUMP: 'invalid JUMP',
    INVALID_OPCODE: 'invalid opcode',
    REVERT: 'revert',
    STATIC_STATE_CHANGE: 'static state change',
    INTERNAL_ERROR: 'internal error',
    CREATE_COLLISION: 'create collision',
    STOP: 'stop',
    REFUND_EXHAUSTED: 'refund exhausted'
  }
  const ret = {
    error: false,
    message: ''
  }
  if (!execResult.exceptionError) {
    return ret
  }
  const exceptionError = execResult.exceptionError.error || ''
  const error = `VM error: ${exceptionError}.\n`
  let msg
  if (exceptionError === errorCode.INVALID_OPCODE) {
    msg = '\t\n\tThe execution might have thrown.\n'
    ret.error = true
  } else if (exceptionError === errorCode.OUT_OF_GAS) {
    msg = '\tThe transaction ran out of gas. Please increase the Gas Limit.\n'
    ret.error = true
  } else if (exceptionError === errorCode.REVERT) {
    const returnData = execResult.returnValue
    const returnDataHex = returnData.slice(0, 4).toString('hex')
    let customError
    if (abi) {
      let decodedCustomErrorInputsClean
      for (const item of abi) {
        if (item.type === 'error') {
          // ethers doesn't crash anymore if "error" type is specified, but it doesn't extract the errors. see:
          // https://github.com/ethers-io/ethers.js/commit/bd05aed070ac9e1421a3e2bff2ceea150bedf9b7
          // we need here to fake the type, so the "getSighash" function works properly
          const fn = getFunctionFragment({ ...item, type: 'function', stateMutability: 'nonpayable' })
          if (!fn) continue
          const sign = fn.getSighash(item.name)
          if (!sign) continue
          if (returnDataHex === sign.replace('0x', '')) {
            customError = item.name
            const functionDesc = fn.getFunction(item.name)
            // decoding error parameters
            const decodedCustomErrorInputs = fn.decodeFunctionData(functionDesc, returnData)
            decodedCustomErrorInputsClean = {}
            let devdoc = {}
            // "contract" reprensents the compilation result containing the NATSPEC documentation
            if (contract && fn.functions && Object.keys(fn.functions).length) {
              const functionSignature = Object.keys(fn.functions)[0]
              // we check in the 'devdoc' if there's a developer documentation for this error
              try {
                devdoc = (contract.object.devdoc.errors && contract.object.devdoc.errors[functionSignature][0]) || {}
              } catch (e) {
                console.error(e.message)
              }
              // we check in the 'userdoc' if there's an user documentation for this error
              try {
                const userdoc = (contract.object.userdoc.errors && contract.object.userdoc.errors[functionSignature][0]) || {}
                if (userdoc && (userdoc as any).notice) customError += ' : ' + (userdoc as any).notice // we append the user doc if any
              } catch (e) {
                console.error(e.message)
              }
            }
            let inputIndex = 0
            for (const input of functionDesc.inputs) {
              const inputKey = input.name || inputIndex
              const v = decodedCustomErrorInputs[inputKey]

              decodedCustomErrorInputsClean[inputKey] = {
                value: v.toString ? v.toString() : v
              }
              if (devdoc && (devdoc as any).params) {
                decodedCustomErrorInputsClean[input.name].documentation = (devdoc as any).params[inputKey] // we add the developer documentation for this input parameter if any
              }
              inputIndex++
            }
            break
          }
        }
      }
      if (decodedCustomErrorInputsClean) {
        msg = '\tThe transaction has been reverted to the initial state.\nError provided by the contract:'
        msg += `\n${customError}`
        msg += '\nParameters:'
        msg += `\n${JSON.stringify(decodedCustomErrorInputsClean, null, ' ')}`
      }
    }
    if (!customError) {
      // It is the hash of Error(string)
      if (returnData && (returnDataHex === '08c379a0')) {
        const abiCoder = new ethers.utils.AbiCoder()
        const reason = abiCoder.decode(['string'], returnData.slice(4))[0]
        msg = `\tThe transaction has been reverted to the initial state.\nReason provided by the contract: "${reason}".`
      } else {
        msg = '\tThe transaction has been reverted to the initial state.\nNote: The called function should be payable if you send value and the value you send should be less than your current balance.'
      }
    }
    ret.error = true
  } else if (exceptionError === errorCode.STATIC_STATE_CHANGE) {
    msg = '\tState changes is not allowed in Static Call context\n'
    ret.error = true
  }
  ret.message = `${error}\n${exceptionError}\n${msg}\nDebug the transaction to get more information.`
  return ret
}
