import { HardhatPluginError } from "hardhat/plugins";
import { HardhatRuntimeEnvironment, TaskArguments } from "hardhat/types";
import { TASK_GAS_REPORTER_MERGE_REPORTS } from "../task-names";
import { GasReporterOutput } from "../types";

/**
 * Tries to merge several gas reporter output objects into one.
 */
export async function mergeReports(
  reports: GasReporterOutput[],
  inputFiles: string[]
): Promise<GasReporterOutput> {
  const result: any = {
    namespace: null,
    toolchain: null,
    options: null,
    data: {
      methods: {},
      deployments: [],
      blockLimit: null,
    },
  };

  const { HardhatGasReporterOutputValidator } = await import("../lib/validators/hardhat");

  for (const [index, report] of reports.entries()) {
    const Validator = new HardhatGasReporterOutputValidator();
    Validator.validateOutputObject(report, inputFiles[index]);

    if (result.options === null)   result.options = report.options;
    if (result.namespace === null) result.namespace = report.namespace;
    if (result.toolchain === null) result.toolchain = report.toolchain;

    // Merge data.methods objects
    Object.entries(report.data!.methods).forEach(([key, value]) => {
      Validator.validateMethodDataItem(value, inputFiles[index]);
      if (result.data.methods[key] === undefined) {
        result.data.methods[key] = value;
        return;
      }

      result.data.methods[key].gasData = [
        ...result.data!.methods[key].gasData,
        ...report.data!.methods[key].gasData,
      ].sort((a, b) => a - b);

      result.data.methods[key].callData = [
        ...result.data!.methods[key].callData,
        ...report.data!.methods[key].callData,
      ].sort((a, b) => a - b);

      result.data.methods[key].numberOfCalls +=
        report.data!.methods[key].numberOfCalls;
    });

    // Merge data.deployments objects
    report.data!.deployments.forEach((deployment) => {
      const current = result.data.deployments.find(
        (d: any) => d.name === deployment.name
      );

      if (current !== undefined) {
        current.gasData = [...current.gasData, ...deployment.gasData].sort(
          (a, b) => a - b
        );
        current.callData = [...current.callData, ...deployment.callData].sort(
          (a, b) => a - b
        );
      } else {
        result.data.deployments.push(deployment);
      }
    });
  }

  return result;
}

/**
 * Task for merging multiple gasReporterOutput.json files generated by the plugin
 * This task is necessary when we want to generate different parts of the reports
 * parallelized on different jobs.
 */
export async function subtaskMergeReportsImplementation(
  { inputFiles }: { inputFiles: string[] }
): Promise<GasReporterOutput> {
  const fs = await import("fs");

  const reports = inputFiles.map((input) => JSON.parse(fs.readFileSync(input, "utf-8")));
  return mergeReports(reports, inputFiles);
};


export async function taskMergeImplementation(
  taskArguments: TaskArguments,
  hre: HardhatRuntimeEnvironment
): Promise<void> {
  const path = await import("path");
  const { globSync } = await import("glob");
  const { uniq } = await import("lodash");
  const { reportMerge } = await import("../utils/ui");
  const { GasData } = await import("../lib/gasData");
  const { setGasAndPriceRates } = await import("../utils/prices");
  const { generateJSONData } = await import("../lib/render/json");


  const output = path.resolve(process.cwd(), taskArguments.output);

  // Parse input files and calculate glob patterns
  const taskArgs = uniq(taskArguments.input.map((input: string) => globSync(input)).flat());
  const files = taskArgs.map((file) => path.resolve(file as string))

  if (files.length === 0) {
    throw new HardhatPluginError(
      `hardhat-gas-reporter`,
      `No files found for the given input: ${taskArguments.input.join(" ")}`
    );
  }

  reportMerge(files, output);

  const result = await hre.run(TASK_GAS_REPORTER_MERGE_REPORTS, { inputFiles: files });
  const warnings = await setGasAndPriceRates(result.options);
  const data = new GasData(result.data.methods, result.data.deployments);
  await data.runAnalysis(hre, result.options);

  // Write warnings
  for (const warning of warnings) console.log(warning);

  // Write json
  result.options.outputJSONFile = output;
  generateJSONData(data, result.options);
};
