#! /usr/bin/env node

var jarName = "osgifx.jar";
var mainClass = "{{MAIN_CLASS}}";
var classPath = "{{CLASSPATH}}";
var port = "0";
var warPath = "";
var javaVersionString = "17";
var tryJavaHomeFirst = false;
var javafx = false;
var bundleType = 'jre';
if ('true' === 'true') {
    javafx = true;
}
if ('{{JDK}}' === 'true') {
    bundleType = 'jdk';
}

var jdk = (bundleType === 'jdk');
var jdkProvider = 'zulu';


function njreWrap() {
    'use strict'

    const path = require('path')
    const fs = require('fs')
    const os = require('os')
    const crypto = require('crypto')
    const fetch = require('node-fetch')
    const yauzl = require('yauzl')
    const tar = require('tar')

    function createDir (dir) {
      return new Promise((resolve, reject) => {
        fs.access(dir, err => {
          if (err && err.code === 'ENOENT') {
            fs.mkdir(dir, err => {
              if (err) reject(err)
              resolve()
            })
          } else if (!err) resolve()
          else reject(err)
        })
      })
    }

    function download (dir, url) {
        if (url.indexOf("?") > 0 || jdkProvider === 'zulu') {
            var ext = ".zip";
            switch (process.platform) {
                case 'linux':
                    ext = ".tar.gz";
                    break;
            }
            var destName = bundleType + ext;
        } else {
            destName = path.basename(url);
        }

      return new Promise((resolve, reject) => {
        createDir(dir)
          .then(() => fetch(url))
          .then(response => {
            const destFile = path.join(dir, destName)
            const destStream = fs.createWriteStream(destFile)
            response.body.pipe(destStream).on('finish', () => resolve(destFile))
          })
          .catch(err => reject(err))
      })
    }

    function downloadAll (dir, url) {
      return download(dir, url + '.sha256.txt').then(() => download(dir, url))
    }

    function genChecksum (file) {
      return new Promise((resolve, reject) => {
        fs.readFile(file, (err, data) => {
          if (err) reject(err)

          resolve(
            crypto
              .createHash('sha256')
              .update(data)
              .digest('hex')
          )
        })
      })
    }

    function verify (file) {
      return new Promise((resolve, reject) => {
        fs.readFile(file + '.sha256.txt', 'utf-8', (err, data) => {
          if (err) reject(err)

          genChecksum(file).then(checksum => {
            checksum === data.split('  ')[0]
              ? resolve(file)
              : reject(new Error('File and checksum don\'t match'))
          })
        })
      })
    }

    function move (file) {
      return new Promise((resolve, reject) => {
        const jdeployDir = path.join(os.homedir(), '.jdeploy');
        if (!fs.existsSync(jdeployDir)) {
            fs.mkdirSync(jdeployDir);
        }

        var jreDir = path.join(jdeployDir, bundleType);
        if (!fs.existsSync(jreDir)) {
            fs.mkdirSync(jreDir);
        }
        var vs = javaVersionString;
        if (javafx) {
            vs += 'fx';
        }
        jreDir = path.join(jreDir, vs);
        if (!fs.existsSync(jreDir)) {
            fs.mkdirSync(jreDir);
        }
        const newFile = path.join(jreDir, file.split(path.sep).slice(-1)[0])
        //console.log("Copying file "+file+" to "+newFile);
        fs.copyFile(file, newFile, err => {
          if (err) reject(err)

          fs.unlink(file, err => {
            if (err) reject(err)
            resolve(newFile)
          })
        })
      })
    }

    function extractZip (file, dir) {
        //console.log("Extracting "+file+" to "+dir);
      return new Promise((resolve, reject) => {
        yauzl.open(file, { lazyEntries: true }, (err, zipFile) => {
          if (err) reject(err)

          zipFile.readEntry()
          zipFile.on('entry', entry => {
            const entryPath = path.join(dir, entry.fileName)

            if (/\/$/.test(entry.fileName)) {
              fs.mkdir(entryPath, { recursive: true }, err => {
                if (err && err.code !== 'EEXIST') reject(err)

                zipFile.readEntry()
              })
            } else {
              zipFile.openReadStream(entry, (err, readStream) => {
                if (err) reject(err)

                readStream.on('end', () => {
                  zipFile.readEntry()
                })
                readStream.pipe(fs.createWriteStream(entryPath))
              })
            }
          })
          zipFile.once('close', () => {
            fs.unlink(file, err => {
              if (err) reject(err)
              resolve(dir)
            })
          })
        })
      })
    }

    function extractTarGz (file, dir) {
      return tar.x({ file: file, cwd: dir }).then(() => {
        return new Promise((resolve, reject) => {
          fs.unlink(file, err => {
            if (err) reject(err)
            resolve(dir)
          })
        })
      })
    }

    function extract (file) {
        var dirString = jdk? 'jdk' : 'jre';

      const dir = path.join(path.dirname(file), dirString)
        //console.log("About to extract "+file+" to "+dir);
      return createDir(dir).then(() => {
        return path.extname(file) === '.zip'
          ? extractZip(file, dir)
          : extractTarGz(file, dir)
      })
    }

    /**
     * Installs a JRE copy for the app
     * @param {number} [version = 8] - Java Version (`8`/`9`/`10`/`11`/`12`)
     * @param {object} [options] - Installation Options
     * @param {string} [options.os] - Operating System (defaults to current) (`windows`/`mac`/`linux`/`solaris`/`aix`)
     * @param {string} [options.arch] - Architecture (defaults to current) (`x64`/`x32`/`ppc64`/`s390x`/`ppc64le`/`aarch64`/`sparcv9`)
     * @param {string} [options.openjdk_impl = hotspot] - OpenJDK Implementation (`hotspot`/`openj9`)
     * @param {string} [options.release = latest] - Release
     * @param {string} [options.type = jre] - Binary Type (`jre`/`jdk`)
     * @param {string} [options.heap_size] - Heap Size (`normal`/`large`)
     * @return Promise<string> - Resolves to the installation directory or rejects an error
     * @example
     * const njre = require('njre')
     *
     * // Use default options
     * njre.install()
     *   .then(dir => {
     *     // Do stuff
     *   })
     *   .catch(err => {
     *     // Handle the error
     *   })
     *
     * // or custom ones
     * njre.install(11, { os: 'aix', arch: 'ppc64', openjdk_impl: 'openj9' })
     *   .then(dir => {
     *     // Do stuff
     *   })
     *   .catch(err => {
     *     // Handle the error
     *   })
     */
    function install (version = 11, options = {}) {
      const { openjdk_impl = 'hotspot', release = 'latest', type = 'jre', javafx = false, provider = 'zulu' } = options
      options = { ...options, openjdk_impl, release, type }

      if (provider === 'zulu') {
          return installZulu(version, options);
      }

      let url = 'https://api.adoptopenjdk.net/v2/info/releases/openjdk' + version + '?'

      if (!options.os) {
        switch (process.platform) {
          case 'aix':
            options.os = 'aix'
            break
          case 'darwin':
            options.os = 'mac'
            break
          case 'linux':
            options.os = 'linux'
            break
          case 'sunos':
            options.os = 'solaris'
            break
          case 'win32':
            options.os = 'windows'
            break
          default:
            return Promise.reject(new Error('Unsupported operating system'))
        }
      }
      if (!options.arch) {
          if (options.os == 'mac') {
              // For now, for compatibility reasons use x64 always
              options.arch = 'x64';
          } else if (/^ppc64|s390x|x32|x64$/g.test(process.arch)) options.arch = process.arch
            else if (process.arch === 'ia32') options.arch = 'x32'
            else return Promise.reject(new Error('Unsupported architecture'))
      }

      Object.keys(options).forEach(key => { url += key + '=' + options[key] + '&' })

      const tmpdir = path.join(os.tmpdir(), 'njre')

      return fetch(url)
        .then(response => response.json())
        .then(json => downloadAll(tmpdir, json.binaries[0]['binary_link']))
        .then(verify)
        .then(move)
        .then(extract)
    }

    function installZulu(version = 11, options = {}) {
         const { type = 'jre', javafx = false } = options
         var q = {

            java_version: version,
            ext: 'zip',
            bundle_type: type,
            javafx: ''+javafx,
            arch: 'x86',
            hw_bitness: '64',

         };


         var zuluBaseURL = "https://api.azul.com/zulu/download/community/v1.0/bundles/latest/binary?"
         if (!options.os) {
             switch (process.platform) {

               case 'darwin':
                 q.os = 'macos'
                 break
               case 'linux':
                 q.os = 'linux'
                 q.ext = 'tar.gz'
                 break

               case 'win32':
               case 'win64':
                 q.os = 'windows'
                 break
               default:
                 return Promise.reject(new Error('Unsupported operating system'))
             }
           }


         	var url = zuluBaseURL;
                Object.keys(q).forEach(key => { url += key + '=' + q[key] + '&' })
             const tmpdir = path.join(os.tmpdir(), 'njre')
            //console.log("Downloading "+url);
              return download(tmpdir, url)
                .then(move)
                .then(extract)

    }

    return {install:install};



}


var fs = require('fs');
var os = require('os');
var path = require('path');
const njre = njreWrap();
const targetJavaVersion = parseInt(javaVersionString);
var shell = require("shelljs/global");
function getJdeploySupportDir() {
    return os.homedir() + path.sep + ".jdeploy";
}

function getJavaVersion(binPath) {

    var oldPath = env['PATH'];
    if (binPath) {
        env['PATH'] = binPath + path.delimiter + env['PATH'];
    }

    try {
        var javaVersionProc = exec('java  -version', {silent:true});
        if (javaVersionProc.code !== 0) {
            return false;
        }
        var stdout = javaVersionProc.stderr;
        var regexp = /version "(.*?)"/;
        var match = regexp.exec(stdout);
        var parts = match[1].split('.');
        var join = '.';
        var versionStr = '';
        parts.forEach(function(v) {
            versionStr += v;
            if (join !== null) {
                versionStr += join;
                join = null;
            }
        });
        versionStr = versionStr.replace('_', '');
        return parseFloat(versionStr);
    } catch (e) {
        return false;
    } finally {
        env['PATH'] = oldPath;
    }
}
var getDirectories = dirPath => fs.readdirSync(dirPath).filter(
    file => fs.statSync(path.join(dirPath, file)).isDirectory()
  );

function getJavaHomeInPath(basepath) {

    var dirs = null;
    try {
        dirs = getDirectories(basepath);
    } catch (e) {
        return null;
    }
    if (dirs && dirs.length > 0) {
        basepath = path.join(basepath, dirs[0]);
        if (os.platform() != 'darwin') {
            return basepath;
        }
        if (fs.existsSync(path.join(basepath, 'Contents', 'Home'))) {
            return path.join(basepath, 'Contents', 'Home');
        }

        var adapterDirectories = getDirectories(basepath).filter(subdir => {
            return subdir.match(/^zulu/) && fs.existsSync(path.join(basepath, subdir, 'Contents', 'Home'));
        });

        if (adapterDirectories && adapterDirectories.length > 0) {
            return path.join(basepath, adapterDirectories[0], 'Contents', 'Home');
        }
    }
    return null;
}

function findSupportedRuntime(javaVersion, jdk, javafx) {
    var jdeployDir = path.join(os.homedir(), ".jdeploy");
    var JAVA_HOME_OVERRIDE = env['JDEPLOY_JAVA_HOME_OVERRIDE'];

    if (JAVA_HOME_OVERRIDE && fs.existsSync(JAVA_HOME_OVERRIDE)) {
        return JAVA_HOME_OVERRIDE;
    }

    // First check for the full-meal deal
    var _javaHomePath = getJavaHomeInPath(path.join(jdeployDir, 'jdk', javaVersion+'fx', 'jdk'));
    if (_javaHomePath && fs.existsSync(_javaHomePath)) {
        return _javaHomePath;
    }
    if (!javafx) {
        var _javaHomePath = getJavaHomeInPath(path.join(jdeployDir, 'jdk', javaVersion, 'jdk'));
        if (_javaHomePath && fs.existsSync(_javaHomePath)) {
            return _javaHomePath;
        }
    }

    if (!jdk) {
        var _javaHomePath = getJavaHomeInPath(path.join(jdeployDir, 'jre', javaVersion+'fx', 'jre'));
        if (_javaHomePath && fs.existsSync(_javaHomePath)) {
            return _javaHomePath;
        }
    }

    if (!jdk && !javafx) {
        var _javaHomePath = getJavaHomeInPath(path.join(jdeployDir, 'jre', javaVersion, 'jre'));
        if (_javaHomePath && fs.existsSync(_javaHomePath)) {
            return _javaHomePath;
        }
    }
    return null;

}

function getEmbeddedJavaHome() {
    var _platform = os.platform();
    var _driver = '';
    switch (_platform) {
      case 'darwin': _platform = 'macosx'; _driver = 'Contents' + path.sep + 'Home'; break;
      case 'win32': _platform = 'windows'; _driver = ''; break;
      case 'linux': _driver = ''; break;
      default:
        fail('unsupported platform: ' + _platform);
    }
    var vs = javaVersionString;
    if (javafx) {
        vs += 'fx';
    }
    var typeDir = jdk ? 'jdk' : 'jre';

    var jreDir = path.join(os.homedir(), '.jdeploy',  'jre', vs, 'jre');
    try {
        var out = jreDir + path.sep + getDirectories(jreDir)[0] + (_driver ? (path.sep + _driver) : '');
        return out;
    } catch (e) {
        return null;
    }
}

function javaVersionMatch(v1, v2) {
    if (v1 === 8) v1 = 1.8;
    if (v2 === 8) v2 = 1.8;
    if (Math.floor(v1) !== Math.floor(v2)) {

        return false;
    }
    if (v1 < 2) {
        // Up to 1.8, the version would be like 1.7, 1.8, etc..
        // So we need to check the minor version for equivalency
        return (Math.floor(v1*10) === Math.floor(v2*10));
    } else {
        // Starting with Java 9, the version is like 9, 10, 11, etc..
        // so we just compare major version.
        return (Math.floor(v1) === Math.floor(v2));
    }

}

var done = false;
if (tryJavaHomeFirst) {
    if (env['JAVA_HOME']) {
        var javaHomeVersion = getJavaVersion(path.join(env['JAVA_HOME'], 'bin'));
        if (javaVersionMatch(javaHomeVersion, targetJavaVersion)) {
            done = true;
            env['PATH'] = path.join(env['JAVA_HOME'], 'bin') + path.delimiter + env['PATH'];
            run(env['JAVA_HOME']);

        }
    }

    if (!done) {
        var javaVersion = getJavaVersion();
        if (javaVersionMatch(javaVersion, targetJavaVersion)) {
            done = true;
            run();
        }
    }
}


if (!done) {

    var _javaHome = findSupportedRuntime(javaVersionString, bundleType === 'jdk', javafx);
    if (_javaHome && fs.existsSync(_javaHome)) {
        var javaVersion = getJavaVersion(path.join(_javaHome, 'bin'));
        if (javaVersionMatch(javaVersion, targetJavaVersion)) {
            env['PATH'] = path.join(_javaHome, 'bin') + path.delimiter + env['PATH'];
            env['JAVA_HOME'] = _javaHome;
            done = true;
            run(_javaHome);
        }
    }

}

if (!done) {
    console.log("Downloading java runtime environment for version "+targetJavaVersion);
    njre.install(targetJavaVersion, {type: bundleType, javafx: javafx}).then(function(dir) {
        var _javaHome = getJavaHomeInPath(dir);
        if (_javaHome == null)

        if (!_javaHome || !fs.existsSync(_javaHome)) {
            throw new Error("After install, could not find java home at "+_javaHome);
        }
        env['JAVA_HOME'] = _javaHome;

        var javaBinary = path.join(_javaHome, 'bin', 'java');
        if (!fs.existsSync(javaBinary)) {
            javaBinary += '.exe';

        }
        fs.chmodSync(javaBinary, 0755);

        env['PATH'] = path.join(env['JAVA_HOME'], 'bin') + path.delimiter + env['PATH'];

        run(env['JAVA_HOME']);
    }).catch(function(err) {
        console.log("Failed to install JRE", err);
    });
}




function run(_javaHome) {
    var fail = reason => {
        console.error(reason);
        process.exit(1);
    };


    classPath = classPath.split(':');
    var classPathStr = '';
    var first = true;
    classPath.forEach(function(part) {
        if (!first) classPathStr += path.delimiter;
        first = false;
        classPathStr += __dirname + '/' + part;
    });
    classPath = classPathStr;

    var userArgs = process.argv.slice(2);
    var javaArgs = [];
    javaArgs.push('-Djdeploy.base='+__dirname);
    javaArgs.push('-Djdeploy.port='+port);
    javaArgs.push('-Djdeploy.war.path='+warPath);
    var programArgs = [];
    userArgs.forEach(function(arg) {
        if (arg.startsWith('-D') || arg.startsWith('-X')) {
            javaArgs.push(arg);
        } else {
            programArgs.push(arg);
        }
    });
    var cmd = 'java';

    if (!_javaHome) {
        env['PATH'] = path.join(getEmbeddedJavaHome(), 'bin') + path.delimiter + env['PATH'];
        if (env['JAVA_HOME']) {
            env['PATH'] = env['JAVA_HOME'] + path.sep + 'bin' + path.delimiter + env['PATH'];
        }

    } else {
        env['JAVA_HOME'] = _javaHome;
        cmd = _javaHome + path.sep + 'bin' + path.sep + 'java';
    }

    javaArgs.forEach(function(arg) {
        cmd += ' "'+arg+'"';
    });
    if (jarName !== '{'+'{JAR_NAME}}') {
        cmd += ' -jar "'+__dirname+'/'+jarName+'" ';
    } else {
        cmd += ' -cp "'+classPath+'" '+mainClass+' ';
    }

    programArgs.forEach(function(arg) {
        cmd += ' "'+arg+'"';
    });
    var child = exec(cmd, {async: true});
    process.stdin.setEncoding('utf8');

    process.stdin.on('readable', function() {
      var chunk = null;
      while (null !== (chunk = process.stdin.read())) {
        try {
          child.stdin.write(chunk);
        } catch(e){}
      }
    });
    child.on('close', function(code) {
        process.exit(code);
    });

}
