/*
 * 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 path from "path";
import * as childProcess from "child_process";
import * as fs from "fs";
import * as lib_url from "url";

import * as lib_decompress+ from "decompress";
import * as npmlog from "npmlog";
import * as followRedirects+ from 'follow-redirects';
import {JreInfo, Platform, JRE_INFO_MAP, JAVA_DEFAULT, BIN_DIR, JRE_URL_DIR, JRE_DIR} from 'Globals';
import {detectJavaVersion} from 'JavaVersion';

const decompress: {function(string, string): Promise<any,any>} = lib_decompress.default;
const npmlog_info = (msg : string) => npmlog.info("", msg);

export const JRE_VERSION = 17;





/**
 * Returns an object literal that contains the Java version of the available environment
 * and the boolean flag 'suffices' which is true if n4jsc.jar can be executed using this Java version.
 */
export public function getJavaVersionInfo(log: {function(string)}) : ~Object with {version: int, suffices: boolean} {
    try {
        const version = detectJavaVersion();
        const suffices: boolean = version >= JRE_VERSION;
        return {version: version, suffices: suffices};

    } catch (err) {
        log(err as string);
        return {version: NaN, suffices: false};
    }
}

/**
 * Returns Java command "java".
 * In case the bin folder contains a JRE, this function returns the absolute path
 * to the Java executable.
 */
export function getJavaCommand() : string {
    const jreInfo = getJreInfo();
    if (jreInfo && fs.existsSync(JRE_DIR)) {
        const directories = fs.readdirSync(JRE_DIR, { withFileTypes: true }) as fs.Dirent[];
        const dirNames = directories
            .filter(dirent => dirent.isDirectory())
            .map(dirent => dirent.name) as string[];

        for (const dirName of dirNames) {
            const absJreJavaPath = path.resolve(JRE_DIR, dirName, jreInfo.bin);
            if (fs.existsSync(absJreJavaPath)) {
                return absJreJavaPath;
            }
        }
    }

    return JAVA_DEFAULT;
}

/**
 * Returns true iff the JRE that will be used is installed to the local bin directory of this package.
 */
export function isLocalJre(javaPath: string): boolean {
    return !!javaPath && (javaPath != JAVA_DEFAULT);
}


/**
 * Checks whether Java 17 is available globally. If not checks whether 
 * Java is available in the local bin folder. If not Java JRE is downloaded
 * and extracted to the local bin folder.
 */
export public async function ensureJRE(log: {function(string)} = npmlog_info) : void {
    const java = getJavaCommand();
    if (isLocalJre(java)) {
        log("Local JRE found.");
        return;
    }

    let versionInfo = getJavaVersionInfo(log);
    if (versionInfo.version) {
        if (versionInfo.suffices) {
            log("Global JRE found in version " + versionInfo.version);
            return;
        } else {
            log("Global JRE found in version " + versionInfo.version + " but required version is " + JRE_VERSION + ".");
        }

    } else {
        log("Global JRE not found.");
    }

    // Download and extract JRE
    const jreInfo = getJreInfo();
    if (jreInfo) {
        log("Downloading and extracting JRE (Adopt OpenJDK) into local folder.");
        
        try {
            await downloadJRE(jreInfo, log);
            await extractJRE(jreInfo, log);
            return;
        } catch (err) {
            log(err as string);
        }

    } else {
        log("Platform not supported: " + process.platform);
    }
    log("Please install JRE in version " + JRE_VERSION + " and make the call to 'bin/java' globally available.");
}

function getJreInfo() : JreInfo {
    const platform = process.arch + "_" + process.platform
    	+ (isPlatformProblemMuslLibc() ? "_alpine" : "");
    return JRE_INFO_MAP.get(platform as Platform);
}


/**
 * Downloads the AdoptOpenJDK JRE.
 */
async function downloadJRE(jreInfo: JreInfo, log: {function(string)}) : void {
    const jreUrl = toValidUrl(JRE_URL_DIR + jreInfo.name);
    if (!jreUrl) {
        throw "Invalid URL: " + lib_url.format(jreUrl);
    }
    const output = getJreFileName(jreInfo);
    await downloadFile(jreUrl, output, log);
}


export public function toValidUrl(urlStr: string) : lib_url.Url {
    try {
        const nodeURL = lib_url.parse(urlStr);
        if (nodeURL.protocol && nodeURL.host && nodeURL.path) {
            return nodeURL;
        }
    } catch (_) {
    }
    return null;
}

export public async function downloadFile(url: lib_url.Url, filename: string, log: {function(string)}) : void {
    log("downloading from " + lib_url.format(url));

    const dir = path.dirname(filename);
    if (!fs.existsSync(dir)) {
        log("creating dir " + dir);
        fs.mkdirSync(dir);
    }
    if (fs.existsSync(filename)) {
        log("deleting already existing file " + filename);
        fs.unlinkSync(filename);
    }

    const get = (url.protocol == "https:") ? followRedirects.https.get : followRedirects.http.get;
    await new Promise<any, any>((resolve, reject) => {
            get(url, (response : any+) => {
                    if (response.statusCode !== 200) {
                        reject("Error during download: status code was " + response.statusCode);

                    } else {
                        const file = fs.createWriteStream(filename);
                        response.pipe(file);
                        log("receiving file ...");
    
                        response.on("error",
                            err => {
                                log(err as string);
                                reject(err);
                            });
                        response.on("end",
                            () => {
                                log("File written to " + filename);
                                resolve();
                            });
                    }
                }).on("error", (err) => {
                    log(err as string);
                    reject(err);
                });
        });
}

/** 
 * Extracts the downloaded JRE to the folder bin/JRE_LOCAL_DIR
 */
async function extractJRE(jreInfo: JreInfo, log: {function(string)}) {
    if (fs.existsSync(JRE_DIR)) {
        log("Cleaning " + JRE_DIR);
        fs.rmdirSync(JRE_DIR, {recursive: true});
    }

    const jreFileName = getJreFileName(jreInfo);
    await decompress(jreFileName, JRE_DIR);
    log("JRE extracted to " + JRE_DIR);
}

function getJreFileName(jreInfo: JreInfo): string {
    return path.resolve(BIN_DIR, jreInfo.name);
}


function isPlatformProblemMuslLibc() : boolean {
    const platform = process.arch + "_" + process.platform;
    if (platform == Platform.x64_linux && isMuslLibc()) {
        return true;
    }
    return false;
}

/** Returns true if the installed libc is musl */
function isMuslLibc() {
    const output = childProcess.spawnSync('ldd', ['--version']).stderr.toString('utf8');
    if (output.indexOf('musl') > -1) {
        return true;
    }
    return false;
}
