import chalk from "chalk";
import { CompileTarget } from "../compile";
import { TypeDefinition, EnumDefinition, ServiceDefinition, Step, IR } from "../ir";
import { lineAppender, LineAppender } from "../utils";

const conversion = {
   boolean: "bool",
   float: "f64",
   int: "i64",
   string: "String",
   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()}`)
   );
}

// TODO: Add other keywords as well!
const keywords = new Set(["type", "static"]);

const fixKeywordName = (name: string) => {
   if (keywords.has(name)) {
      return `${name}_`;
   }
   return name;
}

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)
      );

      this.writeFile("src/base_lib.rs", this.getTemplate("Rust/src/lib.rs"));
   }

   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 {
      const { a, getResult } = LineAppender();

      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) {
         a(1, `#[allow(non_snake_case)]`);

         let fn = `pub ${field.name}:`;
         if (keywords.has(field.name)) {
            // TODO: Add other keywords as well!
            console.log(
               chalk.yellow("[RUST] WARNING:"),
               `Field name '${field.name}' is not allowed in Rust. Renaming to '${field.name}_'`
            );
            fn = `pub ${fixKeywordName(field.name)}:`;
            a(1, `#[serde(rename = "${field.name}")]`);
         }
         let opts = "";
         let opte = "";
         if (field.optional) {
            opts = "Option<";
            opte = ">";
         }

         if (field.array) {
            a(1, `${fn} ${opts}Vec<${toRustType(field.type)}>${opte},`);
         } else if (field.map) {
            a(
               1,
               `${fn} ${opts}HashMap<${toRustType(
                  field.map
               )}, ${toRustType(field.type)}>${opte},`
            );
         } else {
            a(1, `${fn} ${opts}${toRustType(field.type)}${opte},`);
         }
      }
      a(0, `}`);

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

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

      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)]"
      );
      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`, getResult());
   }

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

      const typeToRust = (type: string, array: boolean) => {
         let rt = toRustType(type);
         return array ? `Vec<${rt}>` : rt;
      };

      this.addDependencies(a, definition);

      a(0, `use crate::base_lib::{JRPCClient, JRPCRequest, Result};`);
      a(0, `use serde_json::{json};`);
      a(0, ``);
      a(0, `pub struct ${definition.name}{`);
      a(1, `client: JRPCClient,`);
      a(0, `}`);
      a(0, ``);

      a(0, `impl ${definition.name} {`);
      a(1, `pub fn new(client: JRPCClient) -> Self {`);
      a(2, `return Self { client };`);
      a(1, `}`);
      a(0, ``);
      for (const fnc of definition.functions) {
         let params = fnc.inputs
            .map((i) => fixKeywordName(i.name) + ": " + typeToRust(i.type, i.array))
            .join(", ");
         let ret = fnc.return
            ? typeToRust(fnc.return.type, fnc.return.array)
            : "()";
         a(1, `#[allow(non_snake_case)]`);
         a(1, `pub async fn ${fnc.name}(&self, ${params}) -> Result<${ret}> {`);
         a(2, `let l_req = JRPCRequest {`);
         a(3, `jsonrpc: "2.0".to_owned(),`);
         a(3, `id: None, // 'id' will be set by the send_request function`);
         a(3, `method: "${definition.name}.${fnc.name}".to_owned(),`);
         a(3, `params: json!([${fnc.inputs.map((e) => fixKeywordName(e.name))}])`);
         a(2, `};`);
         a(2, ``);
         if (fnc.return) {
            a(2, `let l_res = self.client.send_request(l_req).await;`);
            a(2, `match l_res {`);
            a(3, `Err(e) => return Err(e),`);
            if (fnc.return.type == "void") {
               a(3, `Ok(_) => {`);
               a(4, `return Ok(());`);
               a(3, `}`);
            } else {
               a(3, `Ok(o) => {`);
               a(
                  4,
                  `return serde_json::from_value(o).map_err(|e| Box::from(e));`
               );
               a(3, `}`);
            }
            a(2, `}`);

         } else {
            a(2, `self.client.send_notification(l_req).await;`);
            a(2, `return Ok(());`);
         }
         a(1, `}`);
      }
      a(0, `}`);
      a(0, ``);

      this.writeFile(`src/client/${toSnake(definition.name)}.rs`, getResult());
   }

   private generateServiceServer(definition: ServiceDefinition): void {
      const { a, getResult } = LineAppender();

      this.addDependencies(a, definition);
      a(0, `use crate::base_lib::{JRPCServerService, JRPCRequest, Result};`);
      a(0, `use serde_json::{Value};`);
      a(0, `use std::sync::Arc;`);
      a(0, `use async_trait::async_trait;`);

      const typeToRust = (type: string, array: boolean) => {
         let rt = toRustType(type);
         return array ? `Vec<${rt}>` : rt;
      };

      a(0, `#[async_trait]`);
      a(0, `pub trait ${definition.name} {`);
      for (const fnc of definition.functions) {
         let params =
            fnc.inputs.length > 0
               ? fnc.inputs
                  .map((i) => fixKeywordName(i.name) + ": " + typeToRust(i.type, i.array))
                  .join(", ")
               : "";
         let ret = fnc.return
            ? typeToRust(fnc.return.type, fnc.return.array)
            : "()";
         a(1, `#[allow(non_snake_case)]`);
         a(1, `async fn ${fnc.name}(&self, ${params}) -> Result<${ret}>;`);
      }
      a(0, `}`);

      a(0, ``);
      a(0, `pub struct ${definition.name}Handler {`);
      a(1, `implementation: Box<dyn ${definition.name} + Sync + Send + 'static>,`);
      a(0, `}`);
      a(0, ``);
      a(0, `impl ${definition.name}Handler {`);
      //TODO: Maybe add a new definition like, pub fn new2<T>(implementation: T) where T: ${definition.name} + Sync + Send + 'static {}
      a(
         1,
         `pub fn new(implementation: Box<dyn ${definition.name} + Sync + Send + 'static>) -> Arc<Self> {`
      );
      a(2, `return Arc::from(Self { implementation });`);
      a(1, `}`);
      a(0, `}`);

      a(0, ``);

      a(0, `#[async_trait]`);
      a(
         0,
         `impl JRPCServerService for ${definition.name}Handler {`
      );

      a(1, `fn get_id(&self) -> String { "${definition.name}".to_owned() }`);
      a(0, ``);

      a(1, `#[allow(non_snake_case)]`);
      a(
         1,
         `async fn handle(&self, msg: &JRPCRequest, function: &str) -> Result<(bool, Value)> {`
      );
      a(2, `match function {`);
      for (const fnc of definition.functions) {
         a(3, `"${fnc.name}" => {`);
         a(4, `if msg.params.is_array() {`);
         if (fnc.inputs.length > 0) {
            a(
               5,
               `let arr = msg.params.as_array().unwrap(); //TODO: Check if this can fail.`
            );
         }
         a(5, `let res = self.implementation.${fnc.name}(`);
         for (let i = 0; i < fnc.inputs.length; i++) {
            const inp = fnc.inputs[i];
            a(6, `serde_json::from_value(arr[${i}].clone())`);
            a(
               7,
               `.map_err(|_| "Parameter for field '${inp.name}' should be of type '${inp.type}'!")?${i == fnc.inputs.length - 1 ? "" : ","}` //TODO: Array
            );
         }
         a(5, `).await?;`);
         if (fnc.return) {
            a(5, `return Ok((true, serde_json::to_value(res)?));`);
         } else {
            a(5, `_ = res;`);
            a(5, `return Ok((false, Value::Null))`);
         }
         a(4, `} else if msg.params.is_object() {`);
         a(5, `return Err(Box::from("Not implemented yet".to_owned()));`);
         a(4, `} else {`);
         a(5, `return Err(Box::from("Invalid parameters??".to_owned()));`);
         a(4, `}`);
         a(3, `},`);
      }
      a(
         3,
         `_ => { return Err(Box::from(format!("Invalid function {}", function).to_owned())) ;},`
      );
      a(2, `}`);
      a(1, `}`);
      a(0, `}`);
      this.writeFile(`src/server/${toSnake(definition.name)}.rs`, getResult());
   }

   generateService(definition: ServiceDefinition): void {
      console.log(
         chalk.yellow("[RUST] WARNING:"),
         "Rust support for services is WIP. Use with care!"
      );
      this.generateServiceServer(definition);
      this.generateServiceClient(definition);
   }

   private generateLib(steps: Step[]) {
      const { a, getResult } = LineAppender();

      const lc = LineAppender();
      const ls = LineAppender();

      const ac = lc.a;
      const as = ls.a;

      a(0, `pub mod base_lib;`);
      a(0, `pub use base_lib::{JRPCServer, JRPCClient, Result};`);

      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};`);
         } else if (typ == "service") {
            as(0, `mod ${toSnake(def.name)};`);
            as(
               0,
               `pub use ${toSnake(def.name)}::{${def.name}, ${def.name
               }Handler};`
            );

            ac(0, `mod ${toSnake(def.name)};`);
            ac(0, `pub use ${toSnake(def.name)}::${def.name};`);
         }
      }

      a(0, `pub mod server;`);
      a(0, `pub mod client;`);

      this.writeFile(`src/lib.rs`, getResult());
      this.writeFile(`src/server/mod.rs`, ls.getResult());
      this.writeFile(`src/client/mod.rs`, lc.getResult());
   }

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