import { RPCS, NETWORK_ID, USDT_ADDR, WMATIC_ADDR, ROUTER_ADDR, WETH_ADDR, USDC_ADDR, COIN_ADDR } from './constants/constants';
import { ethers, ContractTransaction, Contract, Signer, BigNumber } from 'ethers';
import { BaseProvider } from '@ethersproject/providers';
import { ChainId, WETH, Fetcher, Route, Token } from 'quickswap-sdk';
import ERC20 from './abis/ERC20.json';
import ROUTER from './abis/UniswapV2Router02.json';

const MAX_INT = BigNumber.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');

export async function getWMATICPrice(networkId: number): Promise<string> {
  let rpcAddr: string, usdtAddr: string, wMaticAddr: string;
  rpcAddr = RPCS[networkId];
  usdtAddr = USDT_ADDR[networkId];
  wMaticAddr = WMATIC_ADDR[networkId];
  return getTokenPrice(rpcAddr, usdtAddr, 6, wMaticAddr, 18, networkId);
}

export async function getWETHPrice(networkId: number): Promise<string> {
  let rpcAddr: string, usdcAddr: string, wEthAddr: string;
  rpcAddr = RPCS[networkId];
  usdcAddr = USDC_ADDR[networkId];
  wEthAddr = WETH_ADDR[networkId];
  return getTokenPrice(rpcAddr, usdcAddr, 6, wEthAddr, 18, networkId);
}

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 usd = new Token(networkId, sourceAddr, sourceDecimal);
  const token = new Token(networkId, destination, destDecimal);
  try{
    const pair = await Fetcher.fetchPairData(usd, token, provider);
    const route = new Route([pair], token);
    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 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;
}

