const _ = require('lodash');
const { sendTransaction, web3, packVersion, consoleOutput } = require('../utils');
const { BLOCKCHAIN_NETWORK_ID } = require('../constants');
const wallets = require('./wallets.json');

/**
 * Generates required blockchain state for local testing using 3.1.0 contracts
 */
async function seedData(dataFile, dual) {  
  const { accounts } = require(dataFile);

  const PolymathRegistryArtifact = require('../../artifacts/3.1.0/PolymathRegistry.json');
  const polymathRegistryAddress = require(`../../artifacts/${dual ? '2.1.0' : '3.1.0'}/PolymathRegistry.json`).networks[BLOCKCHAIN_NETWORK_ID].address;
  const SecurityTokenRegistryArtifact = require('../../artifacts/3.1.0/ISecurityTokenRegistry.json');
  const ModuleRegistryArtifact = require('../../artifacts/3.1.0/ModuleRegistry.json');
  const PolyTokenFaucetArtifact = require('../../artifacts/3.1.0/PolyTokenFaucet.json');
  const SecurityTokenArtifact = require('../../artifacts/3.1.0/ISecurityToken.json');
  const GeneralTransferManagerArtifact = require('../../artifacts/3.1.0/GeneralTransferManager.json');
  const ModuleFactoryArtifact = require('../../artifacts/3.1.0/ModuleFactory.json');

  const PolymathRegistry = new web3.eth.Contract(
    PolymathRegistryArtifact.abi,
    polymathRegistryAddress
  );

  const strAddress = await PolymathRegistry.methods.getAddress('SecurityTokenRegistry').call();

  const SecurityTokenRegistry = new web3.eth.Contract(
    SecurityTokenRegistryArtifact.abi,
    strAddress
  );

  const mrAddress = await PolymathRegistry.methods.getAddress('ModuleRegistry').call();

  const ModuleRegistry = new web3.eth.Contract(ModuleRegistryArtifact.abi, mrAddress);

  const polyTokenAddress = await PolymathRegistry.methods.getAddress('PolyToken').call();

  const PolyTokenFaucet = new web3.eth.Contract(PolyTokenFaucetArtifact.abi, polyTokenAddress);

  const { address: daiAddress } = PolyTokenFaucetArtifact.networks[BLOCKCHAIN_NETWORK_ID];

  const DaiFaucet = new web3.eth.Contract(PolyTokenFaucetArtifact.abi, daiAddress);

  const registrationFee = await SecurityTokenRegistry.methods.getTickerRegistrationFee().call();
  const launchFee = await SecurityTokenRegistry.methods.getSecurityTokenLaunchFee().call();

  const outputData = _.cloneDeep(accounts);

  console.log('Generating seed data...');

  const { toWei, toChecksumAddress, hexToUtf8, toHex } = web3.utils;

  for (let i = 0; i < accounts.length; ++i) {
    const account = accounts[i];
    const wallet = wallets[account.walletId];
    const { address, privateKey } = wallet;
    const { polyBalance, daiBalance } = account;

    const userAccount = await web3.eth.accounts.privateKeyToAccount(privateKey);

    if (polyBalance > 0) {
      console.log(`\nTransferring ${polyBalance} POLY to ${address}\n`);
      const getPoly = PolyTokenFaucet.methods.getTokens(toWei(String(polyBalance)), address);

      await sendTransaction(userAccount, getPoly, polyTokenAddress);
    }

    if (daiBalance > 0) {      
      console.log(`\nTransferring ${daiBalance} DAI to ${address}\n`);
      const getDai = DaiFaucet.methods.getTokens(toWei(String(daiBalance)), address);    

      await sendTransaction(userAccount, getDai, daiAddress);
    }

    const { tokens } = account;

    if (!tokens) {
      continue;
    }

    for (let j = 0; j < tokens.length; ++j) {
      const token = tokens[j];
      const { name, symbol, divisible, protocolVersion = '3.1.0' } = token;

      if (protocolVersion !== '3.1.0' && dual) {
        continue;
      }

      console.log(`\nSeeding token ${symbol}\n`);

      let approveSpend = PolyTokenFaucet.methods.approve(strAddress, registrationFee);

      await sendTransaction(userAccount, approveSpend, polyTokenAddress);

      const registerTicker = SecurityTokenRegistry.methods.registerTicker(address, symbol, name);
      await sendTransaction(userAccount, registerTicker, strAddress);

      approveSpend = PolyTokenFaucet.methods.approve(strAddress, launchFee);
      await sendTransaction(userAccount, approveSpend, polyTokenAddress);

      const packedVersion = packVersion(3, 0, 0);
      const launchToken = SecurityTokenRegistry.methods.generateNewSecurityToken(
        name,
        symbol,
        '',
        divisible,
        userAccount.address,
        packedVersion
      );

      await sendTransaction(userAccount, launchToken, strAddress);

      const tokenAddress = await SecurityTokenRegistry.methods
        .getSecurityTokenAddress(symbol)
        .call();

      const SecurityToken = new web3.eth.Contract(SecurityTokenArtifact.abi, tokenAddress);

      const gtmModules = await SecurityToken.methods
        .getModulesByName(toHex('GeneralTransferManager'))
        .call();
      const gtmAddress = gtmModules[0];

      const GeneralTransferManager = new web3.eth.Contract(
        GeneralTransferManagerArtifact.abi,
        gtmAddress
      );

      const { minted } = token;

      if (!minted) {
        continue;
      }

      const addressesToMint = [];
      const amountsToMint = [];

      for (let k = 0; k < minted.length; ++k) {
        const mint = minted[k];
        const { amount, shareholderId } = mint;
        const shareholderWallet = wallets[shareholderId];
        const { address: shareholderAddress } = shareholderWallet;

        addressesToMint.push(toChecksumAddress(shareholderAddress));
        amountsToMint.push(toWei(String(amount)));

        const accountOutput = _.find(outputData, outputAcc => shareholderId === outputAcc.walletId);
        accountOutput.mintedTokens = accountOutput.mintedTokens || [];
        accountOutput.mintedTokens.push({
          symbol,
          amount,
        });
      }
      const now = new Date();
      const oneYearFromNow = new Date(now.getFullYear() + 1, now.getMonth(), now.getDate());
      const nowTimestamp = Math.floor(now.getTime() / 1000);
      const oneYearFromNowTimestamp = Math.floor(oneYearFromNow.getTime() / 1000);

      const chunkedAddresses = _.chunk(addressesToMint, 40);
      const chunkedAmounts = _.chunk(amountsToMint, 40);

      console.log('\nMinting...\n');

      for (let l = 0; l < chunkedAddresses.length; ++l) {
        const addressChunk = chunkedAddresses[l];
        const amountChunk = chunkedAmounts[l];
        const range = _.range(addressChunk.length);
        const fromTimes = range.map(() => nowTimestamp);
        const toTimes = range.map(() => oneYearFromNowTimestamp);

        const modifyKYCDataMulti = GeneralTransferManager.methods.modifyKYCDataMulti(
          addressChunk,
          fromTimes,
          toTimes,
          toTimes
        );

        await sendTransaction(userAccount, modifyKYCDataMulti, gtmAddress);

        const issueMulti = SecurityToken.methods.issueMulti(addressChunk, amountChunk);

        await sendTransaction(userAccount, issueMulti, tokenAddress);
      }

      const { stoData } = token;

      if (!stoData) {
        continue;
      }

      console.log('\nDeploying STOs...\n');

      const fundRaiseTypesMap = {
        ETH: 0,
        POLY: 1,
        DAI: 2,
      };

      for (let m = 0; m < stoData.length; ++m) {
        const sto = stoData[m];
        const { type } = sto;

        const moduleFactoryAddresses = await ModuleRegistry.methods
          .getModulesByTypeAndToken(3, tokenAddress)
          .call();

        let requiredFactory;
        let requiredFactoryAddress;

        for (let n = 0; n < moduleFactoryAddresses.length; ++n) {
          requiredFactoryAddress = moduleFactoryAddresses[n];
          requiredFactory = new web3.eth.Contract(
            ModuleFactoryArtifact.abi,
            requiredFactoryAddress
          );

          const nameRes = await requiredFactory.methods.name().call();
          const name = hexToUtf8(nameRes);

          if (name === type) {
            break;
          }
        }

        if (!requiredFactory) {
          throw new Error(`No STO module of type ${type} found `);
        }

        const setupCost = await requiredFactory.methods.setupCost().call();

        const transfer = PolyTokenFaucet.methods.transfer(tokenAddress, setupCost);

        await sendTransaction(userAccount, transfer, polyTokenAddress);

        const now = Math.ceil(new Date().getTime() / 1000);
        const startTime = now + sto.startTimeOffset;
        const endTime = now + sto.endTimeOffset;

        let encodedFunctionCall;

        switch (type) {
          case 'CappedSTO': {
            const { walletId: wId, cap, rate, fundRaiseTypes } = sto;

            const params = {
              startTime,
              endTime,
              cap: toWei(String(cap)),
              rate: toWei(String(rate)),
              fundRaiseTypes: fundRaiseTypes.map(fundRaiseType => fundRaiseTypesMap[fundRaiseType]),
              wallet: wallets[wId].address,
            };

            encodedFunctionCall = web3.eth.abi.encodeFunctionCall(
              {
                name: 'configure',
                type: 'function',
                inputs: [
                  {
                    type: 'uint256',
                    name: '_startTime',
                  },
                  {
                    type: 'uint256',
                    name: '_endTime',
                  },
                  {
                    type: 'uint256',
                    name: '_cap',
                  },
                  {
                    type: 'uint256',
                    name: '_rate',
                  },
                  {
                    type: 'uint8[]',
                    name: '_fundRaiseTypes',
                  },
                  {
                    type: 'address',
                    name: '_fundsReceiver',
                  },
                  {
                    type: 'address',
                    name: '_treasuryWallet',
                  }
                ],
              },
              [
                params.startTime,
                params.endTime,
                params.cap,
                params.rate,
                params.fundRaiseTypes,
                params.wallet,
                params.wallet,
              ]
            );

            break;
          }
          case 'USDTieredSTO': {
            const {
              tiers,
              walletId: wId,
              reserveWalletId,
              fundRaiseTypes,
              minimumInvestmentUSD,
              nonAccreditedLimitUSD,
            } = sto;

            const params = {
              startTime,
              endTime,
              ratePerTier: tiers.map(tier => toWei(String(tier.price))),
              ratePerTierDiscountPoly: tiers.map(() => 0),
              tokensPerTier: tiers.map(tier => toWei(String(tier.amount))),
              tokensPerTierDiscountPoly: tiers.map(() => 0),
              nonAccreditedLimitUSD: toWei(String(nonAccreditedLimitUSD)),
              minimumInvestmentUSD: toWei(String(minimumInvestmentUSD)),
              fundRaiseTypes: fundRaiseTypes.map(fundRaiseType => fundRaiseTypesMap[fundRaiseType]),
              wallet: wallets[wId].address,
              reserveWallet: wallets[reserveWalletId].address,
              usdTokens: fundRaiseTypes.find(fundRaiseType => fundRaiseType === 'DAI')
                ? [PolyTokenFaucetArtifact.networks[BLOCKCHAIN_NETWORK_ID].address]
                : [],
            };

            encodedFunctionCall = web3.eth.abi.encodeFunctionCall(
              {
                name: 'configure',
                type: 'function',
                inputs: [
                  {
                    type: 'uint256',
                    name: '_startTime',
                  },
                  {
                    type: 'uint256',
                    name: '_endTime',
                  },
                  {
                    type: 'uint256[]',
                    name: '_ratePerTier',
                  },
                  {
                    type: 'uint256[]',
                    name: '_ratePerTierDiscountPoly',
                  },
                  {
                    type: 'uint256[]',
                    name: '_tokensPerTier',
                  },
                  {
                    type: 'uint256[]',
                    name: '_tokensPerTierDiscountPoly',
                  },
                  {
                    type: 'uint256',
                    name: '_nonAccreditedLimitUSD',
                  },
                  {
                    type: 'uint256',
                    name: '_minimumInvestmentUSD',
                  },
                  {
                    type: 'uint8[]',
                    name: '_fundRaiseTypes',
                  },
                  {
                    type: 'address',
                    name: '_wallet',
                  },
                  {
                    type: 'address',
                    name: '_reserveWallet',
                  },
                  {
                    type: 'address[]',
                    name: '_usdTokens',
                  },
                ],
              },
              [
                params.startTime,
                params.endTime,
                params.ratePerTier,
                params.ratePerTierDiscountPoly,
                params.tokensPerTier,
                params.tokensPerTierDiscountPoly,
                params.nonAccreditedLimitUSD,
                params.minimumInvestmentUSD,
                params.fundRaiseTypes,
                params.wallet,
                params.reserveWallet,
                params.usdTokens,
              ]
            );

            break;
          }
          default: {
            throw new Error(`Invalid STO type: ${type}`);
          }
        }

        const addModule = SecurityToken.methods.addModule(
          requiredFactoryAddress,
          encodedFunctionCall,
          toWei(setupCost),
          0,
          false
        );

        await sendTransaction(userAccount, addModule, tokenAddress);
      }
    }
  }

  console.log(consoleOutput(outputData));
}

module.exports = seedData;