"use strict";
/*
 * 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.
 */
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 };
};
const baconjs_1 = __importDefault(require("baconjs"));
const debug_1 = __importDefault(require("debug"));
const geolib_1 = require("geolib");
const lodash_1 = __importStar(require("lodash"));
const debug = debug_1.default('signalk-server:subscriptionmanager');
const streambundle_1 = require("./streambundle");
class SubscriptionManager {
    constructor(app) {
        this.subscribe = (command, unsubscribes, errorCallback, callback, user) => {
            const contextFilter = contextMatcher(this.selfContext, this.app, command, errorCallback);
            if (Array.isArray(command.subscribe)) {
                handleSubscribeRows(this.app, command.subscribe, unsubscribes, this.streambundle.buses, contextFilter, callback, errorCallback, user);
                // listen to new keys and then use the same logic to check if we
                // want to subscribe, passing in a map with just that single bus
                unsubscribes.push(this.streambundle.keys.onValue((key) => {
                    const buses = {};
                    buses[key] = this.streambundle.getBus(key);
                    handleSubscribeRows(this.app, command.subscribe, unsubscribes, buses, contextFilter, callback, errorCallback, user);
                }));
            }
        };
        this.streambundle = app.streambundle;
        this.selfContext = app.selfContext;
        this.app = app;
    }
    unsubscribe(msg, unsubscribes) {
        if (msg.unsubscribe &&
            msg.context === '*' &&
            msg.unsubscribe &&
            msg.unsubscribe.length === 1 &&
            msg.unsubscribe[0].path === '*') {
            debug('Unsubscribe all');
            unsubscribes.forEach(unsubscribe => unsubscribe());
            // clear unsubscribes
            unsubscribes.length = 0;
        }
        else {
            throw new Error(`Only '{"context":"*","unsubscribe":[{"path":"*"}]}' supported, received ${JSON.stringify(msg)}`);
        }
    }
}
function handleSubscribeRows(app, rows, unsubscribes, buses, filter, callback, errorCallback, user) {
    rows.reduce((acc, subscribeRow) => {
        if (subscribeRow.path) {
            handleSubscribeRow(app, subscribeRow, unsubscribes, buses, filter, callback, errorCallback, user);
        }
        return acc;
    }, unsubscribes);
}
function handleSubscribeRow(app, subscribeRow, unsubscribes, buses, filter, callback, errorCallback, user) {
    const matcher = pathMatcher(subscribeRow.path);
    // iterate over all the buses, checking if we want to subscribe to its values
    lodash_1.forOwn(buses, (bus, key) => {
        if (matcher(key)) {
            debug('Subscribing to key ' + key);
            let filteredBus = bus.filter(filter);
            if (subscribeRow.minPeriod) {
                if (subscribeRow.policy && subscribeRow.policy !== 'instant') {
                    errorCallback(`minPeriod assumes policy 'instant', ignoring policy ${subscribeRow.policy}`);
                }
                debug('minPeriod:' + subscribeRow.minPeriod);
                filteredBus = filteredBus.debounceImmediate(subscribeRow.minPeriod);
            }
            else if (subscribeRow.period ||
                (subscribeRow.policy && subscribeRow.policy === 'fixed')) {
                if (subscribeRow.policy && subscribeRow.policy !== 'fixed') {
                    errorCallback(`period assumes policy 'fixed', ignoring policy ${subscribeRow.policy}`);
                }
                else {
                    const interval = subscribeRow.period || 1000;
                    filteredBus = filteredBus
                        .bufferWithTime(interval)
                        .flatMapLatest((bufferedValues) => {
                        const uniqueValues = lodash_1.default(bufferedValues)
                            .reverse()
                            .uniqBy(value => value.context + ':' + value.$source + ':' + value.path)
                            .value();
                        return baconjs_1.default.fromArray(uniqueValues);
                    });
                }
            }
            if (subscribeRow.format && subscribeRow.format !== 'delta') {
                errorCallback('Only delta format supported, using it');
            }
            if (subscribeRow.policy &&
                !['instant', 'fixed'].some(s => s === subscribeRow.policy)) {
                errorCallback(`Only 'instant' and 'fixed' policies supported, ignoring policy ${subscribeRow.policy}`);
            }
            unsubscribes.push(filteredBus.map(streambundle_1.toDelta).onValue(callback));
            const latest = app.deltaCache.getCachedDeltas(filter, user, key);
            if (latest) {
                latest.forEach(callback);
            }
        }
    });
}
function pathMatcher(path) {
    const pattern = path.replace('.', '\\.').replace('*', '.*');
    const matcher = new RegExp('^' + pattern + '$');
    return (aPath) => matcher.test(aPath);
}
function contextMatcher(selfContext, app, subscribeCommand, errorCallback) {
    debug('subscribeCommand:' + JSON.stringify(subscribeCommand));
    if (subscribeCommand.context) {
        if (lodash_1.isString(subscribeCommand.context)) {
            const pattern = subscribeCommand.context
                .replace('.', '\\.')
                .replace('*', '.*');
            const matcher = new RegExp('^' + pattern + '$');
            return (normalizedDeltaData) => matcher.test(normalizedDeltaData.context) ||
                ((subscribeCommand.context === 'vessels.self' ||
                    subscribeCommand.context === 'self') &&
                    normalizedDeltaData.context === selfContext);
        }
        else if (lodash_1.get(subscribeCommand.context, 'radius')) {
            if (!lodash_1.get(subscribeCommand.context, 'radius') ||
                !lodash_1.get(subscribeCommand.context, 'position.latitude') ||
                !lodash_1.get(subscribeCommand.context, 'position.longitude')) {
                errorCallback('Please specify a radius and position for relativePosition');
                return x => false;
            }
            return (normalizedDeltaData) => checkPosition(app, subscribeCommand.context, normalizedDeltaData);
        }
    }
    return x => true;
}
function checkPosition(app, origin, normalizedDelta) {
    const vessel = lodash_1.get(app.signalk.root, normalizedDelta.context);
    const position = lodash_1.get(vessel, 'navigation.position');
    return (position &&
        position.value &&
        position.value.latitude &&
        position.value.longitude &&
        geolib_1.isPointWithinRadius(position.value, origin.position, origin.radius));
}
module.exports = SubscriptionManager;
