import fs from "fs";
import path from "path";

export class BuildSnippetsParameters {
  scriptPaths: string[];
  targetFolderPath: string;
}

const ImportTypes = {
  vanilla: [
    "MinecraftDimensionTypes",
    "MinecraftBlockTypes",
    "MinecraftItemTypes",
    "MinecraftEntityTypes",
    "MinecraftEffectTypes",
  ],
  mathutils: ["Vector3Utils"],
  mcui: [
    "MessageFormResponse",
    "MessageFormData",
    "ActionFormData",
    "ActionFormResponse",
    "ModalFormData",
    "ModalFormResponse",
  ],
  mcgt: ["Test", "register"],
  mcsa: ["secrets", "variables"],
  mcnet: ["http", "HttpRequest", "HttpResponse", "HttpRequestMethod", "HttpHeader"],
  mc: [
    "world",
    "system",
    "BlockPermutation",
    "BlockSignComponent",
    "SignSide",
    "DyeColor",
    "EntityQueryOptions",
    "ButtonPushAfterEvent",
    "ItemStack",
    "BlockPistonState",
    "MolangVariableMap",
    "LeverActionAfterEvent",
    "EntityInventoryComponent",
    "BlockInventoryComponent",
    "Enchantment",
    "ItemEnchantsComponent",
    "EntityHealthComponent",
    "EntityOnFireComponent",
    "EntityItemComponent",
    "EntityEquippableComponent",
    "EquipmentSlot",
    "EntityItemComponent",
    "EntitySpawnAfterEvent",
    "PistonActivateBeforeEvent",
    "PistonActivateAfterEvent",
    "MusicOptions",
    "WorldSoundOptions",
    "PlayerSoundOptions",
    "DisplaySlotId",
    "ObjectiveSortOrder",
    "TripWireTripAfterEvent",
    "Vector3",
    "EntityComponentTypes",
    "BlockComponentTypes",
    "ItemComponentTypes",
    "DimensionLocation",
  ],
};

class CodeSnippet {
  name: string;
  description?: string;
  segments: string[];
  function: string;
  codeSampleFull: string;
  codeSampleInterior: string;
}

class SnippetsBuilder {
  _targetFolderPath: string;
  _snippets: CodeSnippet[] = [];
  _snippetsByModules: { [moduleName: string]: { [snippetName: string]: CodeSnippet[] } } = {};
  _scriptLibraryMainCode = "";
  _libraryCode: object = {};

  constructor(targetFolderPath: string) {
    if (!targetFolderPath.endsWith("/") && !targetFolderPath.endsWith("\\")) {
      targetFolderPath += "/";
    }

    this._targetFolderPath = path.join(path.resolve(targetFolderPath));
  }

  processFolder(folderPath: string, depth?: number) {
    if (!fs.existsSync(folderPath)) {
      return;
    }

    console.log("Processing folder '" + folderPath + "'");

    if (!depth) {
      depth = 1;
    }

    const results = fs.readdirSync(folderPath);

    results.forEach((fileName: string) => {
      const filePath = path.join(folderPath, fileName);

      const stat = fs.statSync(filePath);
      if (stat.isDirectory() && depth < 4) {
        this.processFolder(filePath, depth + 1);
      } else if (stat.isFile() && fileName.endsWith(".ts")) {
        const content = fs.readFileSync(filePath, { encoding: "utf8" });
        this.resolve(filePath, content);
      }
    });
  }

  addImports(code: string) {
    code = "\r\n" + code;

    for (const shortHand in ImportTypes) {
      const arr = ImportTypes[shortHand];

      let importStr = "";

      if (arr) {
        for (const str of arr) {
          if (code.indexOf(shortHand + "." + str) >= 0) {
            if (importStr.length > 0) {
              importStr += ", ";
            }

            importStr += str;
          } else if (
            code.indexOf(str + ".") >= 0 ||
            code.indexOf(str + "(") >= 0 ||
            code.indexOf(str + ")") >= 0 ||
            code.indexOf(str + ";") >= 0 ||
            code.indexOf(str + "\r") >= 0 ||
            code.indexOf(str + "\n") >= 0 ||
            code.indexOf(str + " ") >= 0
          ) {
            if (importStr.length > 0) {
              importStr += ", ";
            }

            importStr += str;
          }
        }
      }

      if (importStr.length > 0) {
        switch (shortHand) {
          case "mc":
            code = "import { " + importStr + ' } from "@minecraft/server";\r\n' + code;
            break;
          case "mcui":
            code = "import { " + importStr + ' } from "@minecraft/server-ui";\r\n' + code;
            break;
          case "vanilla":
            code = "import { " + importStr + ' } from "@minecraft/vanilla-data";\r\n' + code;
            break;
          case "mathutils":
            code = "import { " + importStr + ' } from "@minecraft/math";\r\n' + code;
            break;
          case "mcgt":
            code = "import { " + importStr + ' } from "@minecraft/server-gametest";\r\n' + code;
            break;
          case "mcnet":
            code = "import { " + importStr + ' } from "@minecraft/server-net";\r\n' + code;
            break;
          case "mcsa":
            code = "import { " + importStr + ' } from "@minecraft/server-admin";\r\n' + code;
            break;
        }
      }
    }

    return code;
  }

  removeNamespaceAliases(code: string) {
    code = code.replace(/mcui\./gi, "");
    code = code.replace(/mcnet\./gi, "");
    code = code.replace(/mcadmin\./gi, "");
    code = code.replace(/mcgt\./gi, "");
    code = code.replace(/mcgt\r\n/gi, "");
    code = code.replace(/mcgt\n/gi, "");
    code = code.replace(/  .register/gi, "register");
    code = code.replace(/mc\./gi, "");
    code = code.replace(/vanilla\./gi, "");
    code = code.replace(/mathutils\./gi, "");

    return code;
  }

  resolve(filePath: string, content: string) {
    let filePathLower = filePath.toLowerCase();

    if (filePathLower.indexOf("samplemanager.") >= 0) {
      this._scriptLibraryMainCode = this.stripLinesContaining(content, "import * as mc from ");
      this._scriptLibraryMainCode = this.stripLinesContaining(this._scriptLibraryMainCode, "console.");
    } else if (filePathLower.endsWith("samplelibrary.ts")) {
      let lastSlash = Math.max(filePathLower.lastIndexOf("/"), filePathLower.lastIndexOf("\\"));

      if (lastSlash >= 0) {
        let libraryContent = content.replace(/sdf\d{1,4}\./g, "");
        libraryContent = libraryContent.replace(/sm\./g, "");
        libraryContent = this.stripLinesContaining(libraryContent, "import * as sdf");
        libraryContent = this.stripLinesContaining(libraryContent, "import * as mc ");
        libraryContent = this.stripLinesContaining(libraryContent, "import SampleManager from ");

        let moduleName = filePathLower.substring(lastSlash + 1, filePathLower.length - 16);

        this._libraryCode["@minecraft/" + moduleName] = libraryContent;
      }
    }

    let seeLinkStart = content.indexOf("@see ");

    while (seeLinkStart >= 0) {
      let endOfSeeLinkLine = content.indexOf("\n", seeLinkStart);

      if (endOfSeeLinkLine > seeLinkStart + 6) {
        let seeLinkContents = content
          .substring(seeLinkStart + 5, endOfSeeLinkLine)
          .trim()
          .toLowerCase();

        if (seeLinkContents.startsWith("https://learn.microsoft.com/minecraft/creator/scriptapi/")) {
          let description: string | undefined;

          let thisCommentAreaStart = content.lastIndexOf("/**", seeLinkStart);

          if (thisCommentAreaStart >= 0) {
            let firstAsterisk = content.indexOf("* ", thisCommentAreaStart + 5);

            if (firstAsterisk > thisCommentAreaStart) {
              let endOfFirstCommentLine = content.indexOf("\n", firstAsterisk);

              if (endOfFirstCommentLine > firstAsterisk) {
                description = content.substring(firstAsterisk + 2, endOfFirstCommentLine).trim();
              }
            }
          }

          let urlLink = seeLinkContents.substring(56, seeLinkContents.length).replace("#", "/");

          let urlSegments = urlLink.split("/");

          if (urlSegments.length >= 2 && urlSegments[0].startsWith("minecraft")) {
            const nextExport = content.indexOf("export ", endOfSeeLinkLine);

            if (nextExport >= 0) {
              const nextFunction = content.indexOf(" function", nextExport);

              if (nextFunction >= 0) {
                const firstParen = content.indexOf("(", nextFunction);
                let endOfFunction = content.indexOf("\r\n}", nextFunction);
                const gtStart = content.indexOf(".register", nextFunction);

                if (gtStart >= 0) {
                  const gtEnd = content.indexOf(");", gtStart);

                  if (gtEnd > firstParen && firstParen > nextFunction) {
                    const name = content.substring(nextFunction + 10, firstParen);
                    const functionCode = content.substring(nextExport, endOfFunction + 3);
                    let codeSampleFull = content.substring(nextFunction + 1, gtEnd + 3);

                    codeSampleFull = this.addImports(codeSampleFull);
                    codeSampleFull = this.removeNamespaceAliases(codeSampleFull);

                    let cs = new CodeSnippet();
                    cs.name = name;
                    cs.description = description;
                    cs.segments = urlSegments;
                    cs.function = functionCode;
                    cs.codeSampleFull = codeSampleFull;
                    cs.codeSampleInterior = codeSampleFull;

                    this._snippets.push(cs);

                    let moduleKey = urlSegments[0] + "/" + urlSegments[1];

                    if (!this._snippetsByModules[moduleKey]) {
                      this._snippetsByModules[moduleKey] = {};
                    }

                    if (!this._snippetsByModules[moduleKey][name]) {
                      this._snippetsByModules[moduleKey][name] = [];
                    }

                    this._snippetsByModules[moduleKey][name].push(cs);

                    console.log(
                      "Gametest snippet '" + name + "' discovered for " + urlSegments.join(".") + " in " + moduleKey
                    );

                    endOfFunction = -1;
                  }
                }

                if (endOfFunction > firstParen && firstParen > nextFunction) {
                  const name = content.substring(nextFunction + 10, firstParen);
                  const functionCode = content.substring(nextExport, endOfFunction + 3);

                  const firstBrace = functionCode.indexOf("{");

                  if (firstBrace >= 0) {
                    let codeSampleInterior = functionCode.substring(firstBrace + 3, functionCode.length - 3);

                    let codeSampleFull = "";

                    if (codeSampleInterior.indexOf("log(") > 0) {
                      codeSampleFull = content.substring(nextFunction + 1, endOfFunction + 3);
                    } else {
                      codeSampleFull = this.stripLinesContaining(
                        content.substring(nextFunction + 1, endOfFunction + 3),
                        "log("
                      )
                        .replace("log: (message: string, status?: number) => void, ", "")
                        .replace("log: (message: string, status?: number) => void,\r\n", "");
                    }

                    codeSampleFull = this.addImports(codeSampleFull);
                    codeSampleFull = this.removeNamespaceAliases(codeSampleFull);
                    codeSampleInterior = this.removeNamespaceAliases(codeSampleInterior);

                    let cs = new CodeSnippet();
                    cs.name = name;
                    cs.description = description;
                    cs.segments = urlSegments;
                    cs.function = functionCode;
                    cs.codeSampleFull = codeSampleFull;
                    cs.codeSampleInterior = codeSampleInterior;

                    this._snippets.push(cs);

                    let moduleKey = urlSegments[0] + "/" + urlSegments[1];

                    if (!this._snippetsByModules[moduleKey]) {
                      this._snippetsByModules[moduleKey] = {};
                    }

                    if (!this._snippetsByModules[moduleKey][name]) {
                      this._snippetsByModules[moduleKey][name] = [];
                    }

                    this._snippetsByModules[moduleKey][name].push(cs);

                    console.log("Snippet '" + name + "' discovered for " + urlSegments.join(".") + " in " + moduleKey);
                  }
                }
              }
            }
          }
        }

        seeLinkStart = content.indexOf("@see ", seeLinkStart + 5);
      }
    }
  }

  writeSnippetFiles() {
    for (let moduleKey in this._snippetsByModules) {
      let snippetsByName = this._snippetsByModules[moduleKey];

      const snippetNamesByPath: { [path: string]: string[] } = {};

      for (let snippetKey in snippetsByName) {
        const snippets = snippetsByName[snippetKey];

        if (snippets.length <= 1) {
          let coreSnippet = snippets[0];

          let coreUrlSegments = Array.from(coreSnippet.segments);

          if (coreUrlSegments[0] === coreUrlSegments[1]) {
            coreUrlSegments.shift();
          }
          this.writeFile(
            "docsnips/" + coreUrlSegments.join("/") + "/_examples/" + coreSnippet.name + ".ts",
            coreSnippet.codeSampleFull
          );
        } else {
          for (const snippet of snippets) {
            let snipUrlSegments = Array.from(snippet.segments);

            if (snipUrlSegments[0] === snipUrlSegments[1]) {
              snipUrlSegments.shift();
            }

            const path = snipUrlSegments.join("/");

            if (snippetNamesByPath[path] === undefined) {
              snippetNamesByPath[path] = [];
            }

            if (!snippetNamesByPath[path].includes(snippet.name + ".ts")) {
              snippetNamesByPath[path].push(snippet.name + ".ts");
            }

            if (snipUrlSegments.length >= 2) {
              this.writeFile(
                "docsnips/" +
                  snipUrlSegments[0] +
                  "/" +
                  snipUrlSegments[1] +
                  "/_shared_examples/" +
                  snippet.name +
                  ".ts",
                snippet.codeSampleFull
              );
            } else {
              this.writeFile(
                "docsnips/" + snipUrlSegments[0] + "/_shared_examples/" + snippet.name + ".ts",
                snippet.codeSampleFull
              );
            }
          }
        }
      }

      for (const sharedSnippetPath in snippetNamesByPath) {
        const snippetList = snippetNamesByPath[sharedSnippetPath];
        this.writeFile(
          "docsnips/" + sharedSnippetPath + "/_example_files.json",
          JSON.stringify(snippetList, undefined, 2)
        );
      }
    }
  }

  writeCatalogFiles() {
    for (let moduleKey in this._snippetsByModules) {
      let moduleType: string[] = [];
      let samplesWritten: { [snippetName: string]: CodeSnippet } = {};
      let snippetsByName = this._snippetsByModules[moduleKey];

      for (let snippetKey in snippetsByName) {
        const snippets = snippetsByName[snippetKey];
        for (let i = 0; i < snippets.length; i++) {
          let snippet = snippets[i];

          if (!samplesWritten[snippet.name]) {
            samplesWritten[snippet.name] = snippet;
            moduleType.push(snippets[i].function);
          }
        }
      }

      let jsonMarkup = "{" + "\r\n";

      for (let snippetKey in samplesWritten) {
        let snippet = samplesWritten[snippetKey];

        if (jsonMarkup.length > 10) {
          jsonMarkup += ",\r\n";
        }

        let sample = '["' + this.getQuoteSafeContent(snippet.codeSampleInterior).replace(/\r\n/g, '",\r\n"') + '"\r\n]';

        jsonMarkup +=
          '"' +
          snippet.name +
          '": {\r\n  "description": "' +
          (snippet.description ? snippet.description + " " : "") +
          "See https://learn.microsoft.com/minecraft/creator/scriptapi/" +
          snippet.segments.join("/") +
          '",\r\n  "prefix": ["mc"],\r\n  "body": ' +
          sample +
          "}";
      }

      jsonMarkup += "\r\n}";

      this.writeFile("samplejson/" + moduleKey + "-beta.json", jsonMarkup);

      let tsTestFileMarkup =
        "/* eslint-disable  @typescript-eslint/no-unused-vars */\r\n" +
        'import * as mc from "@minecraft/server";\r\n\r\n' +
        moduleType.join("\r\n\r\n");

      tsTestFileMarkup += "\r\n" + this._scriptLibraryMainCode + "\r\n";

      if (this._libraryCode[moduleKey]) {
        tsTestFileMarkup += "\r\n" + this._libraryCode[moduleKey] + "\r\n";
      }

      this.writeFile("typescript/" + moduleKey + "/tests.ts", tsTestFileMarkup);
    }
  }

  getQuoteSafeContent(content: string) {
    var newContent = "";

    for (const chr of content) {
      if (chr === "\\") {
        newContent += "/";
      } else if (chr === '"') {
        newContent += "'";
      } else {
        newContent += chr;
      }
    }

    // eslint-disable-next-line no-control-regex
    newContent = newContent.replace(/[\r\n\x0B\x0C\u0085\u2028\u2029]+/g, '",\n"');
    return newContent;
  }

  stripLinesContaining(content: string, containing: string) {
    let i = content.indexOf(containing);

    while (i >= 0) {
      let previousNewLine = content.lastIndexOf("\n", i);

      let nextNewLine = content.indexOf("\n", i);

      if (previousNewLine < 0) {
        previousNewLine = 0;
      }

      if (previousNewLine > 0 && content[previousNewLine - 1] === "\r") {
        previousNewLine--;
      }

      if (nextNewLine < 0) {
        nextNewLine = content.length - 1;
      }

      content = content.substring(0, previousNewLine) + content.substring(nextNewLine + 1, content.length);
      i = content.indexOf(containing, previousNewLine);
    }

    return content;
  }

  writeFile(relativePath: string, contents: string) {
    const fullPath = path.join(this._targetFolderPath, relativePath);
    const dirName = path.dirname(fullPath);

    if (!fs.existsSync(dirName)) {
      fs.mkdirSync(dirName, { recursive: true });
    }

    fs.writeFileSync(fullPath, contents);
  }
}

export function buildSnippets(params: BuildSnippetsParameters) {
  return () => {
    const snippetsBuilder = new SnippetsBuilder(params.targetFolderPath);

    for (const scriptPath of params.scriptPaths) {
      console.log("Processing " + scriptPath);
      snippetsBuilder.processFolder(scriptPath);
    }

    snippetsBuilder.writeSnippetFiles();
    snippetsBuilder.writeCatalogFiles();
  };
}
