import dbg from "debug";
import * as FS from "fs";
import Color from "chalk";
import * as Path from "path";
import * as Https from "https";
import * as Http from "http";
import tokenize, { TokenizerError } from "./tokenizer";
import parse, { Parsed, ParserError } from "./parser";
import get_ir, { IR, IRError } from "./ir";
import compile, { CompileTarget } from "./compile";
import {
   ESMTypescriptTarget,
   NodeJSTypescriptTarget,
} from "./targets/typescript";
import { CSharpTarget } from "./targets/csharp";
import { RustTarget } from "./targets/rust";
import { ZIGTarget } from "./targets/zig";
import { DartTarget } from "./targets/dart";
import { URL } from "url";

class CatchedError extends Error { }

const log = dbg("app");

export const Targets = new Map<string, typeof CompileTarget>();

Targets.set("ts-esm", ESMTypescriptTarget);
Targets.set("ts-node", NodeJSTypescriptTarget);
Targets.set("c#", CSharpTarget as typeof CompileTarget);
Targets.set("rust", RustTarget as typeof CompileTarget);
Targets.set("zig", ZIGTarget as typeof CompileTarget);
Targets.set("dart", DartTarget as typeof CompileTarget);

function indexToLineAndCol(src: string, index: number) {
   let line = 1;
   let col = 1;
   for (let i = 0; i < index; i++) {
      if (src.charAt(i) === "\n") {
         line++;
         col = 1;
      } else {
         col++;
      }
   }

   return { line, col };
}

function resolve(base: string, sub?: string) {
   if (sub && (sub.startsWith("http://") || sub.startsWith("https://"))) {
      let u = new URL(sub);
      return u.href;
   } else if (base.startsWith("http://") || base.startsWith("https://")) {
      let u = new URL(base);
      if (sub) {
         if (!sub.endsWith(".jrpc")) {
            sub += ".jrpc";
         }
         u = new URL(sub, u);
      }
      return u.href;
   } else {
      if (!sub) return Path.resolve(base);
      else return Path.resolve(Path.dirname(base), sub + ".jrpc");
   }
}

async function fetchFileFromURL(url: string) {
   return new Promise<string>((yes, no) => {
      const makeReq = url.startsWith("https://") ? Https.request : Http.request;
      const req = makeReq(url, (res) => {
         let chunks: Buffer[] = [];
         res.on("data", (chunk) => {
            chunks.push(Buffer.from(chunk));
         });
         res.on("error", no);
         res.on("end", () => yes(Buffer.concat(chunks).toString("utf-8")));
      });

      req.on("error", no);
      req.end();
   });
}

const fileCache = new Map<string, string>();

async function getFile(name: string) {
   if (fileCache.has(name)) return fileCache.get(name);
   else {
      try {
         if (name.startsWith("http://") || name.startsWith("https://")) {
            const data = await fetchFileFromURL(name);
            fileCache.set(name, data);
            return data;
         } else {
            const data = FS.readFileSync(name, "utf-8");
            fileCache.set(name, data);
            return data;
         }
      } catch (err) {
         log(err);
         await printError(new Error(`Cannot open file ${name};`), null, 0);
      }
   }
   return undefined;
}

async function printError(err: Error, file: string | null, idx: number) {
   let loc = { line: 0, col: 0 };
   if (file != null) {
      const data = getFile(file);
      if (data) loc = indexToLineAndCol(await data, idx);
   }

   console.error(`${Color.red("ERROR: at")} ${file}:${loc.line}:${loc.col}`);
   console.error("   ", err.message);
   log(err.stack);
}

export type Target = {
   type: string;
   output: string;
};

export type CompileOptions = {
   input: string;
   targets: Target[];
   emitDefinitions?: string;
};

type ProcessContext = {
   options: CompileOptions;
   processedFiles: Set<string>;
};

async function processFile(
   ctx: ProcessContext,
   file: string,
   root = false
): Promise<Parsed | null> {
   file = resolve(file);
   if (ctx.processedFiles.has(file)) {
      log("Skipping file %s since it has already been processed", file);
      return null;
   }
   ctx.processedFiles.add(file);
   log("Processing file %s", file);

   const content = await getFile(file);
   if (content == undefined) throw new Error("Could not open file " + file);
   try {
      log("Tokenizing %s", file);
      const tokens = tokenize(content);
      log("Parsing %s", file);
      const parsed = parse(tokens, file);

      log("Resolving imports of %s", file);
      let resolved: Parsed = [];
      for (const statement of parsed) {
         if (statement.type == "import") {
            let res = resolve(file, statement.path);
            resolved.push(...((await processFile(ctx, res)) || []));
         } else {
            resolved.push(statement);
         }
      }

      return resolved.filter((e) => !!e).flat(1) as Parsed;
   } catch (err) {
      if (err instanceof TokenizerError) {
         await printError(err, file, err.index);
         if (!root) throw new CatchedError();
      } else if (err instanceof ParserError) {
         await printError(err, file, err.token.startIdx);
         if (!root) throw new CatchedError();
      } else if (root && err instanceof CatchedError) {
         return null;
      } else {
         throw err;
      }
   }
}

export default async function startCompile(options: CompileOptions) {
   const ctx = {
      options,
      processedFiles: new Set(),
   } as ProcessContext;

   let ir: IR | undefined = undefined;
   if (options.input.endsWith(".json")) {
      ir = JSON.parse(FS.readFileSync(options.input, "utf-8"));
   } else {
      const parsed = await processFile(ctx, options.input, true);

      if (!parsed) process.exit(1); // Errors should have already been emitted
      try {
         ir = get_ir(parsed);
      } catch (err) {
         if (err instanceof IRError) {
            await printError(
               err,
               err.statement.location.file,
               err.statement.location.idx
            );
            process.exit(1);
         } else {
            throw err;
         }
      }
   }

   if (!ir) throw new Error("Error compiling: Cannot get IR");

   if (options.emitDefinitions) {
      FS.writeFileSync(
         options.emitDefinitions,
         JSON.stringify(ir, undefined, 3)
      );
   }

   if (options.targets.length <= 0) {
      console.log(Color.yellow("WARNING:"), "No targets selected!");
   }

   options.targets.forEach((target) => {
      const tg = Targets.get(target.type) as any;
      if (!tg) {
         console.log(Color.red("ERROR:"), "Target not supported!");
         return;
      }
      compile(ir, new tg(target.output, ir.options));
   });
}
