/*
 * Copyright (c) 2019 NumberFour AG.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   NumberFour AG - Initial API and implementation
 */


import * as childProcess from "child_process";
import * as path from "path";
import * as fs from "fs";
import * as url from "url";
import npmlog from "npmlog";
import debug from "debug";
import {JAVA_VM_XMX, N4JS_CLI_CONFIG, BIN_DIR} from "Globals";
import {getJavaCommand, downloadFile, toValidUrl} from "JreProvider";
import {N4jscArguments} from "N4jscArguments";


const debugLog = debug("n4js-cli");
const logFn: {function(string)} = (text: string) => {debugLog(text);};
const npmlog_info = (msg : string) => npmlog.info("", msg);

@StringBased
export public enum Goal {
    compile,
    clean,
    lsp,
    api,
    watch,
    setversions : "set-versions",
    init
}

export public class Options {
    public version?: boolean;
    public help?: boolean;
    /** Clean the output folder before compiling. */
    public clean?: boolean;
    public noPersist?: boolean;
    public noTests?: boolean;
    public showSetup?: boolean;
    public stdio?: boolean;
    public testOnly?: boolean;
    public verbose?: boolean;
    public maxErrs?: int;
    public maxWarns?: int;
    public port?: int;
    /** Options of goal init */
    public yes?: boolean;
    public answers?: string;
    public scope?: boolean;
    public workspaces?: string;
    public n4js?: boolean;
}

export public class VmOptions {
    public xmx?: string;
}



/**
 * Version of #runN4jsc() but with typed parameters.
 */
export public function n4jsc(
        goal: Goal,
        dir: string=,
        options: ~Options+ =,
        vmOptions: ~VmOptions+ =,
        log: {function(string)} = logFn) : Promise<int, ?> {

    // const args = getArgStrings(goal, dir, options, vmOptions, log);
    const args = N4jscArguments.fromParameters(goal, dir, options, vmOptions, log);
    return runN4jsc(args, logFn);
}

/**
 * Runs N4jsc in a blocking fashion by executing n4jsc.jar with Java.
 * In case the bin folder contains a JRE, it will be used. Otherwise,
 * the globally available Java will be used. Java XmX option will be
 * set to 4GB if not set already.
 * The arguments will be collected from process.argv
 *
 * Used from bin/n4jsc.js
 */
 export public async function runN4jscSync(log: {function(string)} = npmlog_info) {
    const args = N4jscArguments.fromProcess(log);
    try {
        let exitCode : int = await runN4jsc(args, log);
        process.exit(exitCode);
    } catch (error) {
        if (typeof error == 'number') {
            process.exit(error as number);
        } else {
            process.exit(1);
        }
    }
}

/**
 * Runs N4jsc by executing n4jsc.jar with Java and returns a promise.
 * In case the bin folder contains a JRE, it will be used. Otherwise,
 * the globally available Java will be used. Java XmX option will be
 * set to 4GB if not set already.
 */
export public function runN4jsc(args: N4jscArguments, log: {function(string)} = npmlog_info) : Promise<int, ?> {
    let spawnOptions: childProcess.SpawnOptions = { stdio: "inherit", env: Object.assign({ NODEJS_PATH: process.argv[0] }, process.env)};
    let n4jscProcess: Promise<childProcess.ChildProcess,?> = createN4JSProcess(args, spawnOptions, log);
    let promise: Promise<int, ?> = new Promise<int, int>((resolve, reject) => {
        n4jscProcess.then((childProcess) => {
            childProcess.on("close", (code: int) => {
                if (code === 0) {
                    resolve(0);
                } else {
                    reject(code);
                }
            });
        });
    });

    return promise;
}

/**
 * Version of #createN4JSProcess() but with typed parameters.
 *
 * Used from n4js-vscode-extension
 */
export public async function n4jscProcess(
        goal: Goal,
        dir: string,
        options: ~Options+,
        vmOptions: ~VmOptions+,
        spawnOptions: childProcess.SpawnOptions,
        log: {function(string)}) : childProcess.ChildProcess {

    //const args = getArgStrings(goal, dir, options, vmOptions, log);
    const args = N4jscArguments.fromParameters(goal, dir, options, vmOptions, log);
    return await createN4JSProcess(args, spawnOptions, log);
}

/**
 * Creates and returns a process to execute the n4jsc.jar with Java.
 * In case the bin folder contains a JRE, it will be used. Otherwise,
 * the globally available Java will be used. Java Xmx vm option will
 * overwritten iff defined in environment variable JAVA_TOOL_OPTIONS.
 */
export public async function createN4JSProcess(args: N4jscArguments, spawnOptions: childProcess.SpawnOptions, log: {function(string)} = npmlog_info) : childProcess.ChildProcess {
    const modifedArgs = await modifyArgs(args, log);
    const java = getJavaCommand();
    return childProcess.spawn(java, modifedArgs.toArray(), spawnOptions);
}



async function modifyArgs(args: N4jscArguments, log: {function(string)}): N4jscArguments {
    // set java option -Xmx
    const envJavaToolOptions: string = process.env.JAVA_TOOL_OPTIONS;
    const match = envJavaToolOptions?.match(/-xmx(?<size>[\S]*)/);
    const size = match?.groups?.size;
    if (size) {
        let vmXmxSet = false;
        for (let i=0; i<args.vmArgs.length && !vmXmxSet; i++) {
            const arg = args.vmArgs[i];
            if (arg.startsWith(JAVA_VM_XMX)) {
                args.vmArgs[i] = JAVA_VM_XMX + size;
                vmXmxSet = true;
            }
        }
        if (!vmXmxSet) {
            args.vmArgs.push(JAVA_VM_XMX + size);
        }
    }

    // replace n4jsc.jar (and download if not available)
    try {
        const n4jscJarName = await findN4jscJarPath(args.dir, log);
        if (n4jscJarName) {
            log("using n4jsc.jar: " + n4jscJarName);
            args.n4jscJar = n4jscJarName;
        }
    } catch (e) {
        log("Failed to use jar specified in n4js-cli.config. Using default jar instead.");
    }

    // lsp goal forbids dir argument, but n4js-cli uses this argument to support n4js-cli configuration files
    if (Goal.lsp == args.goal) {
        args.dir = undefined;
    }

    // set verbose flag
    if (debugLog.enabled) {
        args.n4jscOptions.push("--verbose");
    }

    return args;
}

/**
 * Returns the path of the n4jsc.jar in case this was configured inside n4js-cli config file.
 * Otherwise returns null.
 */
export public async function findN4jscJarPath(workingDir: string, log: {function(string)}) : string {
    const n4jscJarValue = findN4jscJarValue(workingDir, log);
    if (n4jscJarValue) {
        const n4jscJarName = await ensureCustomN4jscJar(n4jscJarValue, log);
        return n4jscJarName;
    }
    return null;
}

/**
 * Returns either:
 * - the value of the environment variable 'N4JSC_JAR',
 * - the value of property 'n4jscJar' of the n4js-cli configuration file,
 * - or null.
 */
export public function findN4jscJarValue(workingDir: string, log: {function(string)}) : string {
    if (process.env.N4JSC_JAR) {
        // custom n4jsc.jar given as environment variable
        log("picked up environment variable N4JSC_JAR");
        return process.env.N4JSC_JAR;
    }
    if (workingDir && fs.existsSync(workingDir)) {
        // n4js-cli config file exists
        const pathN4jscliConfigFile = findN4jscliConfig(workingDir);
        const customN4jscName = getN4jscJarFromConfig(pathN4jscliConfigFile);
        if (customN4jscName) {
            // custom n4jsc.jar set in n4js-cli configuration file
            log("picked up property 'n4jsc.jar' from config file " + pathN4jscliConfigFile);
            return customN4jscName;
        }
    }
    // use default/shipped jar
    return null;
}

/**
 * Searches the n4js-cli config file. The search looks for the config file at the following places:
 * (1) in the given folder,
 * (2) in all parent folders,
 * (3) in all direct sub folders (not recursively).
 * The first occurrence is returned, or null otherwise.
 */
export public function findN4jscliConfig(workingDir : string) : string {
    const dir = path.resolve(workingDir);

    // search in parent folders
    for (let tmpDir = dir, lastDir = null; tmpDir != lastDir; lastDir = tmpDir, tmpDir = path.basename(tmpDir)) {
        const pathN4jscliConfig = path.resolve(tmpDir, N4JS_CLI_CONFIG);
        if (fs.existsSync(pathN4jscliConfig)) {
            return pathN4jscliConfig;
        }
    }

    // search in direct child folders (not recursively)
    const files = fs.readdirSync(workingDir);
    for (const file in files) {
        if (file instanceof fs.Dirent && file.isDirectory()) {
            const pathN4jscliConfig = path.resolve(dir, N4JS_CLI_CONFIG);
            if (fs.existsSync(pathN4jscliConfig)) {
                return pathN4jscliConfig;
            }
        }
    }

    return null;
}

function getN4jscJarFromConfig(pathN4jscliConfigFile : string) : string {
    if (pathN4jscliConfigFile) {
        const data = fs.readFileSync(pathN4jscliConfigFile, {encoding : 'utf8'});
        const n4jscliConfig : any+ = JSON.parse(data);
        if (n4jscliConfig && n4jscliConfig["n4jsc.jar"]) {
            return n4jscliConfig["n4jsc.jar"];
        }
    }
    return null;
}

/**
 * Returns the complete file name of the given custom n4jsc.jar property.
 * Also ensures the presence of the given jar.
 */
async function ensureCustomN4jscJar(customN4jscValue: string, log: {function(string)}) : string {
    if (fs.existsSync(customN4jscValue)) {
        // a local file
        return customN4jscValue;
    }

    const nodeURL = toValidUrl(customN4jscValue);
    if (nodeURL) {
        const dfNames = splitCustomN4jscURLValue(nodeURL);
        if (dfNames) {
            // it is an URL
            const jarInFolderInBinDir = path.resolve(BIN_DIR, dfNames.dirName, dfNames.fileName);
            if (fs.existsSync(jarInFolderInBinDir)) {
                return jarInFolderInBinDir;
            }
            // file not yet downloaded
            try {
                await downloadFile(nodeURL, jarInFolderInBinDir, log);
                if (fs.existsSync(jarInFolderInBinDir)) {
                    // should be now inside bin directory
                    return jarInFolderInBinDir;
                }
            } catch (e) {
                log(e as string);
            }
        }

    } else {
        const fileName = path.basename(customN4jscValue);
        const jarInBinDir = path.resolve(BIN_DIR, fileName);
        if (fs.existsSync(jarInBinDir)) {
            // a local file inside bin directory
            return jarInBinDir;
        }
    }

    throw new Error(`n4jsc.jar could not be found: ${customN4jscValue}. Check n4js-cli config file or environment variable.`);
}


function splitCustomN4jscURLValue(nodeURL: url.Url) : ~Object with {dirName:string, fileName:string} {
    const fileName = removeReservedCharacters(path.basename(nodeURL.pathname));
    const dirName = getDirName(nodeURL);
    return {dirName:dirName, fileName:fileName};
}



function getDirName(nodeURL: url.Url) : string {
    let dirName = nodeURL.hostname + path.dirname(nodeURL.pathname);
    dirName = dirName.replace(/\/|\\/g, "."); // replace separators by '.'
    dirName = removeReservedCharacters(dirName);
    return dirName;
}

function removeReservedCharacters(input: string) : string {
    return input.replace(/<|>|:|"|\||\?|\*|&|/g, ""); // remove reserved characters
}
