import { execSync } from "child_process";
import fs from "fs";
import { HardhatPluginError } from "hardhat/plugins";
import path from "path";
import readline from "readline";

import * as ZilliqaUtils from "../ZilliqaUtils";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const parse: any = require("s-expression");

export const isNumeric = (type: string | ADTField) => {
  if (typeof type === "string") {
    switch (type) {
      case "Int64":
      case "Int128":
      case "Int256":
      case "Uint32":
      case "Uint64":
      case "Uint128":
      case "Uint256":
        return true;

      default:
        return false;
    }
  } else {
    return false;
  }
};

export interface TransitionParam {
  type: string;
  name: string;
}

export interface Transition {
  type: string;
  name: string;
  params: TransitionParam[];
}

export interface Field {
  typeJSON?: string | ADTField; // Type in JSON format.
  name: string;
  type: string;
}

export interface ADTField {
  ctor: string;
  argtypes: Field[];
}

export type Transitions = Transition[];
export type ContractName = string;
export type Fields = Field[];

export interface ParsedContract {
  name: ContractName;
  constructorParams: Fields | null;
  transitions: Transitions;
  fields: Fields;
  ctors: ScillaConstructor[];
}

export interface ScillaConstructor {
  typename: string;
  ctorname: string;
  argtypes: string[];
}

export const parseScillaLibrary = async (
  filename: string
): Promise<ParsedContract> => {
  if (!fs.existsSync(filename)) {
    throw new Error(`${filename} doesn't exist.`);
  }

  const fileStream = fs.createReadStream(filename);

  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity,
  });

  let libraryName;
  for await (const line of rl) {
    if (line.trim().startsWith("library")) {
      libraryName = line.trim().split(" ")[1];
      break;
    }
  }

  return {
    name: libraryName || "",
    transitions: [],
    fields: [],
    constructorParams: [],
    ctors: [],
  };
};

export const parseScilla = (filename: string): ParsedContract => {
  const resolvedFilename = path.resolve(filename);
  if (!fs.existsSync(resolvedFilename)) {
    throw new Error(`${resolvedFilename} doesn't exist.`);
  }

  let sexp;
  if (ZilliqaUtils.useNativeScilla()) {
    sexp = execSync(`scilla-fmt --sexp --human-readable ${filename}`);
  } else {
    sexp = execSync(
      `docker run --rm -v ${resolvedFilename}:/tmp/input.scilla -i zilliqa/scilla:v0.13.3 /scilla/0/bin/scilla-fmt --sexp --human-readable /tmp/input.scilla`
    );
  }
  const result: any[] = parse(sexp.toString());

  const libr = result.filter((row: string[]) => row[0] === "libs")[0][1];
  const contr = result.filter((row: string[]) => row[0] === "contr")[0][1];

  const ctors = extractTypes(libr);

  const contractName = extractContractName(contr);
  const contractParams = extractContractParams(contr);

  const cfields = contr.filter((row: string[]) => row[0] === "cfields")[0][1];
  const fields = extractContractFields(cfields);

  const ccomps = contr.filter((row: string[]) => row[0] === "ccomps")[0][1];
  const transitions = extractTransitions(ccomps);

  return {
    name: contractName,
    transitions,
    fields,
    constructorParams: contractParams,
    ctors,
  };
};

const extractTypes = (lib: any) => {
  const ctors: ScillaConstructor[] = [];
  if (lib.length > 0) {
    const lentries = lib[0][1][1];
    for (const lentry of lentries) {
      switch (lentry[0]) {
        case "LibVar":
          break;
        case "LibTyp":
          for (const typector of lentry[2]) {
            const typename = lentry[1][1][1];
            const typectorname = typector[0][1][1][1];
            const typectorargtypes = typector[1][1].map(parseField);

            const userADT: ScillaConstructor = {
              typename,
              ctorname: typectorname,
              argtypes: typectorargtypes,
            };
            ctors.push(userADT);
          }
          break;
      }
    }
  }
  return ctors;
};

const extractContractName = (contrElem: any[]): ContractName => {
  return contrElem
    .filter((row: string[]) => row[0] === "cname")[0][1]
    .filter((row: string[]) => row[0] === "SimpleLocal")[0][1];
};

const extractContractParams = (contrElem: any[]): Fields | null => {
  if (contrElem[1][0] !== "cparams") {
    throw new Error(`Index 0 is not cparams: ${contrElem}`);
  }

  if (contrElem[1][1].length === 0) {
    return null;
  }

  return extractContractFields(contrElem[1][1]);
};

const extractContractFields = (cfieldsElem: any[]): Fields => {
  return cfieldsElem.map((row: any[]): Field => {
    const identData = row[0];
    if (identData[0] !== "Ident") {
      throw new Error(`Index 0 is not Ident: ${identData}`);
    }

    const fieldNameData = identData[1];
    if (fieldNameData[0] !== "SimpleLocal") {
      throw new Error(`Index 0 is not SimpleLocal: ${fieldNameData}`);
    }

    const fieldTypeData = row[1];
    // Currently we just parse PrimType, for the rest we don't parse it completely.
    if (fieldTypeData[0] === "PrimType") {
      return {
        type: fieldTypeData[1],
        name: fieldNameData[1],
      };
    } else if (fieldTypeData[0] === "ADT") {
      const adt = parseAdt(fieldTypeData);
      return {
        typeJSON: adt,
        type:
          adt.ctor + adt.argtypes.map((arg: Field) => " " + arg.type).join(" "),
        name: fieldNameData[1],
      };
    } else if (fieldTypeData[0] === "MapType") {
      return {
        type: "Map",
        name: fieldNameData[1],
      };
    } else if (fieldTypeData[0] === "Address") {
      return {
        type: "ByStr20",
        name: fieldNameData[1],
      };
    } else {
      throw new Error(`Data type is unknown: ${fieldTypeData}`);
    }
  });
};

const extractTransitions = (ccompsElem: any[]): Transitions => {
  return ccompsElem.map((row: any[]) => {
    const compTypeData = row[0];
    if (compTypeData[0] !== "comp_type") {
      throw new Error(`Index 0 is not comp_type ${compTypeData}`);
    }
    const compType = compTypeData[1];

    const compNameData = row[1];
    if (compNameData[0] !== "comp_name") {
      throw new Error(`Index 0 is not comp_name ${compNameData}`);
    }

    const compName = compNameData[1][1];
    if (compName[0] !== "SimpleLocal") {
      throw new Error(`Index 0 is not SimpleLocal: ${compName}`);
    }

    const compParamsData = row[2];

    if (compParamsData[0] !== "comp_params") {
      throw new Error(`Index 0 is not comp_params: ${compParamsData}`);
    }

    const compParams = compParamsData[1].map((r: any[][][]) => {
      const param = parseField(r[1]);
      param.name = r[0][1][1];
      return param;
    });
    return {
      type: compType,
      name: compName[1],
      params: compParams,
    };
  });
};

function parseAdt(row: any): ADTField {
  const ctor = row[1][1][1];
  const argtypes = row[2].map(parseField);
  return {
    ctor,
    argtypes,
  };
}

function generateAdtType(field: ADTField): string {
  if (field.argtypes.length === 0) {
    return field.ctor;
  }

  const type = `${field.ctor} ${field.argtypes
    .map((arg: Field) => {
      // Here we're sure that type is ADTField
      const typeJson: ADTField = arg.typeJSON as ADTField;
      if (["Pair", "List"].includes(typeJson.ctor)) return `(${arg.type})`;
      else return arg.type;
    })
    .reduce((prev, current) => `${prev} ${current}`)}`;
  return type;
}

function parseField(row: any): Field {
  const field_type = row[0];

  if (field_type === "PrimType") {
    const type = row[1];
    return {
      name: "",
      typeJSON: type,
      type,
    };
  } else if (field_type === "ADT") {
    const adt = parseAdt(row);
    const name = row[0][1][1];
    return {
      typeJSON: adt,
      type: generateAdtType(adt),
      name,
    };
  } else if (field_type === "Address") {
    const type = "ByStr20";
    return {
      name: "",
      typeJSON: type,
      type,
    };
  } else {
    throw new HardhatPluginError(
      "hardhat-scilla-plugin",
      `Encountered unexpected field type ${row}`
    );
  }
}

export function generateTypeConstructors(parsedCtors: ScillaConstructor[]) {
  const functions: { [Key: string]: any } = {};
  for (const parsedCtor of parsedCtors) {
    // We need to copy parsedCtor as it is placed in the closure of the function we are declaring so we do
    // not want it to be modified by the floor loop.
    const ctorForClosure: ScillaConstructor = Object.create(parsedCtor);
    functions[ctorForClosure.ctorname] = (args: any[]) => {
      // TODO: Add dynamic type checking.
      return {
        constructor: ctorForClosure.ctorname,
        argtypes: ctorForClosure.argtypes,
        args,
      };
    };
  }
  return functions;
}
