import { genVscodeExt } from "./build-l10n.mjs";

import { spawn } from "child_process";
import * as fs from "fs/promises";
import * as path from "path";
import copyDirSync from "cpr";

import {
  generate as generateTextmate,
  install as installTextmate,
} from "../syntaxes/textmate/main.ts";

/// The default working directory is the root of the project
const cwd = path.resolve(import.meta.dirname, "..");
const vscodeDir = path.resolve(cwd, "editors/vscode");
const previewDir = path.resolve(cwd, "tools/typst-preview-frontend");
const editorToolsDir = path.resolve(cwd, "tools/editor-tools");

/**
 * Spawns a command and return a promise that resolves to the code. The id is used to identify the command in the output and prepended to each line.
 * The line will be buffered and printed to avoid line crossing.
 */
export function spawnAsync(id, cmd, options = { cwd }) {
  return new Promise((resolve, reject) => {
    options.shell = true;
    options.stdio = "pipe";
    const child = spawn(cmd, options);

    const linePrinter = (stream, outStream) => {
      let out = "";
      stream.on("data", (data) => {
        out += data;
        const lines = out.split("\n");
        while (lines.length > 1) {
          const line = lines.shift();
          outStream.write(`[${id}] ${line}\n`);
        }
        out = lines.join("\n");
      });
      stream.on("end", () => {
        if (out) {
          outStream.write(`[${id}] ${out}\n`);
        }
      });
    };

    linePrinter(child.stdout, process.stdout);
    linePrinter(child.stderr, process.stderr);

    child.on("close", (code) => {
      if (code !== 0) {
        reject(new Error(`Command ${cmd} failed with code ${code}`));
      }
      resolve(code);
    });
  });
}

const _copyDirOpts = { deleteFirst: true, overwrite: true, confirm: true };
const copyDir = (fr, to) =>
  new Promise((resolve, reject) =>
    copyDirSync(fr, to, _copyDirOpts, (err) => (err ? reject(err) : resolve())),
  );

export async function extractL10nTs() {
  await spawnAsync(
    "extract:l10n:ts",
    "cargo run --release --bin tinymist-l10n -- --kind ts --dir ./editors/vscode --output ./locales/tinymist-vscode-rt.toml",
  );
}

export async function extractL10nRs() {
  await spawnAsync(
    "extract:l10n:rs",
    "cargo run --release --bin tinymist-l10n -- --kind rs --dir ./crates --output ./locales/tinymist-rt.toml",
  );
  await fs.copyFile(
    path.resolve(cwd, "locales/tinymist-rt.toml"),
    path.resolve(cwd, "crates/tinymist-assets/src/tinymist-rt.toml"),
  );
}

export async function buildL10n() {
  await Promise.all([extractL10nTs(), extractL10nRs()]);
  await genVscodeExt();
}

export async function buildIndexWasm() {
  await spawnAsync(
    "build:embedded:typst",
    "cargo build -p tinymist-index --release --target wasm32-unknown-unknown",
  );
  await fs.copyFile(
    path.resolve(cwd, "target/wasm32-unknown-unknown/release/tinymist_index.wasm"),
    path.resolve(cwd, "typ/packages/tinymist-index/tinymist_index.wasm"),
  );
}

export async function buildSyntax() {
  await generateTextmate();
  await installTextmate();
}

export async function buildPreview() {
  const run = (id, cmd) => spawnAsync(`build:preview:${id}`, cmd, { cwd: previewDir });

  await Promise.all([run("tsc", "npx tsc"), run("vite", "npx vite build")]);

  await fs.copyFile(
    path.resolve(previewDir, "dist/index.html"),
    path.resolve(cwd, "crates/tinymist-assets/src/typst-preview.html"),
  );
}

export async function buildEditorTools() {
  const run = (id, cmd) => spawnAsync(`build:editor-tools:${id}`, cmd, { cwd: editorToolsDir });

  await run("tsc", "npx tsc");
  await run("vite", "npx vite build -- --component=symbol-view");
  await run("vite", "npx vite build");

  await installEditorTools();
}

export async function installEditorTools() {
  await copyDir(path.join(editorToolsDir, "dist"), path.join(vscodeDir, "out/editor-tools/"));
}

export async function checkVersion() {
  const cargoToml = await fs.readFile(path.resolve(cwd, "Cargo.toml"), "utf8");
  const cargoVersion = cargoToml.match(/version = "(.*?)"/)[1];
  const pkgVersion = JSON.parse(
    await fs.readFile(path.resolve(vscodeDir, "package.json"), "utf8"),
  ).version;

  if (cargoVersion !== pkgVersion) {
    throw new Error(
      `Version mismatch: ${cargoVersion} (in Cargo.toml) !== ${pkgVersion} (in package.json)`,
    );
  }

  return { cargoVersion, pkgVersion };
}

export async function buildTinymistVscodeWebBase() {
  await spawnAsync("vscode:web", "cd editors/vscode && node esbuild.web.mjs");
}

export async function buildTinymistVscodeWeb() {
  await Promise.all([
    checkVersion(),
    buildSyntax(),
    buildL10n(),
    buildEditorTools(),
    buildWebLspBinary().then(buildTinymistVscodeWebBase),
  ]);
}

export async function testTinymistVscodeBase() {
  await spawnAsync("vscode:test", "cd editors/vscode && yarn test:vsc");
}

export async function testTinymistVscode() {
  await Promise.all([checkVersion(), buildLspBinary("release").then(testTinymistVscodeBase)]);
}

export async function buildTinymistVscodeSystemBase() {
  await spawnAsync("vscode:system", "cd editors/vscode && node esbuild.system.mjs");
}

export async function buildTinymistVscodeSystem() {
  await Promise.all([
    checkVersion(),
    buildSyntax(),
    buildL10n(),
    buildEditorTools(),
    buildTinymistVscodeSystemBase(),
  ]);
}

export async function buildLspBinary(kind) {
  await spawnAsync(
    `lsp:${kind}`,
    `cargo build -p tinymist-cli --color=always --profile=${kind === "debug" ? "dev" : "release"}`,
    {
      env: {
        ...process.env,
        FORCE_COLOR: "1",
      },
    },
  );

  const binName = process.platform === "win32" ? "tinymist.exe" : "tinymist";

  await Promise.all([
    fs.copyFile(
      path.resolve(cwd, `target/${kind}/${binName}`),
      path.resolve(vscodeDir, `out/${binName}`),
    ),
    process.platform === "win32" && kind === "debug"
      ? [
          fs.copyFile(
            path.resolve(cwd, `target/debug/tinymist.pdb`),
            path.resolve(vscodeDir, `out/tinymist.pdb`),
          ),
        ]
      : [],
  ]);
}

export async function prelaunchVscode(kind) {
  await Promise.all([buildTinymistVscodeSystem(), buildLspBinary(kind)]);
}

export async function installVscode(kind) {
  const [{ pkgVersion }, ..._rest] = await Promise.all([
    checkVersion(),
    buildTinymistVscodeSystem(),
    buildLspBinary(kind),
  ]);
  await spawnAsync("package:vscode", "cd editors/vscode && yarn package");

  // install code
  await spawnAsync(
    "install:vscode",
    `cd editors/vscode && code --install-extension tinymist-${pkgVersion}.vsix`,
  );
}

export async function buildWebLspBinaryBase() {
  await spawnAsync(
    "lsp:web",
    "cd crates/tinymist && wasm-pack build --target web -- --no-default-features --features web,no-content-hint",
  );
}

export async function buildWebLspBinary() {
  await buildWebLspBinaryBase();
  await fs.copyFile(
    path.resolve(cwd, "crates/tinymist/pkg/tinymist_bg.wasm"),
    path.resolve(vscodeDir, "out/tinymist_bg.wasm"),
  );
}
