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

import { CompileTarget } from "../compile";
import { LineAppender, lineAppender } from "../utils";
import chalk from "chalk";

const conversion = {
   boolean: "bool",
   int: "int",
   float: "double",
   string: "String",
   void: "void",
   // bytes: "Uint8List", //TODO: Check this
};

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

export class DartTarget extends CompileTarget<{ dart_library_name: string }> {
   name: string = "dart";

   start(): void {
      if (this.options.allow_bytes == true) {
         throw new Error("Dart has no support for 'bytes' yet!");
      }

      if (!this.options.dart_library_name) {
         throw new Error(
            "Setting dart_library_name is required for DART target!"
         );
      }
   }

   getImport(name: string) {
      return `import "./${name}.dart";`;
   }

   generateImports(a: lineAppender, def: TypeDefinition | ServiceDefinition) {
      a(0, `import "./base.dart";`);
      def.depends.forEach((dep) => {
         a(0, this.getImport(dep));
      });
   }

   getTypeParse(type: string, value: string) {
      if (conversion[type]) {
         return `${toDartType(type)}_fromJson(${value})`;
      } else {
         return `${toDartType(type)}.fromJson(${value})`;
      }
   }

   generateType(definition: TypeDefinition): void {
      const { a, getResult } = LineAppender(2);

      this.generateImports(a, definition);

      a(0, ``);
      a(0, `class ${definition.name} {`);
      for (const field of definition.fields) {
         if (field.array) {
            a(1, `List<${toDartType(field.type)}>? ${field.name};`);
         } else if (field.map) {
            a(
               1,
               `Map<${toDartType(field.map)},${toDartType(field.type)}>? ${
                  field.name
               };`
            );
         } else {
            a(1, `${toDartType(field.type)}? ${field.name};`);
         }
      }

      a(0, ``);
      a(
         1,
         `${definition.name}({${definition.fields
            .map((e) => `this.${e.name}`)
            .join(", ")}});`
      );
      a(0, ``);
      a(1, `${definition.name}.fromJson(Map<String, dynamic> json) {`);
      for (const field of definition.fields) {
         a(2, `if(json.containsKey("${field.name}")) {`);

         if (field.array) {
            a(3, `this.${field.name} = [];`);
            a(3, `(json["${field.name}"] as List<dynamic>).forEach((e) => {`);
            a(
               4,
               `this.${field.name}!.add(${this.getTypeParse(field.type, "e")})`
            );
            a(3, `});`);
         } else if (field.map) {
            a(3, `this.${field.name} = {};`);
            a(
               3,
               `(json["${field.name}"] as Map<${toDartType(
                  field.map
               )},dynamic>).forEach((key, value) => {`
            );
            a(
               4,
               `this.${field.name}![key] = ${this.getTypeParse(
                  field.type,
                  "value"
               )}`
            );
            a(3, `});`);
         } else {
            a(
               3,
               `this.${field.name} = ${this.getTypeParse(
                  field.type,
                  `json["${field.name}"]`
               )};`
            );
         }
         a(2, `} else {`);
         a(3, `this.${field.name} = null;`);
         a(2, `}`);
         a(0, ``);
      }

      a(1, `}`);

      a(1, `Map<String, dynamic> toJson() {`);
      a(2, `Map<String, dynamic> res = {};`);
      for (const field of definition.fields) {
         if (conversion[field.type]) {
            a(2, `res["${field.name}"] = this.${field.name};`);
         } else {
            if (field.array) {
               a(
                  2,
                  `res["${field.name}"] = this.${field.name}?.map((entry) => entry.toJson()).toList();`
               );
            } else if (field.map) {
               // dict.map((key, value) => MapEntry(key, value.toString()));
               a(
                  2,
                  `res["${field.name}"] = this.${field.name}?.map((key, value) => MapEntry(key, value.toJson()));`
               );
            } else {
               a(2, `res["${field.name}"] = this.${field.name};`);
            }
         }
      }
      a(2, `return res;`);
      a(1, `}`);

      a(0, `}`);

      this.writeFile(`lib/src/${definition.name}.dart`, getResult());
   }

   generateEnum(definition: EnumDefinition): void {
      const { a, getResult } = LineAppender(2);

      a(0, `enum ${definition.name} {`);
      for (const entry of definition.values) {
         const isLast =
            definition.values[definition.values.length - 1] == entry;
         a(1, `${entry.name}(${entry.value})${isLast ? ";" : ","}`);
      }
      a(0, ``);
      a(1, `final int val;`);
      a(1, `const ${definition.name}(int valT) : val= valT;`);
      a(1, `static ${definition.name}? fromJson(int val) {`);
      a(2, `switch(val){`);
      for (const entry of definition.values) {
         a(3, `case ${entry.value}:`);
         a(4, `return ${definition.name}.${entry.name};`);
      }
      a(3, `default:`);
      a(4, `return null;`);
      a(2, `}`);
      a(1, `}`);

      a(0, ``);

      a(1, `int toJson() {`);
      a(2, `return this.val;`);
      a(1, `}`);

      a(0, ``);
      a(0, `}`);

      a(0, ``);

      this.writeFile(`lib/src/${definition.name}.dart`, getResult());
   }

   generateServiceClient(definition: ServiceDefinition): void {
      const { a, getResult } = LineAppender(2);

      this.generateImports(a, definition);
      a(0, `import "./service_client.dart";`);
      a(0, ``);
      a(0, `class ${definition.name}Client extends Service {`);
      a(0, ``);

      a(
         1,
         `${definition.name}Client(ServiceProvider provider):super(provider, "${definition.name}");`
      );

      a(0, ``);

      for (const func of definition.functions) {
         const args = func.inputs
            .map(
               (inp) =>
                  (inp.array
                     ? `List<${toDartType(inp.type)}>`
                     : toDartType(inp.type)) +
                  " " +
                  inp.name
            )
            .join(", ");

         const asParams = func.inputs.map((e) => e.name).join(", ");

         if (!func.return) {
            a(1, `void ${func.name}(${args}) {`);
            a(
               2,
               `provider.sendNotification("${definition.name}.${func.name}", [${asParams}]);`
            );
            a(1, `}`);
         } else {
            const baseReturnType =
               func.return.type != "void"
                  ? toDartType(func.return.type) + "?"
                  : toDartType(func.return.type);
            const returnType = func.return.array
               ? `List<${baseReturnType}>`
               : baseReturnType;

            a(1, `Future<${returnType}> ${func.name}(${args}) async {`);
            a(
               2,
               `var res = await this.provider.sendRequest("${definition.name}.${func.name}", [${asParams}]);`
            );
            if (func.return.type !== "void") {
               if (func.return.array) {
                  a(
                     2,
                     `return res.map((entry) =>${this.getTypeParse(
                        func.return.type,
                        "entry"
                     )}).toList();`
                  );
               } else {
                  a(2, `return ${this.getTypeParse(func.return.type, "res")};`);
               }
            }
            a(1, `}`);
         }
         a(0, ``);
      }
      a(0, `}`);
      a(0, ``);

      this.writeFile(`lib/src/${definition.name}Client.dart`, getResult());
   }

   generateServiceServer(definition: ServiceDefinition): void {
      console.log(
         chalk.yellow("[DART] WARNING:"),
         "DART support for services is not yet there. Service generation is currently limited to clients!"
      );
   }

   generateService(definition: ServiceDefinition): void {
      this.generateServiceClient(definition);
      this.writeFile(
         "lib/src/service_client.dart",
         this.getTemplate("Dart/service_client.dart")
      );
   }

   finalize(steps: Step[]): void {
      const { a, getResult } = LineAppender(2);

      a(0, `library ${this.options.dart_library_name};`);
      a(0, ``);

      let hasService = false;

      steps.forEach(([type, def]) => {
         switch (type) {
            case "type":
               a(0, `export 'src/${def.name}.dart';`);
               break;
            case "enum":
               a(0, `export 'src/${def.name}.dart';`);
               break;
            case "service":
               a(0, `export 'src/${def.name}Client.dart';`);
               hasService = true;
               break;
            default:
               console.warn(
                  chalk.yellow("[DART] WARNING:"),
                  "unimplemented step found:",
                  type
               );
         }
      });

      if (hasService) {
         a(0, `export 'src/service_client.dart';`);
      }

      this.writeFile(`lib/${this.options.dart_library_name}.dart`, getResult());
      this.writeFile(
         `pubspec.yaml`,
         this.getTemplate("Dart/pubspec.yaml").replace(
            "__NAME__",
            this.options.dart_library_name
         )
      );
      this.writeFile(`lib/src/base.dart`, this.getTemplate("Dart/base.dart"));
   }
}
