"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/*
 * Copyright 2016 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.
*/
const debug_1 = __importDefault(require("debug"));
const debug = debug_1.default('signalk:interfaces:plugins');
// @ts-ignore
const logging_1 = require("@signalk/streams/logging");
const express_1 = __importDefault(require("express"));
const fs_1 = __importDefault(require("fs"));
const lodash_1 = __importDefault(require("lodash"));
const path_1 = __importDefault(require("path"));
const constants_1 = require("../constants");
const serialports_1 = require("../serialports");
// tslint:disable-next-line:no-var-requires
const modulesWithKeyword = require('../modules').modulesWithKeyword;
// tslint:disable-next-line:no-var-requires
const put = require('../put');
// tslint:disable-next-line
const _putPath = put.putPath;
// tslint:disable-next-line:no-var-requires
const getModulePublic = require('../config/get').getModulePublic;
// tslint:disable-next-line:no-var-requires
const queryRequest = require('../requestResponse').queryRequest;
// tslint:disable-next-line:no-var-requires
const getMetadata = require('@signalk/signalk-schema').getMetadata;
// #521 Returns path to load plugin-config assets.
const getPluginConfigPublic = getModulePublic('@signalk/plugin-config');
const DEFAULT_ENABLED_PLUGINS = process.env.DEFAULTENABLEDPLUGINS
    ? process.env.DEFAULTENABLEDPLUGINS.split(',')
    : [];
function backwardsCompat(url) {
    return [`${constants_1.SERVERROUTESPREFIX}${url}`, url];
}
module.exports = (theApp) => {
    const onStopHandlers = {};
    return {
        start() {
            startPlugins(theApp);
            ensureExists(path_1.default.join(theApp.config.configPath, 'plugin-config-data'));
            theApp.use(backwardsCompat('/plugins/configure'), express_1.default.static(getPluginConfigPublic(theApp)));
            const router = express_1.default.Router();
            theApp.get(backwardsCompat('/plugins'), (req, res) => {
                const providerStatus = theApp.getProviderStatus();
                Promise.all(lodash_1.default.sortBy(theApp.plugins, [
                    (plugin) => {
                        return plugin.name;
                    }
                ]).map((plugin) => getPluginResponseInfo(plugin, providerStatus)))
                    .then(json => res.json(json))
                    .catch(err => {
                    console.error(err);
                    res.status(500);
                    res.send(err);
                });
            });
        }
    };
    function getPluginResponseInfo(plugin, providerStatus) {
        return new Promise((resolve, reject) => {
            let data = null;
            try {
                data = getPluginOptions(plugin.id);
            }
            catch (e) {
                console.error(e.code + ' ' + e.path);
            }
            if (data && lodash_1.default.isUndefined(data.enabled) && plugin.enabledByDefault) {
                data.enabled = true;
            }
            Promise.all([
                Promise.resolve(typeof plugin.schema === 'function' ? plugin.schema() : plugin.schema),
                Promise.resolve(typeof plugin.uiSchema === 'function'
                    ? plugin.uiSchema()
                    : plugin.uiSchema)
            ])
                .then(([schema, uiSchema]) => {
                const status = providerStatus.find((p) => p.id === plugin.name);
                const statusMessage = status ? status.message : '';
                resolve({
                    id: plugin.id,
                    name: plugin.name,
                    packageName: plugin.packageName,
                    keywords: plugin.keywords,
                    version: plugin.version,
                    description: plugin.description,
                    schema,
                    statusMessage,
                    uiSchema,
                    state: plugin.state,
                    data
                });
            })
                .catch(err => {
                reject(err);
            });
        });
    }
    function ensureExists(dir) {
        if (!fs_1.default.existsSync(dir)) {
            fs_1.default.mkdirSync(dir);
        }
    }
    function pathForPluginId(id) {
        return path_1.default.join(theApp.config.configPath, 'plugin-config-data', id + '.json');
    }
    function dirForPluginId(id) {
        const dirName = path_1.default.join(theApp.config.configPath, 'plugin-config-data', id);
        ensureExists(dirName);
        return dirName;
    }
    function savePluginOptions(pluginId, data, callback) {
        const config = JSON.parse(JSON.stringify(data));
        fs_1.default.writeFile(pathForPluginId(pluginId), JSON.stringify(data, null, 2), callback);
    }
    function getPluginOptions(id) {
        let optionsAsString = '{}';
        try {
            optionsAsString = fs_1.default.readFileSync(pathForPluginId(id), 'utf8');
        }
        catch (e) {
            debug('Could not find options for plugin ' +
                id +
                ', returning empty options: ');
        }
        try {
            const options = JSON.parse(optionsAsString);
            if (optionsAsString === '{}' && DEFAULT_ENABLED_PLUGINS.includes(id)) {
                debug('Override enable for plugin ' + id);
                options.enabled = true;
            }
            if (process.env.DISABLEPLUGINS) {
                debug('Plugins disabled by configuration');
                options.enabled = false;
            }
            debug(optionsAsString);
            return options;
        }
        catch (e) {
            console.error('Could not parse JSON options:' + e.message + ' ' + optionsAsString);
            return {};
        }
    }
    function startPlugins(app) {
        app.plugins = [];
        app.pluginsMap = {};
        modulesWithKeyword(app, 'signalk-node-server-plugin').forEach((moduleData) => {
            registerPlugin(app, moduleData.module, moduleData.metadata, moduleData.location);
        });
    }
    function handleMessageWrapper(app, id) {
        const pluginsLoggingEnabled = lodash_1.default.isUndefined(app.config.settings.enablePluginLogging) ||
            app.config.settings.enablePluginLogging;
        return (providerId, data) => {
            const plugin = app.pluginsMap[id];
            if (!lodash_1.default.isUndefined(plugin) &&
                pluginsLoggingEnabled &&
                plugin.enableLogging) {
                if (!plugin.logger) {
                    plugin.logger = logging_1.getLogger(app, providerId);
                }
                plugin.logger(data);
            }
            app.handleMessage(id, data);
        };
    }
    function getSelfPath(aPath) {
        return lodash_1.default.get(theApp.signalk.self, aPath);
    }
    function getPath(aPath) {
        if (aPath === '/sources') {
            return Object.assign(Object.assign({}, theApp.signalk.retrieve().sources), theApp.deltaCache.getSources());
        }
        else {
            return lodash_1.default.get(theApp.signalk.retrieve(), aPath);
        }
    }
    function putSelfPath(aPath, value, updateCb, source) {
        return _putPath(theApp, 'vessels.self', aPath, { value, source }, null, null, updateCb);
    }
    function putPath(aPath, value, updateCb, source) {
        const parts = aPath.length > 0 ? aPath.split('.') : [];
        if (parts.length <= 2) {
            updateCb(new Error(`Put path begin with a two part context:${aPath}`));
            return;
        }
        const context = `${parts[0]}.${parts[1]}`;
        const skpath = parts.slice(2).join('.');
        return _putPath(theApp, context, skpath, { value, source }, null, null, updateCb);
    }
    function getSerialPorts() {
        return serialports_1.listAllSerialPorts();
    }
    function registerPlugin(app, pluginName, metadata, location) {
        debug('Registering plugin ' + pluginName);
        try {
            doRegisterPlugin(app, pluginName, metadata, location);
        }
        catch (e) {
            console.error(e);
        }
    }
    function stopPlugin(plugin) {
        debug('Stopping plugin ' + plugin.name);
        onStopHandlers[plugin.id].forEach((f) => {
            try {
                f();
            }
            catch (err) {
                console.error(err);
            }
        });
        onStopHandlers[plugin.id] = [];
        plugin.stop();
        theApp.setPluginStatus(plugin.id, 'Stopped');
        debug('Stopped plugin ' + plugin.name);
    }
    function setPluginStartedMessage(plugin) {
        const statusMessage = typeof plugin.statusMessage === 'function'
            ? plugin.statusMessage()
            : undefined;
        if (lodash_1.default.isUndefined(statusMessage) &&
            lodash_1.default.isUndefined(theApp.providerStatus[plugin.id]) &&
            lodash_1.default.isUndefined(plugin.statusMessage)) {
            theApp.setPluginStatus(plugin.id, 'Started');
        }
    }
    function doPluginStart(app, plugin, location, configuration, restart) {
        debug('Starting plugin %s from %s', plugin.name, location);
        try {
            app.setPluginStatus(plugin.id, null);
            if (plugin.enableDebug) {
                app.logging.addDebug(plugin.packageName);
            }
            else {
                app.logging.removeDebug(plugin.packageName);
            }
            let safeConfiguration = configuration;
            if (!safeConfiguration) {
                console.error(`${plugin.id}:no configuration data`);
                safeConfiguration = {};
            }
            plugin.start(safeConfiguration, restart);
            debug('Started plugin ' + plugin.name);
            setPluginStartedMessage(plugin);
        }
        catch (e) {
            console.error('error starting plugin: ' + e);
            console.error(e.stack);
            app.setProviderError(plugin.name, `Failed to start: ${e.message}`);
        }
    }
    function doRegisterPlugin(app, packageName, metadata, location) {
        let plugin;
        let setProviderUseLogged = false;
        const logSetProviderUsage = () => {
            if (!setProviderUseLogged) {
                console.log(`Note: ${plugin.name} is using deprecated setProviderStatus/Error https://github.com/SignalK/signalk-server/blob/master/SERVERPLUGINS.md#appsetproviderstatusmsg`);
                setProviderUseLogged = true;
            }
        };
        const appCopy = lodash_1.default.assign({}, app, {
            getSelfPath,
            getPath,
            putSelfPath,
            queryRequest,
            error: (msg) => {
                console.error(`${packageName}:${msg}`);
            },
            debug: require('debug')(packageName),
            registerDeltaInputHandler: (handler) => {
                onStopHandlers[plugin.id].push(app.registerDeltaInputHandler(handler));
            },
            setProviderStatus: (msg) => {
                logSetProviderUsage();
                app.setPluginStatus(plugin.id, msg);
            },
            setProviderError: (msg) => {
                logSetProviderUsage();
                app.setPluginError(plugin.id, msg);
            },
            setPluginStatus: (msg) => {
                app.setPluginStatus(plugin.id, msg);
            },
            setPluginError: (msg) => {
                app.setPluginError(plugin.id, msg);
            },
            getSerialPorts,
            supportsMetaDeltas: true,
            getMetadata
        });
        appCopy.putPath = putPath;
        try {
            const pluginConstructor = require(path_1.default.join(location, packageName));
            plugin = pluginConstructor(appCopy);
        }
        catch (e) {
            console.error(`${packageName} failed to start: ${e.message}`);
            console.error(e);
            app.setProviderError(packageName, `Failed to start: ${e.message}`);
            return;
        }
        onStopHandlers[plugin.id] = [];
        if (app.pluginsMap[plugin.id]) {
            console.log(`WARNING: found multiple copies of plugin with id ${plugin.id} at ${location} and ${app.pluginsMap[plugin.id].packageLocation}`);
            return;
        }
        appCopy.handleMessage = handleMessageWrapper(app, plugin.id);
        appCopy.savePluginOptions = (configuration, cb) => {
            savePluginOptions(plugin.id, Object.assign(Object.assign({}, getPluginOptions(plugin.id)), { configuration }), cb);
        };
        appCopy.readPluginOptions = () => {
            return getPluginOptions(plugin.id);
        };
        appCopy.getDataDirPath = () => dirForPluginId(plugin.id);
        appCopy.registerPutHandler = (context, aPath, callback, source) => {
            onStopHandlers[plugin.id].push(app.registerActionHandler(context, aPath, source || plugin.id, callback));
        };
        appCopy.registerActionHandler = appCopy.registerPutHandler;
        appCopy.registerHistoryProvider = provider => {
            app.registerHistoryProvider(provider);
            onStopHandlers[plugin.id].push(() => {
                app.unregisterHistoryProvider(provider);
            });
        };
        const startupOptions = getPluginOptions(plugin.id);
        const restart = (newConfiguration) => {
            const pluginOptions = getPluginOptions(plugin.id);
            pluginOptions.configuration = newConfiguration;
            savePluginOptions(plugin.id, pluginOptions, err => {
                if (err) {
                    console.error(err);
                }
                else {
                    stopPlugin(plugin);
                    doPluginStart(app, plugin, location, newConfiguration, restart);
                }
            });
        };
        if (isEnabledByPackageEnableDefault(startupOptions, metadata)) {
            startupOptions.enabled = true;
            startupOptions.configuration = {};
            plugin.enabledByDefault = true;
        }
        plugin.enableDebug = startupOptions.enableDebug;
        plugin.version = metadata.version;
        plugin.packageName = metadata.name;
        plugin.keywords = metadata.keywords;
        plugin.packageLocation = location;
        if (startupOptions && startupOptions.enabled) {
            doPluginStart(app, plugin, location, startupOptions.configuration, restart);
        }
        plugin.enableLogging = startupOptions.enableLogging;
        app.plugins.push(plugin);
        app.pluginsMap[plugin.id] = plugin;
        const router = express_1.default.Router();
        router.get('/', (req, res) => {
            const currentOptions = getPluginOptions(plugin.id);
            const enabledByDefault = isEnabledByPackageEnableDefault(currentOptions, metadata);
            res.json({
                enabled: enabledByDefault || currentOptions.enabled,
                enabledByDefault,
                id: plugin.id,
                name: plugin.name,
                version: plugin.version
            });
        });
        router.post('/config', (req, res) => {
            savePluginOptions(plugin.id, req.body, err => {
                if (err) {
                    console.error(err);
                    res.status(500);
                    res.send(err);
                    return;
                }
                res.send('Saved configuration for plugin ' + plugin.id);
                stopPlugin(plugin);
                const options = getPluginOptions(plugin.id);
                plugin.enableLogging = options.enableLogging;
                plugin.enableDebug = options.enableDebug;
                if (options.enabled) {
                    doPluginStart(app, plugin, location, options.configuration, restart);
                }
            });
        });
        router.get('/config', (req, res) => {
            res.json(getPluginOptions(plugin.id));
        });
        if (typeof plugin.registerWithRouter !== 'undefined') {
            plugin.registerWithRouter(router);
        }
        app.use(backwardsCompat('/plugins/' + plugin.id), router);
        if (typeof plugin.signalKApiRoutes === 'function') {
            app.use('/signalk/v1/api', plugin.signalKApiRoutes(express_1.default.Router()));
        }
    }
};
const isEnabledByPackageEnableDefault = (options, metadata) => lodash_1.default.isUndefined(options.enabled) &&
    metadata['signalk-plugin-enabled-by-default'];
