import * as gulp from "gulp";
import fs from "fs-extra";
import * as path from "path";
import archiver from "archiver";
import stringify from "json-stringify-pretty-compact";

const sourcemaps = require("gulp-sourcemaps");
const uglify = require("gulp-uglify");
const concat = require("gulp-concat");
const buffer = require("vinyl-buffer");
const source = require("vinyl-source-stream");
const through = require("through2");
const jsonminify = require("gulp-jsonminify");
const merge2 = require("merge2");

const git = require("gulp-git-streamed");

const loadJson = (path: string): any => {
	try {
		let str = fs.readFileSync(path).toString();
		return JSON.parse(str);
	} catch {
		throw Error("Unable to load " + path);
	}
};

import {
	createLiteral,
	factory,
	isExportDeclaration,
	isImportDeclaration,
	isStringLiteral,
	LiteralExpression,
	Node,
	TransformationContext,
	Transformer as TSTransformer,
	TransformerFactory,
	visitEachChild,
	visitNode,
} from "typescript";
import less from "gulp-less";

import Logger from "./source/utils/Logger";
import { ModuleData } from "@league-of-foundry-developers/foundry-vtt-types/src/foundry/common/packages.mjs";
import browserify from "browserify";
import { data } from "jquery";
const tsify = require("tsify");

const ts = require("gulp-typescript");

const argv = require("yargs").argv;

let distPath = "dist";

function getConfig() {
	const configPath = path.resolve(process.cwd(), "foundryconfig.json");
	let config;

	if (fs.existsSync(configPath)) {
		config = loadJson(configPath);
		return config;
	} else {
		return;
	}
}

interface Manifest {
	root: string;
	file: ModuleData;
	name: string;
}

const getManifest = (): Manifest | null => {
	const json: Manifest = {
		root: "",
		// @ts-ignore
		file: {},
		name: "",
	};

	if (fs.existsSync("source")) {
		json.root = "source";
	} else {
		json.root = distPath;
	}
	const modulePath = path.join(json.root, "module.json");
	const systemPath = path.join(json.root, "system.json");
	if (fs.existsSync(modulePath)) {
		json.file = loadJson(modulePath) as ModuleData;
		json.name = "module.json";
	} else if (fs.existsSync(systemPath)) {
		json.file = loadJson(systemPath) as ModuleData;
		json.name = "system.json";
	} else {
		return null;
	}
	return json;
};

const createTransformer = (): TransformerFactory<any> => {
	/**
	 * @param {typescript.Node} node
	 */
	const shouldMutateModuleSpecifier = (node: Node): boolean => {
		if (!isImportDeclaration(node) && !isExportDeclaration(node)) return false;
		if (node.moduleSpecifier === undefined) return false;
		if (!isStringLiteral(node.moduleSpecifier)) return false;
		if (!node.moduleSpecifier.text.startsWith("./") && !node.moduleSpecifier.text.startsWith("../")) return false;

		return path.extname(node.moduleSpecifier.text) === "";
	};

	return (context: TransformationContext): TSTransformer<any> => {
		return (node: Node) => {
			function visitor(node: Node): Node {
				if (shouldMutateModuleSpecifier(node)) {
					if (isImportDeclaration(node)) {
						const newModuleSpecifier = createLiteral(`${(node.moduleSpecifier as LiteralExpression).text}.js`);
						return factory.updateImportDeclaration(node, node.decorators, node.modifiers, node.importClause, newModuleSpecifier, undefined);
					} else if (isExportDeclaration(node)) {
						const newModuleSpecifier = createLiteral(`${(node.moduleSpecifier as LiteralExpression).text}.js`);
						return factory.updateExportDeclaration(node, node.decorators, node.modifiers, false, node.exportClause, newModuleSpecifier, undefined);
					}
				}
				return visitEachChild(node, visitor, context);
			}

			return visitNode(node, visitor);
		};
	};
};

const tsConfig = ts.createProject("tsconfig.json", {
	getCustomTransformers: (_program: any) => ({
		after: [createTransformer()],
	}),
});

function buildTS() {
	const debug = process.env.npm_lifecycle_event !== "package";
	let res = tsConfig.src().pipe(sourcemaps.init()).pipe(tsConfig());

	return res.js.pipe(sourcemaps.write("", { debug: debug, includeContent: true, sourceRoot: "./ts/source" })).pipe(gulp.dest(distPath));
}

const bundleModule = () => {
	const debug = argv.dbg || argv.debug;
	const bsfy = browserify(path.join(__dirname, "source/index.ts"), { debug: debug });
	return bsfy
		.on("error", Logger.err)
		.plugin(tsify)
		.bundle()
		.pipe(source(path.join(distPath, "bundle.js")))
		.pipe(buffer())
		.pipe(sourcemaps.init({ loadMaps: true }))
		.pipe(uglify())
		.pipe(sourcemaps.write("./"))
		.pipe(gulp.dest("./"));
};

const buildLess = () => {
	return gulp.src("source/style/*.less").pipe(less()).pipe(concat("bundle.css")).pipe(gulp.dest(distPath));
};

interface Pack {
	root: string;
	type: string;
	name: string;
}

const buildPack = (pack: Pack): NodeJS.ReadWriteStream => {
	return gulp
		.src(pack.root + "/" + pack.type + "/" + pack.name + "/*.json")
		.pipe(jsonminify())
		.pipe(concat(pack.name + ".db"))
		.pipe(gulp.dest(distPath + "/" + pack.root + "/" + pack.type));
};

const buildPacks = () => {
	let packs: Pack[] = [];

	const rootDir = "packs";
	const packTypes = fs.readdirSync(rootDir).filter((p) => fs.statSync(path.join(rootDir, p)).isDirectory());
	packTypes.forEach((packType) => {
		const packDir = path.join(rootDir, packType);
		const packNames = fs.readdirSync(packDir).filter((p) => fs.statSync(path.join(packDir, p)).isDirectory());
		packNames.forEach((packName) => {
			packs.push({
				name: packName,
				type: packType,
				root: rootDir,
			});
		});
	});

	return merge2(packs.map((p) => buildPack(p)));
};

const copyFiles = async () => {
	const recursiveFileSearch = (dir: string, callback: (err: NodeJS.ErrnoException | null, res: Array<string>) => void) => {
		let results: Array<string> = [];
		fs.readdir(dir, (err, list) => {
			if (err) return callback(err, results);

			let pending = list.length;
			if (!pending) return callback(null, results);

			for (let file of list) {
				file = path.resolve(dir, file);
				fs.stat(file, (err, stat) => {
					if (stat && stat.isDirectory()) {
						recursiveFileSearch(file, (err, res) => {
							results = results.concat(res);
							if (!--pending) callback(null, results);
						});
					} else {
						results.push(file);
						if (!--pending) callback(null, results);
					}
				});
			}
		});
	};
	try {
		const modulePath = path.join("source", "module.json");
		if (fs.existsSync(modulePath)) await fs.copyFile(modulePath, path.join(distPath, "module.json"));

		const systemPath = path.join("source/system.json");
		if (fs.existsSync(systemPath)) await fs.copyFile(systemPath, path.join(distPath, "system.json"));

		if (!fs.existsSync(path.resolve(__dirname, "assets"))) return Promise.resolve();

		const filter = (src: string, dest: string): boolean => {
			Logger.ok("Copying file: " + dest);
			return true;
		};

		await fs.copyFile(path.join("source", "template.json"), path.join(distPath, "template.json"));

		fs.copySync(path.resolve(__dirname, "assets"), path.resolve(__dirname, distPath + "/assets"), { overwrite: true, filter });
		fs.copySync(path.resolve(__dirname, "lang"), path.resolve(__dirname, distPath + "/lang"), { overwrite: true, filter });
		//fs.copySync(path.resolve(__dirname, "packs"), path.resolve(__dirname, distPath + "/packs"), { overwrite: true, filter })
		fs.copySync(path.resolve(__dirname, "templates"), path.resolve(__dirname, distPath + "/templates"), { overwrite: true, filter });
		return Promise.resolve();
	} catch (err) {
		await Promise.reject(err);
	}
};

const cleanDist = async () => {
	if (argv.dbg || argv.debug) return;
	Logger.log("Cleaning dist file clutter");

	const files: string[] = [];
	const getFiles = async (dir: string) => {
		const arr = await fs.promises.readdir(dir);
		for (const entry of arr) {
			const fullPath = path.join(dir, entry);
			const stat = await fs.promises.stat(fullPath);
			if (stat.isDirectory()) await getFiles(fullPath);
			else files.push(fullPath);
		}
	};

	await getFiles(path.resolve(distPath));
	for (const file of files) {
		if (file.endsWith("bundle.js") || file.endsWith(".css") || file.endsWith("module.json")) continue;

		Logger.warn("Cleaning " + path.relative(process.cwd(), file));
		await fs.promises.unlink(file);
	}
};

/**
 * Watch for changes for each build step
 */
const buildWatch = () => {
	gulp.watch("source/**/*.ts", { ignoreInitial: false }, gulp.series(buildTS, bundleModule));
	gulp.watch("source/**/*.less", { ignoreInitial: false }, buildLess);
	gulp.watch("packs", { ignoreInitial: false }, buildPacks);
	gulp.watch(["assets", "lang", "templates", "source/*.json"], { ignoreInitial: false }, copyFiles);
};

/********************/
/*		CLEAN		*/
/********************/

/**
 * Remove built files from `dist` folder
 * while ignoring source files
 */
const clean = async () => {
	if (!fs.existsSync(distPath)) fs.mkdirSync(distPath);
	else {
		// Attempt to remove the files
		try {
			fs.rmSync(distPath, { recursive: true, force: true });
			fs.mkdirSync(distPath);
			return Promise.resolve();
		} catch (err) {
			await Promise.reject(err);
		}
	}
};

const setTargetDir = async () => {
	const dp = process.env.FOUNDRY_PATH;
	if (!dp) throw Error("FOUNDRY_PATH not defined in environment");

	const name = getManifest()!.file.name ?? "midgard5";
	distPath = path.join(dp, "Data", "systems", name);
};

const linkUserData = async () => {
	const name = getManifest()!.file.name;

	let destDir;
	try {
		if (fs.existsSync(path.resolve(".", distPath, "module.json")) || fs.existsSync(path.resolve(".", "source", "module.json"))) {
			destDir = "modules";
		} else if (fs.existsSync(path.resolve(".", distPath, "system.json")) || fs.existsSync(path.resolve(".", "source", "system.json"))) {
			destDir = "systems";
		} else {
			throw Error(`Could not find module.json or system.json`);
		}

		let linkDir;
		const dataPath = process.env.FOUNDRY_PATH;
		if (dataPath) {
			if (!fs.existsSync(path.join(dataPath, "Data"))) throw Error("User Data path invalid, no Data directory found");
			linkDir = path.join(dataPath, "Data", destDir, name as string);
		} else {
			throw Error("FOUNDRY_PATH not defined in environment");
		}

		//if (argv.clean || argv.c) {
		Logger.warn(`Removing build in ${linkDir}`);
		fs.rmSync(linkDir, { recursive: true, force: true });
		fs.mkdirSync(linkDir);
		//}

		Logger.ok(`Copying build to ${linkDir}`);
		fs.copySync(path.resolve(distPath), linkDir, { overwrite: true });

		return Promise.resolve();
	} catch (err) {
		await Promise.reject(err);
	}
};

/*********************/
/*		PACKAGE		 */
/*********************/

/**
 * Package build
 */
async function packageBuild() {
	const manifest = getManifest();
	if (manifest === null) {
		Logger.err("Manifest file could not be loaded.");
		throw Error();
	}

	return new Promise((resolve, reject) => {
		try {
			// Remove the package dir without doing anything else
			if (argv.clean || argv.c) {
				Logger.warn("Removing all packaged files");
				fs.rmSync(distPath, { force: true, recursive: true });
				return;
			}

			// Ensure there is a directory to hold all the packaged versions
			if (!fs.existsSync(distPath)) fs.mkdirSync(distPath);

			// Initialize the zip file
			const zipName = `${manifest.file.name}-v${manifest.file.version}.zip`;
			const zipFile = fs.createWriteStream(zipName);
			const zip = archiver("zip", { zlib: { level: 9 } });
			zipFile.on("close", () => {
				Logger.ok(zip.pointer() + " total bytes");
				Logger.ok(`Zip file ${zipName} has been written`);
				return resolve(true);
			});

			zipFile.on("end", function () {
				throw "Data has been drained";
			});

			zip.on("error", (err) => {
				throw err;
			});

			zip.pipe(zipFile);

			zip.directory(distPath, manifest.file.name);

			return zip.finalize();
		} catch (err) {
			return reject(err);
		}
	});
}

/*********************/
/*		PACKAGE		 */
/*********************/

/**
 * Update version and URLs in the manifest JSON
 */
const updateManifest = (cb: any) => {
	const packageJson = loadJson("package.json");
	const config = getConfig(),
		manifest = getManifest(),
		rawURL = config.rawURL,
		repoURL = config.repository,
		manifestRoot = manifest!.root;

	if (!config) cb(Error("foundryconfig.json not found"));
	if (manifest === null) {
		cb(Error("Manifest JSON not found"));
		return;
	}
	if (!rawURL || !repoURL) cb(Error("Repository URLs not configured in foundryconfig.json"));

	try {
		const version = argv.update || argv.u;

		/* Update version */

		const versionMatch = /^(\d{1,}).(\d{1,}).(\d{1,})$/;
		const currentVersion = manifest!.file.version;
		let targetVersion = "";

		if (!version) {
			cb(Error("Missing version number"));
		}

		if (versionMatch.test(version)) {
			targetVersion = version;
		} else {
			targetVersion = currentVersion.replace(versionMatch, (substring: string, major: string, minor: string, patch: string) => {
				console.log(substring, Number(major) + 1, Number(minor) + 1, Number(patch) + 1);
				if (version === "major") {
					return `${Number(major) + 1}.0.0`;
				} else if (version === "minor") {
					return `${major}.${Number(minor) + 1}.0`;
				} else if (version === "patch") {
					return `${major}.${minor}.${Number(patch) + 1}`;
				} else {
					return "";
				}
			});
		}

		if (targetVersion === "") {
			return cb(Error("Error: Incorrect version arguments."));
		}

		if (targetVersion === currentVersion) {
			return cb(Error("Error: Target version is identical to current version."));
		}

		Logger.ok(`Updating version number to '${targetVersion}'`);

		packageJson.version = targetVersion;
		manifest.file.version = targetVersion;

		/* Update URLs */

		const result = `${repoURL}/releases/download`;

		manifest.file.url = repoURL;
		manifest.file.manifest = `${result}/v${manifest.file.version}/${manifest.name}`;
		manifest.file.download = `${result}/v${manifest.file.version}/${manifest.file.name}-v${manifest.file.version}.zip`;

		const prettyProjectJson = stringify(manifest.file, {
			maxLength: 35,
			indent: "\t",
		});

		fs.writeFileSync("package.json", JSON.stringify(packageJson, null, "\t"));
		fs.writeFileSync(path.join(manifest.root, manifest.name), prettyProjectJson, "utf8");

		return cb();
	} catch (err) {
		return cb(err);
	}
};

const gitTaskManifest = (cb: gulp.TaskFunctionCallback) => {
	const manifest = getManifest();
	if (!manifest) return cb(Error("could not load manifest."));
	return gulp
		.src([`package.json`, `source/system.json`])
		.pipe(git.add({ args: "--no-all -f" }))
		.pipe(git.commit(`v${manifest.file.version}`, { args: "-a", disableAppendPaths: true }));
};

const gitTaskBuild = (cb: gulp.TaskFunctionCallback) => {
	const manifest = getManifest();
	if (!manifest) return cb(Error("could not load manifest."));

	return gulp
		.src(`${manifest.file.name}-v${manifest.file.version}.zip`)
		.pipe(git.checkout(`v${manifest.file.version}`, { args: "-b" }))
		.pipe(git.add({ args: "--no-all -f" }))
		.pipe(git.commit(`v${manifest.file.version}`, { args: "-a", disableAppendPaths: true }));
};

const execBuild = gulp.parallel(buildTS, buildLess, buildPacks, copyFiles);

exports.build = gulp.series(clean, execBuild, bundleModule);
exports.buildTarget = gulp.series(setTargetDir, clean, execBuild, bundleModule);
exports.watch = buildWatch;
exports.watchTarget = gulp.series(setTargetDir, buildWatch);
exports.clean = clean;
exports.link = linkUserData;
exports.package = packageBuild;
exports.update = updateManifest;
exports.publish = gulp.series(clean, updateManifest, execBuild, bundleModule, packageBuild, gitTaskManifest);
