import { ethers } from "ethers";
import { ParamType } from "ethers/lib/utils";
import { getProvider, Chain } from "../general";
import convertResults from "./convertResults";
import { call } from "./rpcCall";

export const MULTICALL_ADDRESS_MAINNET =
  "0xeefba1e63905ef1d7acba5a8513c70307c1ce441";
export const MULTICALL_ADDRESS_KOVAN =
  "0x2cc8688c5f75e365aaeeb4ea8d6a480405a48d2a";
export const MULTICALL_ADDRESS_RINKEBY =
  "0x42ad527de7d4e9d9d011ac45b31d8551f8fe9821";
export const MULTICALL_ADDRESS_GOERLI =
  "0x77dca2c955b15e9de4dbbcf1246b4b85b651e50e";
export const MULTICALL_ADDRESS_POLYGON =
  "0x95028E5B8a734bb7E2071F96De89BABe75be9C8E";
export const MULTICALL_ADDRESS_BSC =
  "0x1Ee38d535d541c55C9dae27B12edf090C608E6Fb";
export const MULTICALL_ADDRESS_FANTOM =
  "0xb828C456600857abd4ed6C32FAcc607bD0464F4F";
export const MULTICALL_ADDRESS_XDAI =
  "0xb5b692a88BDFc81ca69dcB1d924f59f0413A602a";
export const MULTICALL_ADDRESS_HECO =
  "0xc9a9F768ebD123A00B52e7A0E590df2e9E998707";
export const MULTICALL_ADDRESS_HARMONY =
  "0xFE4980f62D708c2A84D3929859Ea226340759320";
export const MULTICALL_ADDRESS_ARBITRUM =
  "0x842eC2c7D803033Edf55E478F461FC547Bc54EB2";
export const MULTICALL_ADDRESS_AVAX =
  "0xdf2122931FEb939FB8Cf4e67Ea752D1125e18858";
export const MULTICALL_ADDRESS_MOONRIVER =
  "0xe05349d6fE12602F6084550995B247a5C80C0E2C";
export const MULTICALL_ADDRESS_AURORA =
  "0xe0e3887b158F7F9c80c835a61ED809389BC08d1b";
export const MULTICALL_ADDRESS_OPTIMISM =
  "0xD0E99f15B24F265074747B2A1444eB02b9E30422";

export const AGGREGATE_SELECTOR = "0x252dba42";

export default async function makeMultiCall(
  functionABI: any,
  calls: {
    contract: string;
    params: any[];
  }[],
  chain: Chain,
  block?: number
) {
  const contractInterface = new ethers.utils.Interface([functionABI]);
  let fd = Object.values(contractInterface.functions)[0];

  const contractCalls = calls.map((call) => {
    const data = contractInterface.encodeFunctionData(fd, call.params);
    return {
      to: call.contract,
      data,
    };
  });

  const returnValues = await executeCalls(contractCalls, chain, block);

  return returnValues.map((values: any, index: number) => {
    let output: any;
    try {
      output = convertResults(
        contractInterface.decodeFunctionResult(fd, values)
      );
    } catch (e) {
      output = null;
    }
    return {
      input: {
        params: calls[index].params,
        target: calls[index].contract,
      },
      success: output !== null,
      output,
    };
  });
}

async function executeCalls(
  contractCalls: {
    to: string;
    data: string;
  }[],
  chain: Chain,
  block?: number
) {
  if (await networkSupportsMulticall(chain)) {
    try {
      const multicallData = ethers.utils.defaultAbiCoder.encode(
        [
          ParamType.fromObject({
            components: [
              { name: "target", type: "address" },
              { name: "callData", type: "bytes" },
            ],
            name: "data",
            type: "tuple[]",
          }),
        ],
        [contractCalls.map((call) => [call.to, call.data])]
      );
      const address = await multicallAddressOrThrow(chain);

      const callData = AGGREGATE_SELECTOR + multicallData.substr(2);

      const tx = {
        to: address,
        data: callData,
      };

      const returnData = await call(getProvider(chain), tx, block ?? "latest", chain)

      const [blockNumber, returnValues] = ethers.utils.defaultAbiCoder.decode(
        ["uint256", "bytes[]"],
        returnData
      );
      return returnValues;
    } catch (e) {
      if (!process.env.DEFILLAMA_SDK_MUTED) {
        console.log("Multicall failed, defaulting to single transactions...");
      }
    }
  }
  const values = await Promise.all(
    contractCalls.map(async ({ to, data }) => {
      try {
        return await call(getProvider(chain),
          { to, data },
          block ?? "latest",
          chain,
        );
      } catch (e) {
        return null;
      }
    })
  );
  return values;
}

async function multicallAddressOrThrow(chain: Chain) {
  const network = await getProvider(chain).getNetwork();
  const address = multicallAddress(network.chainId);
  if (address === null) {
    const msg = `multicall is not available on the network ${network.chainId}`;
    console.error(msg);
    throw new Error(msg);
  }
  return address;
}

async function networkSupportsMulticall(chain: Chain) {
  const network = await getProvider(chain).network;
  const address = multicallAddress(network.chainId);
  return address !== null;
}

function multicallAddress(chainId: number) {
  switch (chainId) {
    case 1:
      return MULTICALL_ADDRESS_MAINNET;
    case 42:
      return MULTICALL_ADDRESS_KOVAN;
    case 4:
      return MULTICALL_ADDRESS_RINKEBY;
    case 5:
      return MULTICALL_ADDRESS_GOERLI;
    case 137:
      return MULTICALL_ADDRESS_POLYGON;
    case 56:
      return MULTICALL_ADDRESS_BSC;
    case 250:
      return MULTICALL_ADDRESS_FANTOM;
    case 100:
      return MULTICALL_ADDRESS_XDAI;
    case 128:
      return MULTICALL_ADDRESS_HECO;
    case 1666600000:
      return MULTICALL_ADDRESS_HARMONY;
    case 42161:
      return MULTICALL_ADDRESS_ARBITRUM;
    case 43114:
      return MULTICALL_ADDRESS_AVAX;
    case 1285:
      return MULTICALL_ADDRESS_MOONRIVER;
    case 1313161554:
      return MULTICALL_ADDRESS_AURORA;
    case 10:
      return MULTICALL_ADDRESS_OPTIMISM;
    case 25:
      return "0x5e954f5972EC6BFc7dECd75779F10d848230345F"; // cronos
    case 246:
      return "0x18fA376d92511Dd04090566AB6144847c03557d8"; // energy web chain
    case 336:
      return "0x18fA376d92511Dd04090566AB6144847c03557d8"; // shiden
    case 592:
      return "0x18fA376d92511Dd04090566AB6144847c03557d8"; // astar
    case 269:
      return "0x18fA376d92511Dd04090566AB6144847c03557d8"; // High performance blockchain
    default:
      return null;
  }
}