const { exec } = require('child_process');
const path = require('path');

const Web3 = require('web3');
const Tx = require('ethereumjs-tx');
const fs = require('fs-extra');
const _ = require('lodash');
const wallets = require('./seed/wallets.json');
const {
  BLOCKCHAIN_GAS_LIMIT,
  BLOCKCHAIN_MNEMONIC,
  BLOCKCHAIN_NETWORK_ID,
  BLOCKCHAIN_URL,
  GAS_PRICE,
  ACCOUNT_NUMBER,
  INITIAL_ETHER,
  PACKAGE_ROOT_DIR,
} = require('./constants');

const web3 = new Web3(BLOCKCHAIN_URL);

function consoleOutput(data) {
  let output = '\n\n========== INITIAL STATE ==========';

  _.forEach(data, account => {
    let mintedBalances = '';

    const { mintedTokens, tokens: issuedTokens, walletId } = account;
    const { address } = wallets[walletId];
    _.forEach(mintedTokens, minted => {
      mintedBalances = `${mintedBalances}
    - ${minted.symbol}: ${minted.amount}`;
    });

    let issuedTokensOutput = issuedTokens ? '  ** Issued Tokens **' : '';

    _.forEach(issuedTokens, token => {
      issuedTokensOutput = `${issuedTokensOutput}
    - ${token.symbol} (${token.name}, ${token.divisible ? 'divisible' : 'non-divisible'})`;
    });

    output = `${output}

Account ${walletId} (${address}):
  ** Balances **
    - POLY: ${account.polyBalance}${mintedBalances}
`;

    if (issuedTokensOutput) {
      output = `${output}${issuedTokensOutput}`;
    } else {
      output = `${output}\n`;
    }
  });

  return output;
}

async function runCommand(command, { onData = () => {}, isReady = null, ...opts } = {}) {
  return new Promise(resolve => {
    const cmd = exec(command, { ...opts });
    const isAsyncCommand = !!isReady;

    cmd.stdout.setEncoding('utf8');
    cmd.stderr.setEncoding('utf8');

    cmd.stdout.on('data', data => {
      console.log(data);
      onData(data);

      if (isAsyncCommand && isReady(data)) {
        resolve(cmd);
      }
    });
    cmd.stderr.on('data', error => {
      if (error) {
        console.log('\x1b[31m', `Error: ${error}`, '\x1b[0m');
      }
    });

    cmd.on('close', code => {
      console.log(`child process exited with code ${code}`);
      resolve(cmd);
    });

    process.on('exit', () => {
      cmd.kill('SIGINT');
    });
  });
}

async function sendTransaction(userAccount, action, address) {
  const block = await web3.eth.getBlock('latest');
  const networkGasLimit = block.gasLimit;

  let gas = Math.round(
    1.2 *
      (await action.estimateGas({
        from: userAccount.address,
        value: undefined,
      }))
  );
  if (gas > networkGasLimit) gas = networkGasLimit;

  const nonce = await web3.eth.getTransactionCount(userAccount.address);
  const actionABI = action.encodeABI();
  const txParams = {
    from: userAccount.address,
    to: address,
    data: actionABI,
    gasLimit: gas,
    gasPrice: GAS_PRICE,
    nonce,
    value: undefined,
  };

  const transaction = new Tx(txParams);
  transaction.sign(Buffer.from(userAccount.privateKey.replace('0x', ''), 'hex'));
  await web3.eth.sendSignedTransaction(`0x${transaction.serialize().toString('hex')}`);
}

async function startCli(db, opts = {}) {
  let { initialEther } = opts;

  if (!initialEther) {
    initialEther = INITIAL_ETHER;
  }

  const tempStateDir = `${PACKAGE_ROOT_DIR}/temp-state`.split('/').join(path.sep);

  fs.ensureDirSync(db);
  fs.removeSync(tempStateDir);
  fs.ensureDirSync(tempStateDir);
  fs.copySync(db, tempStateDir);

  const ganacheArgs = [
    // Set a directory to which ganache will write the state to
    `--db="${tempStateDir}"`,
    // Set the block gas limit
    `--gasLimit=${BLOCKCHAIN_GAS_LIMIT}`,
    // Set a network id for ganache
    `-i=${BLOCKCHAIN_NETWORK_ID}`,
    // Set an initial amount of ether for all wallets
    `-e=${initialEther}`,
    `--mnemonic="${BLOCKCHAIN_MNEMONIC}"`,
    `--accounts=${ACCOUNT_NUMBER}`,
    // Sets deterministic mode to ensure same accounts everytime
    '-d',
  ].join(' ');

  const command = `node_modules/.bin/ganache-cli ${ganacheArgs}`.split('/').join(path.sep);

  console.log(`Starting ganache-cli with this command: ${command}`);

  return runCommand(command, {
    cwd: PACKAGE_ROOT_DIR,
    isReady: data => data.includes('Listening on'),
  });
}

const packVersion = (major, minor, patch) => {
  const packedVersion = (parseInt(major) << 16) | (parseInt(minor) << 8) | parseInt(patch);
  return packedVersion;
};

module.exports = {
  consoleOutput,
  runCommand,
  sendTransaction,
  web3,
  startCli,
  packVersion
};
