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

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

const conversion = {
   boolean: "bool",
   float: "f64",
   int: "i64",
   string: "String",
   void: "void",
   bytes: "Vec<u8>",
};

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

function toSnake(input: string) {
   return (
      input[0].toLowerCase() +
      input.slice(1).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
   );
}

export class RustTarget extends CompileTarget<{ rust_crate: string }> {
   name: string = "rust";

   get crate() {
      return this.options.rust_crate;
   }

   start(): void {
      if (!this.crate)
         throw new Error(
            "Setting a crate name is required. Add the following to your jrpc file: 'define rust_crate <name>'"
         );

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

      this.writeFile(
         "Cargo.toml",
         this.getTemplate("Rust/Cargo.toml").replace("${NAME}", this.crate)
      );
   }

   private addDependencies(
      a: lineAppender,
      def: TypeDefinition | ServiceDefinition
   ) {
      for (const dep of def.depends) {
         a(0, `use crate::${dep};`);
      }

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

   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()));
      };
      if (definition.fields.find((e) => e.map))
         a(0, `use std::collections::hash_map::HashMap;`);
      a(0, `use serde::{Deserialize, Serialize};`);
      this.addDependencies(a, definition);

      a(0, `#[derive(Clone, Debug, Serialize, Deserialize)]`);
      a(0, `pub struct ${definition.name} {`);
      for (const field of definition.fields) {
         if (field.array) {
            a(1, `pub ${field.name}: Vec<${toRustType(field.type)}>,`);
         } else if (field.map) {
            a(
               1,
               `pub ${field.name}: HashMap<${toRustType(
                  field.map
               )}, ${toRustType(field.type)}>,`
            );
         } else {
            a(1, `pub ${field.name}: ${toRustType(field.type)},`);
         }
      }
      a(0, `}`);

      this.writeFile(`src/${toSnake(definition.name)}.rs`, 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, `use int_enum::IntEnum;`);
      a(0, `use serde::{Deserialize, Serialize};`);

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

      a(0, `#[repr(i64)]`);
      a(
         0,
         "#[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum, Deserialize, Serialize)]"
      );
      a(0, `pub enum ${definition.name} {`);
      for (const field of definition.values) {
         a(1, `${field.name} = ${field.value},`);
      }
      a(0, `}`);

      this.writeFile(`src/${toSnake(definition.name)}.rs`, lines.join("\n"));
   }

   generateService(definition: ServiceDefinition): void {
      // throw new Error("Service not implemented.");
   }

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

      for (const [typ, def] of steps) {
         if (typ == "type" || typ == "enum") {
            a(0, `mod ${toSnake(def.name)};`);
            a(0, `pub use ${toSnake(def.name)}::${def.name};`);
         }
      }

      this.writeFile(`src/lib.rs`, lines.join("\n"));
   }

   finalize(steps: Step[]): void {
      this.generateLib(steps);
      // throw new Error("Method not implemented.");
   }
}
