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

import { CompileTarget } from "../compile";

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

const conversion = {
   boolean: "bool",
   int: "long",
   float: "double",
   string: "string",
   void: "void",
   bytes: "",
};

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

export class CSharpTarget extends CompileTarget<{ csharp_namespace: string }> {
   name: string = "c#";

   get namespace() {
      return this.options.csharp_namespace || "JRPC";
   }

   start(): void {
      if (this.options.allow_bytes == true) {
         throw new Error("C# has no support for 'bytes' yet!");
      }
      this.writeFile(
         this.namespace + ".csproj",
         this.getTemplate("CSharp/CSharp.csproj")
      );

      const fixNS = (input: string) =>
         input.replace("__NAMESPACE__", this.namespace);
      const copyClass = (name: string) =>
         this.writeFile(
            name + ".cs",
            fixNS(this.getTemplate(`CSharp/${name}.cs`))
         );
      copyClass("JRpcClient");
      copyClass("JRpcServer");
      copyClass("JRpcTransport");
   }

   generateType(definition: TypeDefinition): void {
      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, `using System.Text.Json;`);
      a(0, `using System.Text.Json.Serialization;`);
      a(0, `using System.Collections.Generic;`);
      a(0, ``);
      a(0, `namespace ${this.namespace};`);
      a(0, ``);
      a(0, `public class ${definition.name} {`);
      for (const field of definition.fields) {
         if (field.array) {
            a(
               1,
               `public IList<${toCSharpType(field.type)}>? ${
                  field.name
               } { get; set; }`
            );
         } else if (field.map) {
            a(
               1,
               `public Dictionary<${toCSharpType(field.map)}, ${toCSharpType(
                  field.type
               )}>? ${field.name} { get; set; }`
            );
         } else {
            a(
               1,
               `public ${toCSharpType(field.type)}? ${field.name} { get; set; }`
            );
         }
      }
      a(0, `}`);

      this.writeFile(`${definition.name}.cs`, lines.join("\n"));
   }

   generateEnum(definition: EnumDefinition): void {
      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, `using System.Text.Json;`);
      a(0, `using System.Text.Json.Serialization;`);
      a(0, ``);
      a(0, `namespace ${this.namespace};`);
      a(0, ``);
      a(0, `public enum ${definition.name} {`);
      for (const field of definition.values) {
         a(1, `${field.name} = ${field.value},`);
      }
      a(0, `}`);

      this.writeFile(`${definition.name}.cs`, lines.join("\n"));
   }

   generateServiceClient(definition: ServiceDefinition) {
      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, `using System;`);
      a(0, `using System.Text.Json;`);
      a(0, `using System.Text.Json.Serialization;`);
      a(0, `using System.Text.Json.Nodes;`);
      a(0, `using System.Threading.Tasks;`);
      a(0, `using System.Collections.Generic;`);
      a(0, ``);
      a(0, `namespace ${this.namespace};`);
      a(0, ``);
      a(0, `public class ${definition.name}Client {`);
      a(0, ``);
      a(1, `private JRpcClient Client;`);
      a(0, ``);
      a(1, `public ${definition.name}Client(JRpcClient client) {`);
      a(2, `this.Client = client;`);
      a(1, `}`);
      a(0, ``);
      for (const fnc of definition.functions) {
         let params = fnc.inputs
            .map((inp) => {
               if (inp.array) {
                  return `List<${toCSharpType(inp.type)}> ${inp.name}`;
               } else {
                  return `${toCSharpType(inp.type)} ${inp.name}`;
               }
            })
            .join(", ");

         const genParam = () =>
            a(
               2,
               `var param = new JsonArray(${fnc.inputs
                  .map((e) => `JsonSerializer.SerializeToNode(${e.name})`)
                  .join(", ")});`
            );

         if (fnc.return) {
            if (fnc.return.type == "void") {
               a(1, `public async Task ${fnc.name}(${params}) {`);
               genParam();
               a(
                  2,
                  `await this.Client.SendRequestRaw("${definition.name}.${fnc.name}", param);`
               );
               a(1, `}`);
            } else {
               let ret = fnc.return
                  ? fnc.return.array
                     ? `IList<${toCSharpType(fnc.return.type)}>`
                     : toCSharpType(fnc.return.type)
                  : undefined;
               a(1, `public async Task<${ret}?> ${fnc.name}(${params}) {`);
               genParam();
               a(
                  2,
                  `return await this.Client.SendRequest<${ret}>("${definition.name}.${fnc.name}", param);`
               );
               a(1, `}`);
            }
         } else {
            //Notification
            a(1, `public void ${fnc.name}(${params}) {`);
            genParam();
            a(
               2,
               `this.Client.SendNotification("${definition.name}.${fnc.name}", param);`
            );
            a(1, `}`);
         }
         a(1, ``);
      }
      // a(0, ``);
      // a(0, ``);
      // a(0, ``);
      a(0, `}`);

      this.writeFile(`${definition.name}Client.cs`, lines.join("\n"));
   }

   generateServiceServer(definition: ServiceDefinition) {
      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, `using System;`);
      a(0, `using System.Text.Json;`);
      a(0, `using System.Text.Json.Serialization;`);
      a(0, `using System.Text.Json.Nodes;`);
      a(0, `using System.Threading.Tasks;`);
      a(0, `using System.Collections.Generic;`);
      a(0, ``);
      a(0, `namespace ${this.namespace};`);
      a(0, ``);
      a(
         0,
         `public abstract class ${definition.name}Server<TContext> : JRpcService<TContext> {`
      );

      a(0, ``);
      a(1, `public override string Name {`);
      a(2, `get {`);
      a(3, `return "${definition.name}";`);
      a(2, `}`);
      a(1, `}`);
      a(0, ``);
      a(1, `public ${definition.name}Server() {`);
      for (const fnc of definition.functions) {
         a(2, `this.RegisterFunction("${fnc.name}");`);
      }
      a(1, `}`);
      a(0, ``);
      for (const fnc of definition.functions) {
         let params = [
            ...fnc.inputs.map((inp) => {
               if (inp.array) {
                  return `List<${toCSharpType(inp.type)}> ${inp.name}`;
               } else {
                  return `${toCSharpType(inp.type)} ${inp.name}`;
               }
            }),
            "TContext ctx",
         ].join(", ");

         if (fnc.return) {
            if (fnc.return.type == "void") {
               a(1, `public abstract Task ${fnc.name}(${params});`);
            } else {
               let ret = fnc.return
                  ? fnc.return.array
                     ? `IList<${toCSharpType(fnc.return.type)}>`
                     : toCSharpType(fnc.return.type)
                  : undefined;
               a(1, `public abstract Task<${ret}> ${fnc.name}(${params});`);
            }
         } else {
            a(1, `public abstract void ${fnc.name}(${params});`);
         }
      }
      a(0, ``);
      a(
         1,
         `public async override Task<JsonNode?> HandleRequest(string func, JsonNode param, TContext context) {`
      );
      a(2, `switch(func) {`);
      for (const fnc of definition.functions) {
         a(3, `case "${fnc.name}": {`);
         a(4, `if(param is JsonObject) {`);
         a(
            5,
            `var ja = new JsonArray(${fnc.inputs
               .map((inp) => {
                  return `param["${inp.name}"]`;
               })
               .join(", ")});`
         );
         a(5, `param = ja;`);
         a(4, `}`);

         let pref = "";
         if (fnc.return) {
            if (fnc.return.type != "void") pref = "var result = await ";
            else pref = "await ";
         }

         a(
            4,
            pref +
               `this.${fnc.name}(${[
                  ...fnc.inputs.map((inp, idx) => {
                     let type = inp.array
                        ? `List<${toCSharpType(inp.type)}>`
                        : `${toCSharpType(inp.type)}`;
                     return `param[${idx}]!.Deserialize<${type}>()`;
                  }),
                  "context",
               ].join(", ")});`
         );

         if (fnc.return && fnc.return.type != "void") {
            // if(fnc.return.type == "void") {
            //    a(3, `return null;`);
            // } else {
            //    a(3, ``);
            // }
            a(4, `return JsonSerializer.SerializeToNode(result);`);
            // a(3, ``);
         } else {
            a(4, `return null;`);
         }
         a(3, `}`);
         a(0, ``);
      }
      a(3, `default:`);
      a(4, `throw new Exception("Invalid Method!");`);
      // a(0, ``);
      // a(0, ``);
      // a(0, ``);
      a(2, `}`);
      a(1, `}`);
      a(0, `}`);

      this.writeFile(`${definition.name}Server.cs`, lines.join("\n"));
   }

   generateService(definition: ServiceDefinition): void {
      this.generateServiceClient(definition);
      this.generateServiceServer(definition);
   }

   finalize(steps: Step[]): void {}
}
