// This wrapper was created by extracting the parts of the solc-js package
// (https://github.com/ethereum/solc-js) that we need to perform compilation.

import type { SemverVersion } from "@nomicfoundation/hardhat-utils/fast-semver";

import {
  HardhatError,
  assertHardhatInvariant,
} from "@nomicfoundation/hardhat-errors";
import {
  greaterThanOrEqual,
  parseVersion,
} from "@nomicfoundation/hardhat-utils/fast-semver";

const VERSION_6: SemverVersion = [0, 6, 0];

interface Solc {
  cwrap<T>(ident: string, returnType: string | null, argTypes: string[]): T;

  // eslint-disable-next-line @typescript-eslint/naming-convention -- this is a C function
  _solidity_reset?: Reset | null;
  // eslint-disable-next-line @typescript-eslint/naming-convention -- this is a C function
  _solidity_version?: Version | null;

  _version?: Version | null;

  _compileStandard?: Compile | null;
  // eslint-disable-next-line @typescript-eslint/naming-convention -- this is a C function
  _solidity_compile?: Compile | null;
}

type Reset = () => string;
type Version = () => string;
type Compile = (
  input: string,
  callbackPtr: number | null,
  callbackContextPtr?: null,
) => string;

export interface SolcWrapper {
  compile: CompileWrapper;
  version: Version;
}

export type CompileWrapper = (input: string) => string;

export default function wrapper(solc: Solc): SolcWrapper {
  const version = bindVersion(solc);
  const rawVersion = version();
  const parsedVersion = parseVersion(rawVersion);
  assertHardhatInvariant(
    parsedVersion !== undefined,
    `Invalid solc version: ${rawVersion}`,
  );
  const isVersion6OrNewer = greaterThanOrEqual(parsedVersion, VERSION_6);
  const reset = bindReset(solc);
  const compile = bindCompile(solc, isVersion6OrNewer);

  if (compile === undefined) {
    throw new HardhatError(
      HardhatError.ERRORS.CORE.SOLIDITY.INVALID_SOLCJS_COMPILER,
      {
        version: version(),
      },
    );
  }

  return {
    compile: compileWrapper(isVersion6OrNewer, compile, reset),
    version,
  };
}

function compileWrapper(
  isVersion6OrNewer: boolean,
  compile: Compile,
  reset?: Reset,
): CompileWrapper {
  return (input: string): string => {
    const output = isVersion6OrNewer
      ? compile(input, null, null)
      : compile(input, null);

    if (reset !== undefined) {
      // Explicitly free memory.
      //
      // NOTE: cwrap() of "compile" will copy the returned pointer into a
      //       Javascript string and it is not possible to call free() on it.
      //       reset() however will clear up all allocations.
      reset();
    }

    return output;
  };
}

function bindVersion(solc: Solc): Version {
  if (solc._solidity_version === null || solc._solidity_version === undefined) {
    return solc.cwrap("version", "string", []);
  }

  return solc.cwrap("solidity_version", "string", []);
}

function bindReset(solc: Solc): Reset | undefined {
  if (solc._solidity_reset === null || solc._solidity_reset === undefined) {
    return undefined;
  }

  return solc.cwrap("solidity_reset", null, []);
}

function bindCompile(
  solc: Solc,
  isVersion6OrNewer: boolean,
): CompileWrapper | undefined {
  if (isVersion6OrNewer) {
    if (
      solc._solidity_compile !== null &&
      solc._solidity_compile !== undefined
    ) {
      return solc.cwrap("solidity_compile", "string", [
        "string",
        "number",
        "number",
      ]);
    }
  } else {
    if (
      solc._solidity_compile !== null &&
      solc._solidity_compile !== undefined
    ) {
      return solc.cwrap("solidity_compile", "string", ["string", "number"]);
    }
    if (solc._compileStandard !== null && solc._compileStandard !== undefined) {
      return solc.cwrap("compileStandard", "string", ["string", "number"]);
    }
  }

  return undefined;
}
