import { dA as caches, ax as getCreate2FactoryAddress, aK as fetchAndCacheDeployMetadata, aJ as fetchPublishedContractFromPolygon, aF as computeDeploymentInfo, as as GAS_LIMIT_FOR_DEPLOYER, au as DEPLOYER_ABI, at as DEPLOYER_BYTECODE, av as isContractDeployed } from './index-e27ddc0a.browser.esm.js';
import { utils, ContractFactory, BigNumber } from 'ethers';
import invariant from 'tiny-invariant';

function getFunctionSignature(fnInputs) {
  return "(" + fnInputs.map(i => {
    return i.type === "tuple" ? getFunctionSignature(i.components) : i.type === "tuple[]" ? getFunctionSignature(i.components) + `[]` : i.type;
  }).join(",") + ")";
}
function generatePluginFunctions(pluginAddress, pluginAbi) {
  const pluginInterface = new utils.Interface(pluginAbi);
  const pluginFunctions = [];
  // TODO - filter out common functions like _msgSender(), contractType(), etc.
  for (const fnFragment of Object.values(pluginInterface.functions)) {
    const fn = pluginInterface.getFunction(pluginInterface.getSighash(fnFragment));
    if (fn.name.includes("_")) {
      continue;
    }
    pluginFunctions.push({
      functionSelector: pluginInterface.getSighash(fn),
      functionSignature: fn.name + getFunctionSignature(fn.inputs),
      pluginAddress: pluginAddress
    });
  }
  return pluginFunctions;
}
function generateExtensionFunctions(extensionAbi) {
  const extensionInterface = new utils.Interface(extensionAbi);
  const extensionFunctions = [];
  // TODO - filter out common functions like _msgSender(), contractType(), etc.

  for (const fnFragment of Object.values(extensionInterface.functions)) {
    const fn = extensionInterface.getFunction(extensionInterface.getSighash(fnFragment));
    if (fn.name.startsWith("_")) {
      continue;
    }
    extensionFunctions.push({
      functionSelector: extensionInterface.getSighash(fn),
      functionSignature: fn.name + getFunctionSignature(fn.inputs)
    });
  }
  return extensionFunctions;
}

/**
 *
 * Returns txn data for keyless deploys as well as signer deploys.
 * Also provides a list of infra contracts to deploy.
 *
 * @internal
 *
 * @param metadataUri - The metadata uri to use
 * @param storage - The storage to use
 * @param provider - The provider to use
 * @param create2Factory - The create2 factory to use
 */
async function getDeploymentInfo(metadataUri, storage, provider, create2Factory, clientId, secretKey) {
  caches.deploymentPresets = {};
  const [create2FactoryAddress, {
    compilerMetadata,
    extendedMetadata
  }] = await Promise.all([create2Factory ? create2Factory : getCreate2FactoryAddress(provider), fetchAndCacheDeployMetadata(metadataUri, storage)]);
  const customParams = {};
  const finalDeploymentInfo = [];
  const defaultExtensions = extendedMetadata?.defaultExtensions;
  if (extendedMetadata?.routerType === "plugin" && defaultExtensions) {
    invariant(clientId || secretKey, "Require Client Id / Secret Key");
    const publishedExtensions = await Promise.all(defaultExtensions.map(e => {
      return fetchPublishedContractFromPolygon(e.publisherAddress, e.extensionName, e.extensionVersion, storage, clientId, secretKey);
    }));
    const pluginMetadata = (await Promise.all(publishedExtensions.map(c => fetchAndCacheDeployMetadata(c.metadataUri, storage)))).map(fetchedMetadata => fetchedMetadata.compilerMetadata);

    // get deployment info for all plugins
    const pluginDeploymentInfo = await Promise.all(pluginMetadata.map(metadata => computeDeploymentInfo("plugin", provider, storage, create2FactoryAddress, {
      metadata: metadata
    }, clientId, secretKey)));

    // create constructor param input for PluginMap
    const mapInput = [];
    pluginMetadata.forEach((metadata, index) => {
      const input = generatePluginFunctions(pluginDeploymentInfo[index].transaction.predictedAddress, metadata.abi);
      mapInput.push(...input);
    });

    // get PluginMap deployment transaction
    const pluginMapTransaction = await computeDeploymentInfo("plugin", provider, storage, create2FactoryAddress, {
      contractName: "PluginMap",
      constructorParams: {
        _pluginsToAdd: {
          value: mapInput
        }
      }
    }, clientId, secretKey);

    // address of PluginMap is input for MarketplaceV3's constructor
    customParams["_pluginMap"] = {
      value: pluginMapTransaction.transaction.predictedAddress
    };
    finalDeploymentInfo.push(...pluginDeploymentInfo, pluginMapTransaction);
  } else if (extendedMetadata?.routerType === "dynamic" && defaultExtensions) {
    invariant(clientId || secretKey, "Require Client Id / Secret Key");
    const publishedExtensions = await Promise.all(defaultExtensions.map(e => {
      return fetchPublishedContractFromPolygon(e.publisherAddress, e.extensionName, e.extensionVersion, storage, clientId, secretKey);
    }));
    const extensionMetadata = (await Promise.all(publishedExtensions.map(async c => {
      return fetchAndCacheDeployMetadata(c.metadataUri, storage);
    }))).map(fetchedMetadata => fetchedMetadata.compilerMetadata);

    // get deployment info for all extensions
    const extensionDeploymentInfo = await Promise.all(extensionMetadata.map(metadata => computeDeploymentInfo("extension", provider, storage, create2FactoryAddress, {
      metadata: metadata
    }, clientId, secretKey)));

    // create constructor param input for BaseRouter
    const routerInput = [];
    extensionMetadata.forEach((metadata, index) => {
      const extensionFunctions = generateExtensionFunctions(metadata.abi);
      routerInput.push({
        metadata: {
          name: metadata.name,
          metadataURI: "",
          implementation: extensionDeploymentInfo[index].transaction.predictedAddress
        },
        functions: extensionFunctions
      });
    });

    // routerInput as constructor param for BaseRouter
    customParams["_extensions"] = {
      value: routerInput
    };
    finalDeploymentInfo.push(...extensionDeploymentInfo);
  }
  const [implementationDeployInfo, factoryInfo] = await Promise.all([computeDeploymentInfo("implementation", provider, storage, create2FactoryAddress, {
    metadata: compilerMetadata,
    constructorParams: customParams
  }, clientId, secretKey),
  // get clone factory
  computeDeploymentInfo("infra", provider, storage, create2FactoryAddress, {
    contractName: "TWCloneFactory"
  }, clientId, secretKey)]);
  finalDeploymentInfo.push(factoryInfo);
  finalDeploymentInfo.push(...Object.values(caches.deploymentPresets));
  finalDeploymentInfo.push(implementationDeployInfo);
  return finalDeploymentInfo;
}

/**
 * @internal
 */
function estimateGasForDeploy(initCode) {
  let gasLimit = utils.arrayify(initCode).map(x => x === 0 ? 4 : 16).reduce((sum, x) => sum + x) + 200 * initCode.length / 2 + 6 * Math.ceil(initCode.length / 64) + 32000 + 21000;
  gasLimit = Math.floor(gasLimit * 64 / 63);
  return gasLimit;
}

/**
 * @internal
 */
function createTransactionBatches(transactions) {
  let upperGasLimit = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : GAS_LIMIT_FOR_DEPLOYER;
  transactions = transactions.filter(tx => {
    return tx.data.length > 0;
  });
  if (transactions.length === 0) {
    return [];
  }
  const transactionBatches = [];
  let sum = 0;
  let batch = [];
  transactions.forEach(tx => {
    const gas = estimateGasForDeploy(tx.data);
    if (sum + gas > upperGasLimit) {
      if (batch.length === 0) {
        transactionBatches.push([tx]);
      } else {
        transactionBatches.push(batch);
        sum = gas;
        batch = [tx];
      }
    } else {
      sum += gas;
      batch.push(tx);
    }
  });
  if (batch.length > 0) {
    transactionBatches.push(batch);
  }
  return transactionBatches;
}

/**
 * @internal
 */
async function deployWithThrowawayDeployer(signer, transactions, options) {
  const transactionBatches = createTransactionBatches(transactions);
  if (transactionBatches.length === 0) {
    return;
  }
  options?.notifier?.("deploying", "infra");
  const deployTxns = await Promise.all(transactionBatches.map(txBatch => {
    // Using the deployer contract, send the deploy transactions to common factory with a signer
    const deployer = new ContractFactory(DEPLOYER_ABI, DEPLOYER_BYTECODE).connect(signer).deploy(txBatch);
    return deployer;
  }));
  await Promise.all(deployTxns.map(tx => {
    return tx.deployed();
  }));
  options?.notifier?.("deployed", "infra");
}

/**
 * Deploy a contract at a deterministic address, using Create2 method
 * Address depends on the Create2 factory address.
 *
 * @internal
 *
 * @param signer - The signer to use
 * @param bytecode - The bytecode to deploy
 * @param encodedArgs - The encoded constructor args to use
 * @param create2FactoryAddress - The create2 factory address to use
 */
async function deployContractDeterministic(signer, transaction, options) {
  let gasLimit = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 7000000;
  // Check if the implementation contract is already deployed
  invariant(signer.provider, "Provider required");
  const contractDeployed = await isContractDeployed(transaction.predictedAddress, signer.provider);
  if (!contractDeployed) {
    console.debug(`deploying contract via create2 factory at: ${transaction.predictedAddress}`);
    const tx = {
      to: transaction.to,
      data: transaction.data
    };
    try {
      await signer.estimateGas(tx);
    } catch (e) {
      console.debug("error estimating gas while deploying prebuilt: ", e);
      tx.gasLimit = BigNumber.from(gasLimit);
    }
    options?.notifier?.("deploying", "preset");
    await (await signer.sendTransaction(tx)).wait();
    options?.notifier?.("deployed", "preset");
  }
}

export { deployWithThrowawayDeployer as a, generateExtensionFunctions as b, createTransactionBatches as c, deployContractDeterministic as d, estimateGasForDeploy as e, getDeploymentInfo as g };
