const net = require("net");
const fs = require("fs");
const process = require("process");
const camelCase = require("camelcase");

const SOCKETFILE =
  process.env.SIGNALD_SOCKET || "/run/user/1000/signald/signald.sock";
const GENERATEDFILE = "./src/generated.ts";
const VERSION = "v1";
let globalVersion;

const toJs = (name) => name.replace("-", "_");

const TYPES = {
  String: "string",
  long: "number",
  int: "number",
  Integer: "number",
  Map: "Record<string, string>",
  boolean: "boolean",
  Boolean: "boolean",
  Long: "number",
  UUID: "string",
};

const convertType = (field) => {
  let name = "";
  if (field.version) {
    name = `${field.type}${field.version}`;
  } else {
    if (!TYPES[field.type]) {
      throw new Error(
        `unknown type ${field.type} inside ${JSON.stringify(field)}`
      );
    }
    name = TYPES[field.type];
  }

  if (field.list) {
    return `${name}[]`;
  }
  return name;
};
const fieldToData = ([rawName, field]) => {
  const name = toJs(rawName);
  const type = convertType(field);
  const requiredStr = field.required ? "" : "?";
  const example = field.example ? `\n      Example: ${field.example}\n` : "\n";
  const doc = field.doc ? `\n/**\n    ${field.doc}.${example}  */\n` : "";
  let code = `${doc}${name}${requiredStr}: ${type};`;
  return {
    name,
    rawName,
    type,
    code,
    doc: field.doc,
    example: field.example,
    required: field.required,
  };
};

const typeToData = (name, version, data) => {
  const typeName = `${name}${version}`;
  const doc = data.doc
    ? `\n/**
  ${data.doc}
 */\n`
    : "\n";

  const header = `export type ${typeName} = {
`;
  const dataFields = Object.entries(data.fields).map(fieldToData);
  const codeFields = dataFields.map((field) => field.code);
  const body = codeFields.join("\n");
  const footer = "\n}\n";
  // ensure required fields are sorted before non-required fields
  dataFields.sort((a, b) =>
    a.required === b.required ? 0 : a.required ? -1 : 1
  );
  return {
    name,
    version,
    typeName,
    fields: dataFields,
    code: doc + header + body + footer,
  };
};

const actionToCode = (structures, rawName, version, data) => {
  const name = camelCase(rawName);
  const { request, response } = data;
  const requestFields = structures[request].fields;
  const returnTypeStr = response
    ? `Promise<${response}${version}>`
    : "Promise<void>";
  let code = `  async ${name}(`;
  requestFields.map((field) => {
    const requiredStr = field.required ? "" : "?";
    code += `${field.name}${requiredStr}: ${field.type},\n`;
  });

  code += `): ${returnTypeStr} {\n`;
  const params = requestFields
    .map((field) => {
      return `"${field.rawName}": ${field.name}`;
    })
    .join(",\n");
  const requestStr = `{"type": "${rawName}","version": "${version}", ${params}}`;
  const responseStr = `this.getResponse(${requestStr})`;
  if (response) {
    code += `return ${responseStr} as Promise<${response}${version}>`;
  } else {
    code += `await ${responseStr}; return;`;
  }
  code += "\n}\n";
  return code;
};

const saveFile = (code) => {
  fs.writeFileSync(GENERATEDFILE, code);
  console.log("generated code at ", GENERATEDFILE);
};

const processProtocolDefinition = (client, def, version) => {
  console.log("processing protocol definition");
  const when = new Date(new Date().toUTCString());
  let contents = `
/* Generated by the output of the 'protocol' command of signald
   Date: ${when}
   Version: ${globalVersion}
 */
import {JSONTransport} from "./util";
`;

  const structures = {};
  // generate type definitions
  for (version in def.types) {
    Object.entries(def.types[version]).map(([name, data]) => {
      structures[name] = typeToData(name, version, data);
      contents += structures[name].code;
    });
  }

  // TODO this should be improved once v0 is totally deprecated
  const typeNames = ["version", "message", "subscribe", "unsubscribe"]
    .map((name) => `"${name}"`)
    .join(" | ");
  contents += `\ndeclare module "./util" {
  interface EventTypes {
    /**
     * The v0 event emitted when a signal message is received
     * @event
     */
    messagev0(message: JsonMessageEnvelopev1): void;
    /**
     * @event
     */
    subscribev0(): void;
    /**
     * @event
     */
    unsubscribev0(): void;
  }
}\n`;

  // generate action functions
  for (version in def.actions) {
    if (version != VERSION) continue;
    contents += "export class SignaldGeneratedApi extends JSONTransport{\n";
    Object.entries(def.actions[version]).map(([name, data]) => {
      contents += actionToCode(structures, name, version, data);
    });
    contents += "\n}\n";
  }

  saveFile(contents);
  cleanup();
};

// := Driver

const getProtocolDefinition = (client) => {
  const msg = {
    type: "protocol",
  };
  console.log("requesting protocol definition");
  client.write(JSON.stringify(msg) + "\n");
};
const client = net.createConnection(SOCKETFILE);
let buffer = "";
client.setEncoding("utf8");

function cleanup() {
  client.end();
  process.exit(0);
}
client.on("connect", () => {
  console.log("connected");
  buffer = "";
});

client.on("data", (payload) => {
  buffer += payload;
  if (!buffer.endsWith("\n")) {
    return;
  } else {
    const data = buffer;
    buffer = "";
    data.split("\n").forEach((line) => {
      if (!line) return;

      const msg = JSON.parse(line);
      if (msg.type === "version") {
        getProtocolDefinition(client);
        globalVersion = msg.data.version;
      }
      if (msg.type === "protocol") {
        // console.log(JSON.stringify(msg.data, null, 2));
        processProtocolDefinition(client, msg.data, globalVersion);
      }
    });
  }
});

client.on("error", (err) => {
  console.error(err);
});

process.on("SIGINT", cleanup);
