"use strict";
/* eslint-disable @typescript-eslint/no-explicit-any */
/*
 * 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;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (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 () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
const Bacon = __importStar(require("baconjs"));
const geolib_1 = require("geolib");
const lodash_1 = __importStar(require("lodash"));
const debug_1 = require("./debug");
const streambundle_1 = require("./streambundle");
const debug = (0, debug_1.createDebug)('signalk-server:subscriptionmanager');
class SubscriptionManager {
    streambundle;
    selfContext;
    app;
    constructor(app) {
        this.streambundle = app.streambundle;
        this.selfContext = app.selfContext;
        this.app = app;
    }
    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((path) => {
                const buses = {};
                buses[path] = this.streambundle.getBus(path);
                handleSubscribeRows(this.app, command.subscribe, unsubscribes, buses, contextFilter, callback, errorCallback, user);
            }));
        }
        // Handle announceNewPaths: announce all paths matching context (once each)
        // This allows clients with granular subscriptions to discover available paths
        // without subscribing to everything continuously
        if (command.announceNewPaths) {
            const announcedPaths = new Set();
            // 1. Announce ALL existing paths matching context (send cached deltas once)
            const existingDeltas = this.app.deltaCache.getCachedDeltas(contextFilter, user);
            if (existingDeltas) {
                existingDeltas.forEach((delta) => {
                    // Track which paths we've announced
                    delta.updates?.forEach((update) => {
                        update.values?.forEach((vp) => {
                            if (vp.path) {
                                announcedPaths.add(vp.path);
                            }
                        });
                    });
                    callback(delta);
                });
            }
            // 2. Listen for NEW paths appearing later and announce once
            unsubscribes.push(this.streambundle.keys.onValue((path) => {
                if (announcedPaths.has(path)) {
                    return; // Already announced this path
                }
                announcedPaths.add(path);
                // Subscribe to the bus to get the first value for this new path
                // We can't rely on deltaCache here because it might not have
                // received the value yet (race condition with keys.onValue)
                const bus = this.streambundle.getBus(path);
                const unsubscribeBus = bus
                    .filter(contextFilter)
                    .take(1) // Only take the first value
                    .map(streambundle_1.toDelta)
                    .onValue((delta) => {
                    callback(delta);
                });
                // Add to unsubscribes so it gets cleaned up
                unsubscribes.push(unsubscribeBus);
            }));
        }
    }
    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 !== undefined) {
            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
    (0, 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}`);
                }
                const minPeriodValue = Number(subscribeRow.minPeriod);
                debug('minPeriod:' + subscribeRow.minPeriod);
                if (isNaN(minPeriodValue)) {
                    errorCallback(`invalid minPeriod value '${subscribeRow.minPeriod}', ignoring`);
                }
                else if (key !== '') {
                    // we can not apply minPeriod for empty path subscriptions
                    debug('debouncing');
                    filteredBus = filteredBus.debounceImmediate(minPeriodValue);
                }
            }
            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 if (key !== '') {
                    // we can not apply period for empty path subscriptions
                    const interval = Number(subscribeRow.period) || 1000;
                    filteredBus = filteredBus
                        .bufferWithTime(interval)
                        .flatMapLatest((bufferedValues) => {
                        const uniqueValues = (0, lodash_1.default)(bufferedValues)
                            .reverse()
                            .uniqBy((value) => value.context + ':' + value.$source + ':' + value.path)
                            .value();
                        return Bacon.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(/[\\^$+?()[\]{}|]/g, '\\$&')
        .replace(/\./g, '\\.')
        .replace(/\*/g, '.*');
    const matcher = new RegExp('^' + pattern + '$');
    return (aPath) => matcher.test(aPath);
}
function contextMatcher(selfContext, app, subscribeCommand, errorCallback) {
    debug.enabled && debug('subscribeCommand:' + JSON.stringify(subscribeCommand));
    if (subscribeCommand.context) {
        if ((0, lodash_1.isString)(subscribeCommand.context)) {
            const pattern = subscribeCommand.context
                .replace(/[\\^$+?()[\]{}|]/g, '\\$&')
                .replace(/\./g, '\\.')
                .replace(/\*/g, '.*');
            const matcher = new RegExp('^' + pattern + '$');
            return (normalizedDeltaData) => matcher.test(normalizedDeltaData.context) ||
                ((subscribeCommand.context === 'vessels.self' ||
                    subscribeCommand.context === 'self') &&
                    normalizedDeltaData.context === selfContext);
        }
        else if ('radius' in subscribeCommand.context) {
            if (!(0, lodash_1.get)(subscribeCommand.context, 'radius') ||
                !(0, lodash_1.get)(subscribeCommand.context, 'position.latitude') ||
                !(0, lodash_1.get)(subscribeCommand.context, 'position.longitude')) {
                errorCallback('Please specify a radius and position for relativePosition');
                return () => false;
            }
            return (normalizedDeltaData) => checkPosition(app, subscribeCommand.context, normalizedDeltaData);
        }
    }
    return () => true;
}
function checkPosition(app, origin, normalizedDelta) {
    const vessel = (0, lodash_1.get)(app.signalk.root, normalizedDelta.context);
    const position = (0, lodash_1.get)(vessel, 'navigation.position');
    return (position &&
        position.value &&
        position.value.latitude &&
        position.value.longitude &&
        (0, geolib_1.isPointWithinRadius)(position.value, origin.position, origin.radius));
}
module.exports = SubscriptionManager;
//# sourceMappingURL=subscriptionmanager.js.map