import { register, run } from "@coder/runner";
import * as fs from "fs";
import * as fse from "fs-extra";
import * as os from "os";
import * as path from "path";
import * as zlib from "zlib";

const isWin = os.platform() === "win32";
const libPath = path.join(__dirname, "../lib");
const vscodePath = path.join(libPath, "vscode");
const pkgsPath = path.join(__dirname, "../packages");
const defaultExtensionsPath = path.join(libPath, "VSCode-linux-x64/resources/app/extensions");

const buildServerBinary = register("build:server:binary", async (runner) => {
	await ensureInstalled();
	await copyForDefaultExtensions();
	await Promise.all([
		buildBootstrapFork(),
		buildWeb(),
		buildDefaultExtensions(),
		buildServerBundle(),
		buildAppBrowser(),
	]);

	await buildServerBinaryPackage();
});

const buildServerBinaryPackage = register("build:server:binary:package", async (runner) => {
	const cliPath = path.join(pkgsPath, "server");
	runner.cwd = cliPath;
	if (!fs.existsSync(path.join(cliPath, "out"))) {
		throw new Error("Cannot build binary without server bundle built");
	}
	await buildServerBinaryCopy();
	await dependencyNexeBinary();
	const resp = await runner.execute(isWin ? "npm.cmd" : "npm", ["run", "build:nexe"]);
	if (resp.exitCode !== 0) {
		throw new Error(`Failed to package binary: ${resp.stderr}`);
	}
});

const dependencyNexeBinary = register("dependency:nexe", async (runner) => {
	if (os.platform() === "linux") {
		const nexeDir = path.join(os.homedir(), ".nexe");
		const targetBinaryName = `${os.platform()}-${os.arch()}-${process.version.substr(1)}`;
		const targetBinaryPath = path.join(nexeDir, targetBinaryName);
		if (!fs.existsSync(targetBinaryPath)) {
			/**
			 * We create a binary with nexe
			 * so we can compress it
			 */
			fse.mkdirpSync(nexeDir);
			runner.cwd = nexeDir;
			await runner.execute("wget", [`https://github.com/nexe/nexe/releases/download/v3.0.0-beta.15/${targetBinaryName}`]);
			await runner.execute("chmod", ["+x", targetBinaryPath]);
		}
		if (fs.statSync(targetBinaryPath).size >= 20000000) {
			// Compress w/ upx
			const upxFolder = path.join(os.tmpdir(), "upx");
			const upxBinary = path.join(upxFolder, "upx");
			if (!fs.existsSync(upxBinary)) {
				fse.mkdirpSync(upxFolder);
				runner.cwd = upxFolder;
				const upxExtract = await runner.execute("bash", ["-c", "curl -L https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz | tar xJ --strip-components=1"]);
				if (upxExtract.exitCode !== 0) {
					throw new Error(`Failed to extract upx: ${upxExtract.stderr}`);
				}
			}
			if (!fs.existsSync(upxBinary)) {
				throw new Error("Not sure how, but the UPX binary does not exist");
			}
			await runner.execute(upxBinary, [targetBinaryPath]);
		}
	}
});

const buildServerBinaryCopy = register("build:server:binary:copy", async (runner) => {
	const cliPath = path.join(pkgsPath, "server");
	const cliBuildPath = path.join(cliPath, "build");
	fse.removeSync(cliBuildPath);
	fse.mkdirpSync(path.join(cliBuildPath, "extensions"));
	const bootstrapForkPath = path.join(pkgsPath, "vscode", "out", "bootstrap-fork.js");
	const webOutputPath = path.join(pkgsPath, "web", "out");
	const browserAppOutputPath = path.join(pkgsPath, "app", "browser", "out");
	const nodePtyModule = path.join(pkgsPath, "protocol", "node_modules", "node-pty-prebuilt", "build", "Release", "pty.node");
	const spdlogModule = path.join(pkgsPath, "protocol", "node_modules", "spdlog", "build", "Release", "spdlog.node");
	let ripgrepPath = path.join(pkgsPath, "..", "lib", "vscode", "node_modules", "vscode-ripgrep", "bin", "rg");
	if (isWin) {
		ripgrepPath += ".exe";
	}

	if (!fs.existsSync(nodePtyModule)) {
		throw new Error("Could not find pty.node. Ensure all packages have been installed");
	}
	if (!fs.existsSync(spdlogModule)) {
		throw new Error("Could not find spdlog.node. Ensure all packages have been installed");
	}
	if (!fs.existsSync(webOutputPath)) {
		throw new Error("Web bundle must be built");
	}
	if (!fs.existsSync(defaultExtensionsPath)) {
		throw new Error("Default extensions must be built");
	}
	if (!fs.existsSync(bootstrapForkPath)) {
		throw new Error("Bootstrap fork must exist");
	}
	if (!fs.existsSync(ripgrepPath)) {
		throw new Error("Ripgrep must exist");
	}
	fse.copySync(defaultExtensionsPath, path.join(cliBuildPath, "extensions"));
	fs.writeFileSync(path.join(cliBuildPath, "bootstrap-fork.js.gz"), zlib.gzipSync(fs.readFileSync(bootstrapForkPath)));
	const cpDir = (dir: string, subdir: "auth" | "unauth", rootPath: string): void => {
		const stat = fs.statSync(dir);
		if (stat.isDirectory()) {
			const paths = fs.readdirSync(dir);
			paths.forEach((p) => cpDir(path.join(dir, p), subdir, rootPath));
		} else if (stat.isFile()) {
			const newPath = path.join(cliBuildPath, "web", subdir, path.relative(rootPath, dir));
			fse.mkdirpSync(path.dirname(newPath));
			fs.writeFileSync(newPath + ".gz", zlib.gzipSync(fs.readFileSync(dir)));
		} else {
			// Nothing
		}
	};
	cpDir(webOutputPath, "auth", webOutputPath);
	cpDir(browserAppOutputPath, "unauth", browserAppOutputPath);
	fse.mkdirpSync(path.join(cliBuildPath, "dependencies"));
	fse.copySync(nodePtyModule, path.join(cliBuildPath, "dependencies", "pty.node"));
	fse.copySync(spdlogModule, path.join(cliBuildPath, "dependencies", "spdlog.node"));
	fse.copySync(ripgrepPath, path.join(cliBuildPath, "dependencies", "rg"));
});

const buildServerBundle = register("build:server:bundle", async (runner) => {
	const cliPath = path.join(pkgsPath, "server");
	runner.cwd = cliPath;
	await runner.execute(isWin ? "npm.cmd" : "npm", ["run", "build"]);
});

const buildBootstrapFork = register("build:bootstrap-fork", async (runner) => {
	await ensureInstalled();
	await ensurePatched();

	const vscodePkgPath = path.join(pkgsPath, "vscode");
	runner.cwd = vscodePkgPath;
	await runner.execute(isWin ? "npm.cmd" : "npm", ["run", "build:bootstrap-fork"]);
});

const buildAppBrowser = register("build:app:browser", async (runner) => {
	await ensureInstalled();

	const appPath = path.join(pkgsPath, "app/browser");
	runner.cwd = appPath;
	fse.removeSync(path.join(appPath, "out"));
	await runner.execute(isWin ? "npm.cmd" : "npm", ["run", "build"]);
});

const buildWeb = register("build:web", async (runner) => {
	await ensureInstalled();
	await ensurePatched();

	const webPath = path.join(pkgsPath, "web");
	runner.cwd = webPath;
	fse.removeSync(path.join(webPath, "out"));
	await runner.execute(isWin ? "npm.cmd" : "npm", ["run", "build"]);
});

const extDirPath = path.join("lib", "vscode-default-extensions");
const copyForDefaultExtensions = register("build:copy-vscode", async (runner) => {
	if (!fs.existsSync(defaultExtensionsPath)) {
		await ensureClean();
		await ensureInstalled();
		await new Promise((resolve, reject): void => {
			fse.remove(extDirPath, (err) => {
				if (err) {
					return reject(err);
				}

				resolve();
			});
		});
		await new Promise((resolve, reject): void => {
			fse.copy(vscodePath, extDirPath, (err) => {
				if (err) {
					return reject(err);
				}

				resolve();
			});
		});
	}
});

const buildDefaultExtensions = register("build:default-extensions", async (runner) => {
	if (!fs.existsSync(defaultExtensionsPath)) {
		await copyForDefaultExtensions();
		runner.cwd = extDirPath;
		const resp = await runner.execute(isWin ? "npx.cmd" : "npx", [isWin ? "gulp.cmd" : "gulp", "vscode-linux-x64"]);
		if (resp.exitCode !== 0) {
			throw new Error(`Failed to build default extensions: ${resp.stderr}`);
		}
	}
});

const ensureInstalled = register("vscode:install", async (runner) => {
	await ensureCloned();

	runner.cwd = vscodePath;
	const install = await runner.execute(isWin ? "yarn.cmd" : "yarn", []);
	if (install.exitCode !== 0) {
		throw new Error(`Failed to install vscode dependencies: ${install.stderr}`);
	}
});

const ensureCloned = register("vscode:clone", async (runner) => {
	if (fs.existsSync(vscodePath)) {
		await ensureClean();
	} else {
		fse.mkdirpSync(libPath);
		runner.cwd = libPath;
		const clone = await runner.execute("git", ["clone", "https://github.com/microsoft/vscode"]);
		if (clone.exitCode !== 0) {
			throw new Error(`Failed to clone: ${clone.exitCode}`);
		}
	}

	runner.cwd = vscodePath;
	const checkout = await runner.execute("git", ["checkout", "tags/1.31.0"]);
	if (checkout.exitCode !== 0) {
		throw new Error(`Failed to checkout: ${checkout.stderr}`);
	}
});

const ensureClean = register("vscode:clean", async (runner) => {
	runner.cwd = vscodePath;

	const status = await runner.execute("git", ["status", "--porcelain"]);
	if (status.stdout.trim() !== "") {
		const clean = await runner.execute("git", ["clean", "-f", "-d", "-X"]);
		if (clean.exitCode !== 0) {
			throw new Error(`Failed to clean git repository: ${clean.stderr}`);
		}
		const removeUnstaged = await runner.execute("git", ["checkout", "--", "."]);
		if (removeUnstaged.exitCode !== 0) {
			throw new Error(`Failed to remove unstaged files: ${removeUnstaged.stderr}`);
		}
	}
});

const ensurePatched = register("vscode:patch", async (runner) => {
	if (!fs.existsSync(vscodePath)) {
		throw new Error("vscode must be cloned to patch");
	}
	await ensureClean();

	runner.cwd = vscodePath;
	const patchPath = path.join(__dirname, "../scripts/vscode.patch");
	const apply = await runner.execute("git", ["apply", "--unidiff-zero", patchPath]);
	if (apply.exitCode !== 0) {
		throw new Error(`Failed to apply patches: ${apply.stderr}`);
	}
});

run();
