const gulp = require("gulp");
const fs = require("fs-extra");
const path = require("path");
const chalk = require("chalk");
const archiver = require("archiver");
const stringify = require("json-stringify-pretty-compact");
const typescript = require("typescript");

const ts = require("gulp-typescript");
const less = require("gulp-less");
const sass = require("gulp-sass");
const git = require("gulp-git");

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

sass.compiler = require("sass");

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

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

function getManifest() {
    const json = {};

    if (fs.existsSync("src")) {
        json.root = "src";
    } else {
        json.root = "dist";
    }

    const modulePath = path.join(json.root, "module.json");
    const systemPath = path.join(json.root, "system.json");

    if (fs.existsSync(modulePath)) {
        json.file = fs.readJSONSync(modulePath);
        json.name = "module.json";
    } else if (fs.existsSync(systemPath)) {
        json.file = fs.readJSONSync(systemPath);
        json.name = "system.json";
    } else {
        return;
    }

    return json;
}

/**
 * TypeScript transformers
 * @returns {typescript.TransformerFactory<typescript.SourceFile>}
 */
function createTransformer() {
    /**
     * @param {typescript.Node} node
     */
    function shouldMutateModuleSpecifier(node) {
        if (!typescript.isImportDeclaration(node) && !typescript.isExportDeclaration(node)) return false;
        if (node.moduleSpecifier === undefined) return false;
        if (!typescript.isStringLiteral(node.moduleSpecifier)) return false;
        if (!node.moduleSpecifier.text.startsWith("./") && !node.moduleSpecifier.text.startsWith("../")) return false;
        if (path.extname(node.moduleSpecifier.text) !== "") return false;
        return true;
    }

    /**
     * Transforms import/export declarations to append `.js` extension
     * @param {typescript.TransformationContext} context
     */
    function importTransformer(context) {
        return (node) => {
            /**
             * @param {typescript.Node} node
             */
            function visitor(node) {
                if (shouldMutateModuleSpecifier(node)) {
                    if (typescript.isImportDeclaration(node)) {
                        const newModuleSpecifier = typescript.createLiteral(`${node.moduleSpecifier.text}.js`);
                        return typescript.updateImportDeclaration(
                            node,
                            node.decorators,
                            node.modifiers,
                            node.importClause,
                            newModuleSpecifier,
                        );
                    } else if (typescript.isExportDeclaration(node)) {
                        const newModuleSpecifier = typescript.createLiteral(`${node.moduleSpecifier.text}.js`);
                        return typescript.updateExportDeclaration(
                            node,
                            node.decorators,
                            node.modifiers,
                            node.exportClause,
                            newModuleSpecifier,
                        );
                    }
                }
                return typescript.visitEachChild(node, visitor, context);
            }

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

    return importTransformer;
}

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

/********************/
/*		BUILD		*/
/********************/

/**
 * Build TypeScript
 */
function buildTS() {
    return gulp.src("src/**/*.ts").pipe(tsConfig()).pipe(gulp.dest("dist"));
}

/**
 * Build Less
 */
function buildLess() {
    return gulp.src("src/*.less").pipe(less()).pipe(gulp.dest("dist"));
}

/**
 * Build SASS
 */
function buildSASS() {
    return gulp.src("src/*.scss").pipe(sass().on("error", sass.logError)).pipe(gulp.dest("dist"));
}

/**
 * Copy static files
 */
async function copyFiles() {
    const statics = ["lang", "fonts", "assets", "templates", "module.json", "system.json", "template.json"];
    try {
        for (const file of statics) {
            if (fs.existsSync(path.join("src", file))) {
                await fs.copy(path.join("src", file), path.join("dist", file));
            }
        }
        return Promise.resolve();
    } catch (err) {
        Promise.reject(err);
    }
}

/**
 * Watch for changes for each build step
 */
function buildWatch() {
    gulp.watch("src/**/*.ts", { ignoreInitial: false }, buildTS);
    gulp.watch("src/**/*.less", { ignoreInitial: false }, buildLess);
    gulp.watch("src/**/*.scss", { ignoreInitial: false }, buildSASS);
    gulp.watch(["src/fonts", "src/lang", "src/templates", "src/*.json"], { ignoreInitial: false }, copyFiles);
}

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

/**
 * Remove built files from `dist` folder
 * while ignoring source files
 */
async function clean() {
    const name = path.basename(path.resolve("."));
    const files = [];

    // If the project uses TypeScript
    if (fs.existsSync(path.join("src", `${name}.ts`))) {
        files.push(
            "lang",
            "templates",
            "assets",
            "module",
            `${name}.js`,
            "module.json",
            "system.json",
            "template.json",
        );
    }

    // If the project uses Less or SASS
    if (fs.existsSync(path.join("src", `${name}.less`)) || fs.existsSync(path.join("src", `${name}.scss`))) {
        files.push("fonts", `${name}.css`);
    }

    console.log(" ", chalk.yellow("Files to clean:"));
    console.log("   ", chalk.blueBright(files.join("\n    ")));

    // Attempt to remove the files
    try {
        for (const filePath of files) {
            await fs.remove(path.join("dist", filePath));
        }
        return Promise.resolve();
    } catch (err) {
        Promise.reject(err);
    }
}

/********************/
/*		LINK		*/
/********************/

/**
 * Link build to User Data folder
 */
async function linkUserData() {
    const name = path.basename(path.resolve("."));
    const config = fs.readJSONSync("foundryconfig.json");

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

        let linkDir;
        if (config.dataPath) {
            if (!fs.existsSync(path.join(config.dataPath, "Data")))
                throw Error("User Data path invalid, no Data directory found");

            linkDir = path.join(config.dataPath, "Data", destDir, name);
        } else {
            throw Error("No User Data path defined in foundryconfig.json");
        }

        if (argv.clean || argv.c) {
            console.log(chalk.yellow(`Removing build in ${chalk.blueBright(linkDir)}`));

            await fs.remove(linkDir);
        } else if (!fs.existsSync(linkDir)) {
            console.log(chalk.green(`Copying build to ${chalk.blueBright(linkDir)}`));
            await fs.symlink(path.resolve("./dist"), linkDir);
        }
        return Promise.resolve();
    } catch (err) {
        Promise.reject(err);
    }
}

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

/**
 * Package build
 */
async function packageBuild() {
    const manifest = getManifest();

    return new Promise((resolve, reject) => {
        try {
            // Remove the package dir without doing anything else
            if (argv.clean || argv.c) {
                console.log(chalk.yellow("Removing all packaged files"));
                fs.removeSync("package");
                return;
            }

            // Ensure there is a directory to hold all the packaged versions
            fs.ensureDirSync("package");

            // Initialize the zip file
            const zipName = `${manifest.file.name}-v${manifest.file.version}.zip`;
            const zipFile = fs.createWriteStream(path.join("package", zipName));
            const zip = archiver("zip", { zlib: { level: 9 } });

            zipFile.on("close", () => {
                console.log(chalk.green(zip.pointer() + " total bytes"));
                console.log(chalk.green(`Zip file ${zipName} has been written`));
                return resolve();
            });

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

            zip.pipe(zipFile);

            // Add the directory with the final code
            zip.directory("dist/", manifest.file.name);

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

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

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

    if (!config) cb(Error(chalk.red("foundryconfig.json not found")));
    if (!manifest) cb(Error(chalk.red("Manifest JSON not found")));
    if (!rawURL || !repoURL) cb(Error(chalk.red("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, major, minor, patch) => {
                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(chalk.red("Error: Incorrect version arguments.")));
        }

        if (targetVersion === currentVersion) {
            return cb(Error(chalk.red("Error: Target version is identical to current version.")));
        }
        console.log(`Updating version number to '${targetVersion}'`);

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

        /* Update URLs */

        const result = `${rawURL}/v${manifest.file.version}/package/${manifest.file.name}-v${manifest.file.version}.zip`;

        manifest.file.url = repoURL;
        manifest.file.manifest = `${rawURL}/master/${manifestRoot}/${manifest.name}`;
        manifest.file.download = result;

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

        fs.writeJSONSync("package.json", packageJson, { spaces: "\t" });
        fs.writeFileSync(path.join(manifest.root, manifest.name), prettyProjectJson, "utf8");

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

function gitAdd() {
    return gulp.src("package").pipe(git.add({ args: "--no-all" }));
}

function gitCommit() {
    return gulp.src("./*").pipe(
        git.commit(`v${getManifest().file.version}`, {
            args: "-a",
            disableAppendPaths: true,
        }),
    );
}

function gitTag() {
    const manifest = getManifest();
    return git.tag(`v${manifest.file.version}`, `Updated to ${manifest.file.version}`, (err) => {
        if (err) throw err;
    });
}

const execGit = gulp.series(gitAdd, gitCommit, gitTag);

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

exports.build = gulp.series(clean, execBuild);
exports.watch = buildWatch;
exports.clean = clean;
exports.link = linkUserData;
exports.package = packageBuild;
exports.update = updateManifest;
exports.publish = gulp.series(clean, updateManifest, execBuild, packageBuild, execGit);