import {
  RPCS,
  NETWORK_ID,
  USDT_ADDR,
  WBEAM_ADDR,
  ROUTER_ADDR,
  WETH_ADDR,
  USDC_ADDR,
  COIN_ADDR,
} from './constants/constants';
import { ethers, ContractTransaction, Contract, Signer, BigNumber } from 'ethers';
import { BigNumber as DecimalBigNumber } from 'bignumber.js';
import { BaseProvider } from '@ethersproject/providers';
import { ChainId, WETH, Fetcher, Route, Token } from 'quickswap-sdk';
import ERC20 from './abis/ERC20.json';
import EVENTS from './abis/Events.json';
import ROUTER from './abis/UniswapV2Router02.json';
import axios from 'axios';
import _ from 'underscore';

const MAX_INT = BigNumber.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
const priceOracleUrl: string = 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2';
const wethAddr: string = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
const ufoAddr: string = '0x249e38ea4102d0cf8264d3701f1a0e39c4f2dc3b';
export async function getWBEAMPrice(): Promise<string> {
  let rpcAddr: string, usdtAddr: string, wBeamAddr: string;
  rpcAddr = RPCS[NETWORK_ID.BEAM];
  usdtAddr = USDT_ADDR[NETWORK_ID.BEAM];
  wBeamAddr = WBEAM_ADDR[NETWORK_ID.BEAM];
  return getTokenPrice(rpcAddr, usdtAddr, 6, wBeamAddr, 18, NETWORK_ID.BEAM);
}

export async function getWETHPrice(): Promise<string> {
  return getWETHPriceFromUniswap();
}

export async function getWETHPriceFromUniswap(): Promise<string> {
  let price = await getPrice(priceOracleUrl, wethAddr);
  return price.toString();
}

export async function getUfoPrice(): Promise<string> {
  let ethUfoPrice = await getPriceETHUFO(priceOracleUrl, ufoAddr);
  let price = await getPrice(priceOracleUrl, wethAddr);
  return DecimalBigNumber(ethUfoPrice).multipliedBy(price).toFixed(9);
}

export async function getMaticPriceFromUniswap(): Promise<string> {
  let ufoAddr: string = '0x7c9f4c87d911613fe9ca58b579f737911aad2d43';
  let price = await getPrice(priceOracleUrl, ufoAddr);
  return price.toString();
}

async function fetchData(requestData: { url; body; headers }): Promise<any> {
  const { data } = await axios.post(requestData.url, requestData.body, { headers: requestData.headers });
  return data;
}

async function getPriceETHUFO(url, address: string): Promise<DecimalBigNumber> {
  address = address.toLowerCase();
  const data = JSON.stringify({
    query: `
      {
        price0: pairs(where:{
          token0: "${address}",
          token1_in : [
            "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
          ]
        }, orderBy: volumeUSD, orderDirection: desc, first: 1) {
          token1Price
          volumeUSD
        }
        price1: pairs(where:{
          token1: "${address}",
          token0_in : [
            "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
          ]
        }, orderBy: volumeUSD, orderDirection: desc, first: 1) {
          token0Price
          volumeUSD
        }
      }
    `,
  });

  const options = {
    url,
    headers: { 'Content-Type': 'application/json' },
    body: data,
  };

  const result = await fetchData(options);

  if (!result) {
    throw new Error('Error while fetching data from subgraph');
  }

  if (result.errors) {
    throw new Error('Error while fetching data from subgraph');
  }

  let price: DecimalBigNumber = DecimalBigNumber(0);

  if (result.data.price0 && result.data.price0.length != 0) {
    price = result.data.price0[0].token1Price;
  }

  if (result.data.price1 && result.data.price1.length != 0) {
    price = result.data.price1[0].token0Price;
  }

  if (result.data.price0 && result.data.price1 && result.data.price0.length != 0 && result.data.price1.length != 0) {
    const volume0 = DecimalBigNumber(result.data.price0[0].volumeUSD);
    const volume1 = DecimalBigNumber(result.data.price1[0].volumeUSD);
    if (volume0.gt(volume1)) {
      price = result.data.price0[0].token1Price;
    }
  }

  return price;
}

async function getPrice(url, address: string): Promise<DecimalBigNumber> {
  address = address.toLowerCase();
  const data = JSON.stringify({
    query: `
      {
        price0: pairs(where:{
          token0: "${address}",
          token1_in : [
            "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
            "0xdac17f958d2ee523a2206206994597c13d831ec7",
            "0x6b175474e89094c44da98b954eedeac495271d0f"
          ]
        }, orderBy: volumeUSD, orderDirection: desc, first: 1) {
          token1Price
          volumeUSD
        }
        price1: pairs(where:{
          token1: "${address}",
          token0_in : [
            "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
            "0xdac17f958d2ee523a2206206994597c13d831ec7",
            "0x6b175474e89094c44da98b954eedeac495271d0f"
          ]
        }, orderBy: volumeUSD, orderDirection: desc, first: 1) {
          token0Price
          volumeUSD
        }
      }
    `,
  });

  const options = {
    url,
    headers: { 'Content-Type': 'application/json' },
    body: data,
  };

  const result = await fetchData(options);

  if (!result) {
    throw new Error('Error while fetching data from subgraph');
  }

  if (result.errors) {
    throw new Error('Error while fetching data from subgraph');
  }

  let price: DecimalBigNumber = DecimalBigNumber(0);

  if (result.data.price0 && result.data.price0.length != 0) {
    price = result.data.price0[0].token1Price;
  }

  if (result.data.price1 && result.data.price1.length != 0) {
    price = result.data.price1[0].token0Price;
  }

  if (result.data.price0 && result.data.price1 && result.data.price0.length != 0 && result.data.price1.length != 0) {
    const volume0 = DecimalBigNumber(result.data.price0[0].volumeUSD);
    const volume1 = DecimalBigNumber(result.data.price1[0].volumeUSD);
    if (volume0.gt(volume1)) {
      price = result.data.price0[0].token1Price;
    }
  }

  return price;
}

export async function getToken1Token2Rate(networkId: number, token1: string, token2: string): Promise<string> {
  let rpcAddr: string, token1Addr: string, token2Addr: string;
  rpcAddr = RPCS[networkId];
  token1Addr = COIN_ADDR[token1][networkId];
  token2Addr = COIN_ADDR[token2][networkId];
  return getTokenPrice(
    rpcAddr,
    token2Addr,
    COIN_ADDR[token2]['decimal'],
    token1Addr,
    COIN_ADDR[token1]['decimal'],
    networkId
  );
}

export async function getTokenBalance(networkId: number, token: string, wallet: string): Promise<string> {
  let rpcAddr: string, tokenAddr: string;
  rpcAddr = RPCS[networkId];
  tokenAddr = COIN_ADDR[token][networkId];
  const provider = ethers.providers.getDefaultProvider(rpcAddr);
  const cryptoContract = new Contract(tokenAddr, ERC20.abi, provider);
  try {
    const balance = await cryptoContract.balanceOf(wallet);
    return ethers.utils.formatUnits(balance.toString(), COIN_ADDR[token]['decimal']).toString();
  } catch (e) {
    console.log(e);
    return '0';
  }
}

async function getTokenPrice(
  rpcProvider,
  sourceAddr,
  sourceDecimal,
  destination,
  destDecimal,
  networkId
): Promise<string> {
  const provider = ethers.providers.getDefaultProvider(rpcProvider);
  const token1 = new Token(networkId, sourceAddr, sourceDecimal);
  const token2 = new Token(networkId, destination, destDecimal);
  try {
    const pair = await Fetcher.fetchPairData(token1, token2, provider);
    const route = new Route([pair], token2);
    return route.midPrice.toSignificant(6);
  } catch (e) {
    console.log('get Token price error', e);
    return '0';
  }
}

export async function swapForExactOutput(
  amountOut: string,
  networkId: number,
  signer: Signer,
  inputTokenType: string,
  outputTokenType: string
): Promise<ContractTransaction> {
  let sourceTokenAddr, destTokenAddr;
  sourceTokenAddr = COIN_ADDR[inputTokenType][networkId];
  destTokenAddr = COIN_ADDR[outputTokenType][networkId];
  const routerContract = new Contract(ROUTER_ADDR[networkId], ROUTER.abi, signer);
  const address = await signer.getAddress();
  return swapExactOutputTokens(sourceTokenAddr, destTokenAddr, amountOut, routerContract, address, signer);
}

export async function getAmountOut(
  amountIn: string,
  networkId: number,
  signer: Signer,
  inputTokenType: string,
  outputTokenType: string
): Promise<string> {
  let sourceTokenAddr, destTokenAddr;
  sourceTokenAddr = COIN_ADDR[inputTokenType][networkId];
  destTokenAddr = COIN_ADDR[outputTokenType][networkId];
  const routerContract = new Contract(ROUTER_ADDR[networkId], ROUTER.abi, signer);
  const tokens = [sourceTokenAddr, destTokenAddr];
  const amountOut = await routerContract.callStatic.getAmountsOut(amountIn, tokens);

  return amountOut[1];
}

export async function getAmountIn(
  amountOut: string,
  networkId: number,
  signer: Signer,
  inputTokenType: string,
  outputTokenType: string
): Promise<string> {
  let sourceTokenAddr, destTokenAddr;
  sourceTokenAddr = COIN_ADDR[inputTokenType][networkId];
  destTokenAddr = COIN_ADDR[outputTokenType][networkId];
  const routerContract = new Contract(ROUTER_ADDR[networkId], ROUTER.abi, signer);
  const tokens = [sourceTokenAddr, destTokenAddr];
  const amountIn = await routerContract.callStatic.getAmountsIn(amountOut, tokens);

  return amountIn[1];
}

export async function swapForExactInput(
  amountIn: string,
  networkId: number,
  signer: Signer,
  inputTokenType: string,
  outputTokenType: string
): Promise<ContractTransaction> {
  let sourceTokenAddr, destTokenAddr;

  sourceTokenAddr = COIN_ADDR[inputTokenType][networkId];
  destTokenAddr = COIN_ADDR[outputTokenType][networkId];

  const routerContract = new Contract(ROUTER_ADDR[networkId], ROUTER.abi, signer);
  const address = await signer.getAddress();
  return swapExactInputTokens(sourceTokenAddr, destTokenAddr, amountIn, routerContract, address, signer);
}

export async function approveTokenToRouter(
  tokenType: string,
  networkId: number,
  signer: Signer
): Promise<ContractTransaction> {
  let tokenAddr = COIN_ADDR[tokenType][networkId];
  const token1 = new Contract(tokenAddr, ERC20.abi, signer);
  return await token1.approve(ROUTER_ADDR[networkId], MAX_INT);
}

async function swapExactInputTokens(
  address1,
  address2,
  amountExactInput,
  routerContract,
  accountAddress,
  signer
): Promise<ContractTransaction> {
  const tokens = [address1, address2];
  const time = Math.floor(Date.now() / 1000) + 200000;
  const deadline = ethers.BigNumber.from(time);

  const token1 = new Contract(address1, ERC20.abi, signer);
  const tokenDecimals = await getDecimals(token1);

  const amountIn = ethers.utils.parseUnits(amountExactInput, tokenDecimals);
  const amountOut = await routerContract.callStatic.getAmountsOut(amountIn, tokens);

  const wMaticAddress = await routerContract.WETH();
  if (address1 === wMaticAddress) {
    // Eth -> Token
    return await routerContract.swapExactETHForTokens(amountOut[1], tokens, accountAddress, deadline, {
      value: amountIn,
    });
  } else if (address2 === wMaticAddress) {
    // Token -> Eth
    return await routerContract.swapExactTokensForETH(amountIn, amountOut[1], tokens, accountAddress, deadline);
  } else {
    return await routerContract.swapExactTokensForTokens(amountIn, amountOut[1], tokens, accountAddress, deadline);
  }
}

async function swapExactOutputTokens(
  address1,
  address2,
  amountExactOut,
  routerContract,
  accountAddress,
  signer
): Promise<ContractTransaction> {
  const tokens = [address1, address2];
  const time = Math.floor(Date.now() / 1000) + 200000;
  const deadline = ethers.BigNumber.from(time);

  const token2 = new Contract(address2, ERC20.abi, signer);
  const tokenDecimals = await getDecimals(token2);

  const amountOut = ethers.utils.parseUnits(amountExactOut, tokenDecimals);
  const amountIn = await routerContract.callStatic.getAmountsIn(amountOut, tokens);
  const wethAddress = await routerContract.WETH();
  if (address1 === wethAddress) {
    // Eth -> Token
    return await routerContract.swapETHForExactTokens(amountOut, tokens, accountAddress, deadline, {
      value: amountIn[0],
    });
  } else if (address2 === wethAddress) {
    // Token -> Eth
    return await routerContract.swapTokensForExactETH(amountIn[0], amountOut, tokens, accountAddress, deadline);
  } else {
    return await routerContract.swapTokensForExactTokens(amountIn[0], amountOut, tokens, accountAddress, deadline);
  }
}

async function getDecimals(token) {
  const decimals = await token
    .decimals()
    .then((result) => {
      return result;
    })
    .catch((error) => {
      console.log('No tokenDecimals function for this token, set to 0');
      return 0;
    });
  return decimals;
}

export function getNftIdsFromTx(eventName: string, logs: ethers.Event[] | undefined): any[] {
  let ret = [];
  let iface = new ethers.utils.Interface(EVENTS);

  logs.forEach((log) => {
    try {
      let data = iface.parseLog(log);
      if (data) {
        if (data.name == eventName && eventName == 'purchaseLootEvent') {
          ret.push(data.args.tokenIds);
        }else if (data.name == eventName && eventName == 'purchaseAndSendGiftEvent') {
          ret.push(data.args.tokenIds);
        } else if (data.name == eventName && eventName == 'mintGensisNFT') {
          ret.push(data.args.nftId);
        } else if (data.name == eventName && eventName == 'mintRandomGensisNFT') {
          ret.push(log.address.substring(2) + '0x' + data.args.nftId);
        }
      }
    } catch (e) {}
  });
  return _.flatten(ret);
}
