import {
   TypeDefinition,
   ServiceDefinition,
   EnumDefinition,
   TypeFieldDefinition,
   Step,
} from "../ir";

import { CompileTarget } from "../compile";
// import * as prettier from "prettier";

type lineAppender = (ind: number, line: string | string[]) => void;

const conversion = {
   boolean: "boolean",
   number: "number",
   string: "string",
   void: "void",
};

function toJSType(type: string): string {
   return (conversion as any)[type] || type;
}

export class TypescriptTarget extends CompileTarget {
   name = "Typescript";

   flavour: "esm" | "node" = "node";

   start() {
      this.writeFormattedFile("ts_base.ts", this.getTemplate("ts_base.ts"));
   }

   private generateImport(imports: string, path: string) {
      return `import ${imports} from "${
         path + (this.flavour === "esm" ? ".ts" : "")
      }";\n`;
   }

   private generateImports(
      a: lineAppender,
      def: TypeDefinition | ServiceDefinition
   ) {
      a(
         0,
         this.generateImport(
            `{ VerificationError, apply_number, apply_string, apply_boolean, apply_void }`,
            `./ts_base`
         )
      );
      a(
         0,
         def.depends.map((dep) =>
            this.generateImport(
               `${dep}, { apply_${dep} }`,
               "./" + dep
            )
         )
      );
   }

   private getFileName(typename: string) {
      return typename + ".ts";
   }

   private writeFormattedFile(file: string, code: string) {
      this.writeFile(file, code);
      // const formatted = prettier.format(code, {
      //    parser: "typescript",
      //    tabWidth: 3,
      // });

      // this.writeFile(file, formatted);
   }

   generateType(def: TypeDefinition) {
      let lines: string[] = [];
      const a: lineAppender = (i, t) => {
         if (!Array.isArray(t)) {
            t = [t];
         }
         t.forEach((l) => lines.push("   ".repeat(i) + l.trim()));
      };

      this.generateImports(a, def);
      a(0, `export default class ${def.name} {`);
      a(
         1,
         def.fields.map((field) => {
            let type = "";
            if (field.array) {
               type = toJSType(field.type) + "[]";
            } else if (field.map) {
               type = `{ [key: ${toJSType(field.map)}]: ${toJSType(
                  field.type
               )} }`;
            } else {
               type = toJSType(field.type);
            }
            return `${field.name}?: ${type}; `;
         })
      );

      a(0, ``);
      a(1, `constructor(init?:Partial<${def.name}>){`);
      a(2, `if(init){`);
      def.fields.forEach((field) => {
         a(3, `if(init["${field.name}"])`);
         a(4, `this.${field.name} = init["${field.name}"];`);
      });
      a(2, `}`);
      a(1, `}`);

      a(0, ``);

      a(0, ``);

      a(1, `static apply(data: ${def.name}) {`);
      a(2, `apply_${def.name}(data);`);
      a(1, `}`);
      a(0, `}`);

      a(0, ``);

      a(
         0,
         `export function apply_${def.name}(data: ${def.name}): ${def.name} {`
      );
      {
         a(1, `if(typeof data !== "object") throw new VerificationError("${def.name}", undefined, data);`)
         a(1, `let res = {} as any;`);
         def.fields.forEach((field) => {
            a(1, `if(data["${field.name}"] !== null && data["${field.name}"] !== undefined) {`)
            if (field.array) {
               a(2, `if(!Array.isArray(data["${field.name}"])) throw new VerificationError("array", "${field.name}", data["${field.name}"]);`)
               a(2, `res["${field.name}"] = data["${field.name}"].map(elm=>`);
               a(3, `apply_${field.type}(elm)`);
               a(2, `)`);
            } else if (field.map) {
               a(2, `if(typeof data["${field.name}"] !== "object") throw new VerificationError("map", "${field.name}", data["${field.name}"]);`)
               a(2, `res["${field.name}"] = {}`);
               a(
                  2,
                  `Object.entries(data["${field.name}"]).forEach(([key, val]) => res["${field.name}"][key] = apply_${field.type}(val))`
               );
            } else {
               a(
                  2,
                  `res["${field.name}"] = apply_${field.type}(data["${field.name}"])`
               );
            }
            a(1, `}`);
         });
         a(1, `return res;`);
      }
      a(0, `}`);

      a(0, ``);

      // a(0, `export function verify_${def.name}(data: ${def.name}) {`);
      // {
      //    def.fields.forEach((field) => {
      //       a(
      //          1,
      //          `if(data["${field.name}"] !== null && data["${field.name}"] !== undefined ) {`
      //       );

      //       const verifyType = (varName: string, off = 0) => {
      //          switch (field.type) {
      //             case "string":
      //                a(
      //                   2 + off,
      //                   `if(typeof ${varName} !== "string") throw new VerificationError("string", "${field.name}", ${varName});`
      //                );
      //                break;
      //             case "number":
      //                a(
      //                   2 + off,
      //                   `if(typeof ${varName} !== "number") throw new VerificationError("number", "${field.name}", ${varName});`
      //                );
      //                break;
      //             case "boolean":
      //                a(
      //                   2 + off,
      //                   `if(typeof ${varName} !== "boolean") throw new VerificationError("boolean", "${field.name}", ${varName});`
      //                );
      //                break;
      //             default:
      //                a(
      //                   2 + off,
      //                   `verify_${field.type}(${varName});`
      //                );
      //          }
      //       };

      //       if (field.array) {
      //          a(
      //             2,
      //             ``
      //          );
      //          a(2, `for(const elm of data["${field.name}"]) {`);
      //          verifyType("elm", 1);
      //          a(2, `}`);
      //       } else if (field.map) {
      //          a(
      //             2,
      //             `if(typeof data["${field.name}"] !== "object") throw new VerificationError("object", "${field.name}", data["${field.name}"]);`
      //          );
      //          a(2, `for(const key in data["${field.name}"]) {`);
      //          verifyType(`data["${field.name}"][key]`, 1);
      //          a(2, `}`);
      //       } else {
      //          verifyType(`data["${field.name}"]`);
      //       }
      //       a(1, "}");
      //       a(0, ``);
      //    });
      //    a(1, `return data`);
      // }
      // a(0, `}`);

      this.writeFormattedFile(this.getFileName(def.name), lines.join("\n"));
   }

   generateEnum(def: EnumDefinition) {
      let lines: string[] = [];
      const a: lineAppender = (i, t) => {
         if (!Array.isArray(t)) {
            t = [t];
         }
         t.forEach((l) => lines.push("   ".repeat(i) + l.trim()));
      };
      a(0, this.generateImport("{ VerificationError }", "./ts_base"));

      a(0, `enum ${def.name} {`);
      for (const value of def.values) {
         a(1, `${value.name}=${value.value},`);
      }
      a(0, `}`);

      a(0, ``);

      a(0, `export default ${def.name}`);

      a(0, ``);

      a(
         0,
         `export function apply_${def.name} (data: ${def.name}): ${def.name} {`
      );
      a(1, `data = Number(data);`);
      a(
         1,
         `if(${def.name}[data] == undefined) throw new VerificationError("${def.name}", undefined, data);`
      );
      a(1, `return data;`);
      a(0, `}`);

      this.writeFormattedFile(this.getFileName(def.name), lines.join("\n"));
   }

   generateServiceClient(def: ServiceDefinition) {
      this.writeFormattedFile(
         "service_client.ts",
         this.generateImport(
            "{ RequestObject, ResponseObject, ErrorCodes, Logging }",
            "./service_base"
         ) +
            this.generateImport(" { VerificationError }", "./ts_base") +
            "\n\n" +
            this.getTemplate("ts_service_client.ts")
      );

      let lines: string[] = [];
      const a: lineAppender = (i, t) => {
         if (!Array.isArray(t)) {
            t = [t];
         }
         t.forEach((l) => lines.push("   ".repeat(i) + l.trim()));
      };

      this.generateImports(a, def);

      a(0, `export type {`);
      def.depends.forEach((dep) => {
         a(1, `${dep},`);
      });
      a(0, `}`);

      a(
         0,
         this.generateImport(
            "{ Service, ServiceProvider, getRandomID }",
            "./service_client"
         )
      );

      a(0, ``);

      a(0, `export class ${def.name} extends Service {`);
      a(1, `constructor(provider: ServiceProvider){`);
      a(2, `super(provider, "${def.name}");`);
      a(1, `}`);

      for (const fnc of def.functions) {
         const params = fnc.inputs
            .map(
               (e) => `${e.name}: ${toJSType(e.type) + (e.array ? "[]" : "")}`
            )
            .join(", ");

         //TODO: Prio 1 : Verify and strip input parameters
         //TODO: Prio 2 : Add optional parameters to this and the declaration file
         // const verifyInputs = () => {

         // }

         if (!fnc.return) {
            a(1, `${fnc.name}(${params}): void {`);1
            a(2, `this._provider.sendMessage({`);
            a(3, `jsonrpc: "2.0",`);
            a(3, `method: "${def.name}.${fnc.name}",`);
            a(3, `params: [...arguments]`);
            a(2, `});`);
            a(1, `}`);
         } else {
            const retType = fnc.return
               ? toJSType(fnc.return.type) + (fnc.return.array ? "[]" : "")
               : "void";
            a(1, `${fnc.name}(${params}): Promise<${retType}> {`);
            a(2, `return new Promise<${retType}>((ok, err) => {`);
            a(3, `this._provider.sendMessage({`);
            a(4, `jsonrpc: "2.0",`);
            a(4, `id: getRandomID(16),`);
            a(4, `method: "${def.name}.${fnc.name}",`);
            a(4, `params: [...arguments]`);
            a(3, `}, {`);
            a(4, `ok, err`);
            a(3, `});`);
            a(2, `}).then(result => {`);
            if (fnc.return.array) {
               a(3, `for(let i = 0; i < result.length; i++) {`);
               a(4, `result[i] = apply_${fnc.return.type}(result[i]);`);
               a(3, `}`);
            } else {
               a(3, `result = apply_${fnc.return.type}(result);`);
            }
            a(3, `return result;`);
            a(2, `});`);
            a(1, `}`);
         }
         a(0, ``);
      }

      a(0, `}`);

      this.writeFormattedFile(
         this.getFileName(def.name + "_client"),
         lines.join("\n")
      );
   }

   generateServiceServer(def: ServiceDefinition) {
      let lines: string[] = [];
      const a: lineAppender = (i, t) => {
         if (!Array.isArray(t)) {
            t = [t];
         }
         t.forEach((l) => lines.push("   ".repeat(i) + l.trim()));
      };

      this.writeFormattedFile(
         "service_server.ts",
         this.generateImport(
            "{ RequestObject, ResponseObject, ErrorCodes, Logging }",
            "./service_base"
         ) +
            this.generateImport(" { VerificationError }", "./ts_base") +
            "\n\n" +
            this.getTemplate("ts_service_server.ts")
      );

      this.generateImports(a, def);

      a(0, `export type {`);
      def.depends.forEach((dep) => {
         a(1, `${dep},`);
      });
      a(0, `}`);

      a(0, this.generateImport("{ Service }", "./service_server"));

      a(0, ``);

      a(0, `export abstract class ${def.name}<T> extends Service<T> {`);
      a(1, `public name = "${def.name}";`);
      a(1, `constructor(){`);
      a(2, `super();`);
      for (const fnc of def.functions) {
         a(2, `this.functions.add("${fnc.name}")`);
      }
      a(1, `}`);
      a(0, ``);

      for (const fnc of def.functions) {
         const params = [
            ...fnc.inputs.map(
               (e) => `${e.name}: ${toJSType(e.type) + (e.array ? "[]" : "")}`
            ),
            `ctx: T`,
         ].join(", ");
         const retVal = fnc.return
            ? `Promise<${
                 toJSType(fnc.return.type) + (fnc.return.array ? "[]" : "")
              }>`
            : `void`;
         a(1, `abstract ${fnc.name}(${params}): ${retVal};`);

         // a(0, ``);

         a(1, `_${fnc.name}(params: any[] | any, ctx: T): ${retVal} {`);
         a(2, `let p: any[] = [];`);
         a(2, `if(Array.isArray(params)){`);
         a(3, `p = params;`);
         a(2, `} else {`);
         for (const param of fnc.inputs) {
            a(3, `p.push(params["${param.name}"])`);
         }
         a(2, `}`);
         a(2, ``);

         for (let i = 0; i < fnc.inputs.length; i++) {
            a(2, `if(p[${i}] !== null && p[${i}] !== undefined) {`);
            if (fnc.inputs[i].array) {
               a(2, `for(const elm of p[${i}]) {`);
               a(3, `apply_${fnc.inputs[i].type}(elm)`);
               a(2, `}`);
            } else {
               a(2, `apply_${fnc.inputs[i].type}(p[${i}])`);
            }
            a(2, `}`);
         }

         a(2, ``);
         a(2, `p.push(ctx);`);

         a(
            2,
            `return this.${fnc.name}.call(this, ...p)` + //TODO: Refactor. This line is way to compicated for anyone to understand, including me
               (fnc.return
                  ? `.then(${
                       fnc.return?.array
                          ? `res => res.map(e => apply_${fnc.return.type}(e))`
                          : `res => apply_${fnc.return.type}(res)`
                    });`
                  : "")
         );
         a(1, `}`);
         a(0, ``);
      }

      a(0, `}`);

      this.writeFormattedFile(
         this.getFileName(def.name + "_server"),
         lines.join("\n")
      );
   }

   generateService(def: ServiceDefinition) {
      this.writeFormattedFile(
         "service_base.ts",
         this.getTemplate("ts_service_base.ts")
      );
      this.generateServiceClient(def);
      this.generateServiceServer(def);
   }

   finalize(steps: Step[]) {
      let linesClient: string[] = [];
      let linesServer: string[] = [];

      const ac: lineAppender = (i, t) => {
         if (!Array.isArray(t)) {
            t = [t];
         }
         t.forEach((l) => linesClient.push("   ".repeat(i) + l.trim()));
      };

      const as: lineAppender = (i, t) => {
         if (!Array.isArray(t)) {
            t = [t];
         }
         t.forEach((l) => linesServer.push("   ".repeat(i) + l.trim()));
      };

      let lines: string[] = [];
      const a: lineAppender = (i, t) => {
         if (!Array.isArray(t)) {
            t = [t];
         }
         t.forEach((l) => lines.push("   ".repeat(i) + l.trim()));
      };

      let hasService = false;
      steps.forEach(([type, def]) => {
         switch (type) {
            case "type":
               a(
                  0,
                  this.generateImport(
                     `${def.name}, { apply_${def.name} }`,
                     "./" + def.name
                  )
               );

               a(0, `export { apply_${def.name} }`);
               a(0, `export type { ${def.name} }`);
               a(0, ``);
               break;
            case "enum":
               a(
                  0,
                  this.generateImport(
                     `${def.name}, { apply_${def.name} }`,
                     "./" + def.name
                  )
               );
               a(
                  0,
                  `export { ${def.name}, apply_${def.name} }`
               );
               a(0, ``);
               break;

            case "service":
               let ext = this.flavour == "esm" ? ".ts" : "";
               if (!hasService) {
                  hasService = true;
                  ac(0, `export * from "./service_client${ext}"`);
                  ac(0, ``);

                  as(0, `export * from "./service_server${ext}"`);
                  as(0, ``);

                  a(0, `export * as Client from "./index_client${ext}"`);
                  a(0, `export * as Server from "./index_server${ext}"`);
                  a(0, `export { Logging } from "./service_base${ext}"`);
                  a(0, ``);
               }

               ac(
                  0,
                  `export { ${def.name} } from "./${def.name}_client${ext}"`
               );
               as(
                  0,
                  `export { ${def.name} } from "./${def.name}_server${ext}"`
               );
               ac(0, ``);
               as(0, ``);
               break;
         }
      });

      this.writeFormattedFile(this.getFileName("index"), lines.join("\n"));

      this.writeFormattedFile(
         this.getFileName("index_client"),
         linesClient.join("\n")
      );

      this.writeFormattedFile(
         this.getFileName("index_server"),
         linesServer.join("\n")
      );
   }
}

export class ESMTypescriptTarget extends TypescriptTarget {
   name = "ts-esm";
   flavour: "esm" = "esm";
}

export class NodeJSTypescriptTarget extends TypescriptTarget {
   name = "ts-node";
   flavour: "node" = "node";
}
