import { Server, Socket } from "net";
import browserSync = require("browser-sync");
import browserify = require("browserify");
import log = require("fancy-log");
import FileSystem = require("fs-extra");
import { TaskFunction } from "gulp";
import gulp = require("gulp");
import gulpIf = require("gulp-if");
import rename = require("gulp-rename");
import sass = require("gulp-sass");
import terser = require("gulp-terser");
import merge = require("merge-stream");
import minimist = require("minimist");
import PromiseQueue = require("promise-queue");
import { parseArgsStringToArgv } from "string-argv";
import Path = require("upath");
import buffer = require("vinyl-buffer");
import vinylSourceStream = require("vinyl-source-stream");
import Watchify = require("watchify");
import { Settings } from "./gulp/Settings";
import "./gulp/TaskFunction";

/**
 * The port to listen for stop-requests.
 */
const watchConnectorPort = 50958;

/**
 * An object for syncing browsers.
 */
let syncer = browserSync.create();

/**
 * The message that is printed when starting the compilation in watch mode.
 */
const watchStartMessage = "Starting compilation in watch mode...";

/**
 * The message that is printed when starting an incremental compilation.
 */
const incrementalMessage = "File change detected. Starting incremental compilation...";

/**
 * Generates the message that is printed after finishing a compilation in watch mode.
 *
 * @param errorCount
 * The number of errors which occurred.
 *
 * @returns
 * The formatted message.
 */
const watchFinishMessage = (errorCount: number): string =>
{
    return `Found ${errorCount} errors. Watching for file changes.`;
};

/**
 * The arguments passed by the user.
 */
let options = ParseArgs(process.argv.slice(2));

/**
 * Parses the specified arguments.
 *
 * @param args
 * The arguments to parse.
 *
 * @returns
 * The parsed arguments.
 */
function ParseArgs(args: string[]): minimist.ParsedArgs
{
    return minimist(
        args,
        {
            string: [
                "target"
            ],
            alias: {
                target: "t"
            },
            default: {
                target: "Debug"
            }
        });
}

/**
 * The settings for building the project.
 */
let settings = new Settings(options["target"]);

/**
 * Cleans the project.
 */
export async function Clean(): Promise<void>
{
    let directories = [
        "javascript",
        "css",
        "templates",
        "assets"
    ];

    for (let directory of directories)
    {
        await FileSystem.emptyDir(settings.RootPath(directory));
    }

    if (await FileSystem.pathExists(settings.TestThemePath()))
    {
        await FileSystem.remove(settings.TestThemePath());
    }

    await FileSystem.mkdirp(settings.TestThemePath());
    await FileSystem.remove(settings.TestThemePath());
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    await require("create-symlink")(settings.RootPath(), settings.TestThemePath(), { type: "junction" });
}

Clean.description = "Cleans the project.";

/**
 * Builds the project in watched mode.
 *
 * @param done
 * A callback which is executed once the task has finished.
 */
export let Watch: TaskFunction = (done) =>
{
    settings.Watch = true;
    Build();

    let server = new Server(
        (socket) =>
        {
            socket.on(
                "data",
                (data) =>
                {
                    let args = parseArgsStringToArgv(data.toString());
                    socket.destroy();

                    if (args[0] === "stop")
                    {
                        let options = ParseArgs(args.slice(1));

                        if (options["target"] === settings.Target)
                        {
                            syncer.exit();
                            server.close();
                            done();
                            process.exit();
                        }
                    }
                });
        });

    server.listen(watchConnectorPort);
};

Watch.description = "Builds the project in watched mode.";

/**
 * Reloads all browsers using `browser-sync`.
 *
 * @param filePath
 * A glob-path which points to the files which must be reloaded.
 *
 * @returns
 * The actual task.
 */
function BrowserSync(filePath?: string): TaskFunction
{
    let BrowserSync: TaskFunction = (done) =>
    {
        if (filePath)
        {
            syncer.reload(filePath);
        }
        else
        {
            syncer.reload();
        }

        done();
    };

    return BrowserSync;
}

/**
 * Builds the project.
 */
export async function Build(): Promise<void>
{
    if (settings.Watch)
    {
        log.info(watchStartMessage);

        syncer.init({
            open: false,
            proxy: "http://localhost",
            port: 3000,
            ui: {
                port: 3001
            },
            ghostMode: false,
            online: false
        });

        gulp.watch(settings.ThemeSource("**"), { usePolling: true }, gulp.series(Theme, BrowserSync("*.css")));
        gulp.watch(settings.TemplateSource("**"), { usePolling: true }, gulp.series(Templates, BrowserSync()));
    }

    await Promise.all(
        [
            Library(),
            Theme(),
            Templates()
        ]);
}

/**
 * Builds the TypeScript- and JavaScript-library.
 *
 * @returns
 * The pipeline to execute.
 */
export async function Library(): Promise<NodeJS.ReadWriteStream>
{
    let streams: Array<Promise<NodeJS.ReadWriteStream>> = [];
    let queue = new PromiseQueue();
    let tsConfigFile = settings.TypeScriptProjectRoot("tsconfig.json");
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    let tsConfig = require(tsConfigFile);

    let optionBase: browserify.Options = {
        ...Watchify.args,
        node: true,
        ignoreMissing: true,
        debug: settings.Debug
    };

    {
        let errorMessages: string[] = [];
        let files = (tsConfig.files as string[]).map(
            (file) => Path.relative(settings.TypeScriptSourceRoot(), settings.TypeScriptProjectRoot(file)));

        for (let file of files)
        {
            let bundler = browserify(
                {
                    ...optionBase,
                    basedir: settings.LibraryPath(Path.dirname(file)),
                    entries: [
                        settings.TypeScriptSourceRoot(file)
                    ],
                    standalone: Path.join(Path.dirname(file), Path.parse(file).name)
                });

            if (settings.Watch)
            {
                bundler = Watchify(bundler, { poll: true });
            }

            bundler.plugin(
                // eslint-disable-next-line @typescript-eslint/no-var-requires
                require("tsify"),
                {
                    project: tsConfigFile
                });

            let bundle = async (): Promise<NodeJS.ReadWriteStream> =>
            {
                return new Promise<NodeJS.ReadWriteStream>(
                    (resolve) =>
                    {
                        let stream = bundler.bundle().on(
                            "error",
                            (error) =>
                            {
                                let message: string = error.message;

                                if (!errorMessages.includes(message))
                                {
                                    let result = new RegExp(`^(${error["fileName"]})\\((\\d+|\\d+(,\\d+){1,3})\\): .* TS([\\d]+): (.*)$`).exec(message);
                                    errorMessages.push(message);
                                    console.log(`${Path.relative(process.cwd(), result[1])}(${result[2]}): ${result[4]} ${result[5]}`);
                                }
                            }
                        ).pipe(
                            vinylSourceStream(Path.changeExt(file, "js"))
                        ).pipe(
                            buffer()
                        ).pipe(
                            gulpIf(
                                !settings.Debug,
                                terser()
                            )
                        ).pipe(
                            gulp.dest(settings.LibraryPath())
                        );

                        stream.on(
                            "end",
                            () =>
                            {
                                if (settings.Watch && ((queue.getQueueLength() + queue.getPendingLength()) === 1))
                                {
                                    if (errorMessages.length === 0)
                                    {
                                        syncer.reload("*.js");
                                    }

                                    log.info(watchFinishMessage(errorMessages.length));
                                }

                                errorMessages.splice(0, errorMessages.length);
                                resolve(stream);
                            });
                    });
            };

            if (settings.Watch)
            {
                bundler.on(
                    "update",
                    () =>
                    {
                        if ((queue.getQueueLength() + queue.getPendingLength()) === 0)
                        {
                            log.info(incrementalMessage);
                        }

                        queue.add(
                            async () =>
                            {
                                return bundle();
                            });
                    });
            }

            let build = (): Promise<NodeJS.ReadWriteStream> => queue.add(bundle);
            build.displayName = Build.displayName;
            build.description = Build.description;
            streams.push(build());
        }
    }

    return merge(await Promise.all(streams)) as NodeJS.ReadWriteStream;
}

Library.description = "Builds the TypeScript- and JavaScript-library.";

/**
 * Builds the theme.
 *
 * @returns
 * The pipeline to execute.
 */
export async function Theme(): Promise<NodeJS.ReadWriteStream>
{
    if (settings.Watch)
    {
        console.log("Rebuilding scss-code.");
    }

    return gulp.src(
        settings.ThemeSource("main.scss"),
        {
            sourcemaps: settings.Debug,
            base: settings.StylePath()
        }).pipe(
            sass(
                {
                    importer: require("node-sass-tilde-importer")
                }
            ).on("error",
                (error) =>
                {
                    console.log(
                        JSON.stringify(
                            {
                                status: error.status,
                                file: error.file,
                                line: error.line,
                                column: error.column,
                                message: error.messageOriginal,
                                formatted: error.formatted
                            },
                            null,
                            4));
                })
        ).pipe(
            rename(
                (parsedPath) =>
                {
                    parsedPath.dirname = "";
                    parsedPath.basename = "mantra";
                })
        ).pipe(
            gulp.dest(
                settings.StylePath(),
                settings.Debug ?
                    {
                        sourcemaps: true
                    } :
                    undefined)
        ).on(
            "end",
            () =>
            {
                if (settings.Watch)
                {
                    console.log("Rebuilding scss-code finished.");
                }
            });
}

Theme.description = "Builds the theme.";

/**
 * Builds the templates.
 *
 * @returns
 * The pipeline to execute.
 */
export function Templates(): NodeJS.ReadWriteStream
{
    return gulp.src(
        settings.TemplateSource("**")).pipe(
            gulp.dest(settings.TemplatePath()));
}

Templates.description = "Builds the templates.";

/**
 * Stops a watch-task.
 */
export async function Stop(): Promise<void>
{
    try
    {
        await new Promise(
            (resolve, reject) =>
            {
                let client = new Socket();

                client.connect(
                    watchConnectorPort,
                    "localhost",
                    async () =>
                    {
                        client.write(`stop -t ${settings.Target}`);
                    });

                client.on("close", resolve);
                client.on("error", reject);
            });
    }
    catch
    {
        log.info("The specified task is not running.");
    }
}

Stop.description = "Stops a watch-task";