import async from 'async'
import { execution } from '@remix-project/remix-lib'
import Web3 from 'web3'
import { compilationInterface } from './types'

/**
 * @dev Deploy all contracts from compilation result
 * @param compileResult compilation result
 * @param web3 web3 object
 * @param withDoubleGas If true, try deployment with gas double of estimation (used for Out-of-gas error only)
 * @param callback Callback
 */

export function deployAll (compileResult: compilationInterface, web3: Web3, withDoubleGas: boolean, callback) {
  const compiledObject = {}
  const contracts = {}
  let accounts: string[] = []

  async.waterfall([
    function getAccountList (next) {
      web3.eth.getAccounts((_err, _accounts) => {
        accounts = _accounts
        next()
      })
    },
    function getContractData (next) {
      for (const contractFile in compileResult) {
        for (const contractName in compileResult[contractFile]) {
          const contract = compileResult[contractFile][contractName]

          const className = contractName
          const filename = contractFile

          const abi = contract.abi
          const code = contract.evm.bytecode.object

          compiledObject[className] = {}
          compiledObject[className].abi = abi
          compiledObject[className].code = code
          compiledObject[className].filename = filename
          compiledObject[className].className = className
          compiledObject[className].raw = contract

          if (contractFile.endsWith('_test.sol')) {
            compiledObject[className].isTest = true
          }
        }
      }
      next()
    },
    function determineContractsToDeploy (next) {
      const contractsToDeploy: string[] = ['Assert']
      const allContracts = Object.keys(compiledObject)

      for (const contractName of allContracts) {
        if (contractName === 'Assert') {
          continue
        }
        if (compiledObject[contractName].isTest) {
          contractsToDeploy.push(contractName)
        }
      }
      next(null, contractsToDeploy)
    },
    function deployContracts (contractsToDeploy: string[], next) {
      const deployRunner = (deployObject, contractObject, contractName, filename, callback) => {
        deployObject.estimateGas().then((gasValue) => {
          const gasBase = Math.ceil(gasValue * 1.2)
          const gas = withDoubleGas ? gasBase * 2 : gasBase
          deployObject.send({
            from: accounts[0],
            gas: gas
          }).on('receipt', function (receipt) {
            contractObject.options.address = receipt.contractAddress
            contractObject.options.from = accounts[0]
            contractObject.options.gas = 5000 * 1000
            compiledObject[contractName].deployedAddress = receipt.contractAddress

            contracts[contractName] = contractObject
            contracts[contractName].filename = filename

            callback(null, { result: { createdAddress: receipt.contractAddress } }) // TODO this will only work with JavaScriptV VM
          }).on('error', function (err) {
            console.error(err)
            callback(err)
          })
        })
      }

      async.eachOfLimit(contractsToDeploy, 1, function (contractName, index, nextEach) {
        const contract = compiledObject[contractName]
        const encodeDataFinalCallback = (error, contractDeployData) => {
          if (error) return nextEach(error)
          const contractObject = new web3.eth.Contract(contract.abi)
          const deployObject = contractObject.deploy({ arguments: [], data: '0x' + contractDeployData.dataHex })
          deployRunner(deployObject, contractObject, contractName, contract.filename, (error) => { nextEach(error) })
        }

        const encodeDataStepCallback = (msg) => { console.dir(msg) }

        const encodeDataDeployLibraryCallback = (libData, callback) => {
          const abi = compiledObject[libData.data.contractName].abi
          const code = compiledObject[libData.data.contractName].code
          const libraryObject = new web3.eth.Contract(abi)
          const deployObject = libraryObject.deploy({ arguments: [], data: '0x' + code })
          deployRunner(deployObject, libraryObject, libData.data.contractName, contract.filename, callback)
        }

        const funAbi = null // no need to set the abi for encoding the constructor
        const params = '' // we suppose that the test contract does not have any param in the constructor
        execution.txFormat.encodeConstructorCallAndDeployLibraries(contractName, contract.raw, compileResult, params, funAbi, encodeDataFinalCallback, encodeDataStepCallback, encodeDataDeployLibraryCallback)
      }, function (err) {
        if (err) next(err)
        next(null, contracts)
      })
    }
  ], callback)
}
