"use strict";
/*
 * Copyright 2017 Teppo Kurki <teppo.kurki@iki.fi>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const child_process_1 = require("child_process");
const debug_1 = __importDefault(require("debug"));
const fs_1 = __importDefault(require("fs"));
const node_fetch_1 = __importDefault(require("node-fetch"));
const debug = debug_1.default('signalk:modules');
const lodash_1 = __importDefault(require("lodash"));
const path_1 = __importDefault(require("path"));
const semver_1 = __importStar(require("semver"));
function findModulesInDir(dir, keyword) {
    // If no directory by name return empty array.
    if (!fs_1.default.existsSync(dir)) {
        return [];
    }
    debug('findModulesInDir: ' + dir);
    return fs_1.default
        .readdirSync(dir)
        .filter(name => name !== '.bin')
        .reduce((result, filename) => {
        if (filename.indexOf('@') === 0) {
            return result.concat(findModulesInDir(dir + filename + '/', keyword).map(entry => {
                return {
                    module: entry.module,
                    metadata: entry.metadata,
                    location: dir
                };
            }));
        }
        else {
            let metadata;
            try {
                metadata = require(dir + filename + '/package.json');
            }
            catch (err) {
                debug(err);
            }
            if (metadata &&
                metadata.keywords &&
                metadata.keywords.includes(keyword)) {
                result.push({
                    module: metadata.name,
                    metadata,
                    location: dir
                });
            }
        }
        return result;
    }, []);
}
// Extract unique directory paths from app object.
function getModulePaths(app) {
    // appPath is the app working directory.
    const { appPath, configPath } = app.config;
    return (appPath === configPath
        ? [appPath]
        : [configPath, appPath]).map(pathOption => path_1.default.join(pathOption, 'node_modules/'));
}
const getModuleSortName = (x) => (x.module || '').replace('@signalk', ' ');
// Sort handler that puts strings with '@signalk' first.
const priorityPrefix = (a, b) => getModuleSortName(a).localeCompare(getModuleSortName(b));
// Searches for installed modules that contain `keyword`.
function modulesWithKeyword(app, keyword) {
    return lodash_1.default.uniqBy(
    // _.flatten since values are inside an array. [[modules...], [modules...]]
    lodash_1.default.flatten(getModulePaths(app).map(pathOption => findModulesInDir(pathOption, keyword))), moduleData => moduleData.module).sort(priorityPrefix);
}
function installModule(app, name, version, onData, onErr, onClose) {
    runNpm(app, name, version, 'install', onData, onErr, onClose);
}
function removeModule(app, name, version, onData, onErr, onClose) {
    runNpm(app, name, null, 'remove', onData, onErr, onClose);
}
function restoreModules(app, onData, onErr, onClose) {
    runNpm(app, null, null, 'remove', onData, onErr, onClose);
}
function runNpm(app, name, version, command, onData, onErr, onClose) {
    let npm;
    const opts = {};
    let packageString;
    if (name) {
        packageString = version ? `${name}@${version}` : name;
    }
    else {
        packageString = '';
    }
    debug(`${command}: ${packageString}`);
    if (isTheServerModule(name, app)) {
        if (process.platform === 'win32') {
            npm = child_process_1.spawn('cmd', ['/c', `npm ${command} -g --unsafe-perm ${packageString} `], opts);
        }
        else {
            npm = child_process_1.spawn('sudo', ['npm', command, '-g', '--unsafe-perm', packageString], opts);
        }
    }
    else {
        opts.cwd = app.config.configPath;
        if (process.platform === 'win32') {
            npm = child_process_1.spawn('cmd', ['/c', `npm --save ${command} ${packageString}`], opts);
        }
        else {
            npm = child_process_1.spawn('npm', ['--save', command, packageString], opts);
        }
    }
    npm.stdout.on('data', onData);
    npm.stderr.on('data', onErr);
    npm.on('close', onClose);
    npm.on('error', (err) => {
        onErr(err);
        onClose(-1);
    });
}
function isTheServerModule(moduleName, app) {
    return moduleName === app.config.name;
}
function findModulesWithKeyword(keyword) {
    return new Promise((resolve, reject) => {
        let errorCount = 0;
        let resultCount = 0;
        const result = {};
        const handleResultWithTimeout = (fetchResult) => {
            fetchResult
                .then(r => r.json())
                .then(parsed => {
                const data = parsed.results || parsed.objects || [];
                data.reduce((acc, module) => {
                    const name = module.package.name;
                    if (!acc[name] ||
                        semver_1.default.gt(module.package.version, acc[name].package.version)) {
                        acc[name] = module;
                    }
                    return acc;
                }, result);
                if (resultCount++ || errorCount) {
                    resolve(lodash_1.default.values(result));
                }
                else {
                    setTimeout(() => resolve(lodash_1.default.values(result)), Number(process.env.NPMREGISTRYTIMEOUT) || 20 * 1000);
                }
            })
                .catch(e => {
                if (errorCount++) {
                    reject(e);
                }
            });
        };
        [
            node_fetch_1.default(`https://api.npms.io/v2/search?size=250&q=keywords:${keyword}+not:deprecated`),
            node_fetch_1.default('http://registry.npmjs.org/-/v1/search?size=250&text=keywords:' +
                keyword)
        ].forEach(handleResultWithTimeout);
    });
}
function doFetchDistTags() {
    return node_fetch_1.default('http://registry.npmjs.org/-/package/signalk-server/dist-tags');
}
function getLatestServerVersion(currentVersion, distTags = doFetchDistTags) {
    return new Promise((resolve, reject) => {
        distTags()
            .then(npmjsResults => npmjsResults.json())
            .then(npmjsParsed => {
            const prereleaseData = semver_1.default.prerelease(currentVersion);
            if (prereleaseData) {
                if (semver_1.default.satisfies(npmjsParsed.latest, `>${currentVersion}`)) {
                    resolve(npmjsParsed.latest);
                }
                else {
                    resolve(npmjsParsed[prereleaseData[0]]);
                }
            }
            else {
                resolve(npmjsParsed.latest);
            }
        })
            .catch(reject);
    });
}
function checkForNewServerVersion(currentVersion, serverUpgradeIsAvailable, getLatestServerVersionP = getLatestServerVersion) {
    getLatestServerVersionP(currentVersion)
        .then((version) => {
        if (semver_1.default.satisfies(new semver_1.SemVer(version), `>${currentVersion}`)) {
            serverUpgradeIsAvailable(undefined, version);
        }
    })
        .catch((err) => {
        serverUpgradeIsAvailable(`unable to check for new server version: ${err}`);
    });
}
function getAuthor(thePackage) {
    debug(thePackage.name + ' author: ' + thePackage.author);
    return ((thePackage.author && (thePackage.author.name || thePackage.author.email)) +
        '' +
        (thePackage.contributors || [])
            .map((contributor) => contributor.name || contributor.email)
            .join(',') +
        '' +
        (thePackage.name.startsWith('@signalk/') ? ' (Signal K team)' : ''));
}
module.exports = {
    modulesWithKeyword,
    installModule,
    removeModule,
    isTheServerModule,
    findModulesWithKeyword,
    getLatestServerVersion,
    checkForNewServerVersion,
    getAuthor,
    restoreModules
};
