"use strict";
/* eslint-disable @typescript-eslint/no-explicit-any */
/*
 * Copyright 2017 Scott Bender <scott@scottbender.net>
 *
 * 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;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
const debug_1 = require("./debug");
const debug = (0, debug_1.createDebug)('signalk-server:deltacache');
const signalk_schema_1 = require("@signalk/signalk-schema");
const lodash_1 = __importStar(require("lodash"));
const streambundle_1 = require("./streambundle");
class DeltaCache {
    cache = {};
    lastModifieds = {};
    app;
    defaults;
    sourceDeltas = {};
    cachedContextPaths = {};
    constructor(app, streambundle) {
        this.app = app;
        streambundle.keys.onValue((key) => {
            streambundle.getBus(key).onValue(this.onValue.bind(this));
        });
        // String.split() is heavy enough and called frequently enough
        // to warrant caching the result. Has a noticeable effect
        // on throughput of a server going full blast with the n2k
        // sample data and the memory hit is negligible. The cache
        // must be pruned, or AIS vessel data will stick forever.
        // No fancy pruning, just clear & let it recreate.
        setInterval(() => {
            this.cachedContextPaths = {};
        }, 5 * 60 * 1000);
    }
    getContextAndPathParts(msg) {
        let result;
        if (this.cachedContextPaths[msg.context] &&
            (result = this.cachedContextPaths[msg.context][msg.path])) {
            return result;
        }
        let contextAndPathParts = msg.context.split('.');
        if (msg.path.length !== 0) {
            contextAndPathParts = contextAndPathParts.concat(msg.path.split('.'));
        }
        if (!this.cachedContextPaths[msg.context]) {
            this.cachedContextPaths[msg.context] = {};
        }
        this.cachedContextPaths[msg.context][msg.path] = contextAndPathParts;
        return contextAndPathParts;
    }
    onValue(msg) {
        // debug(`onValue ${JSON.stringify(msg)}`)
        if (msg.isMeta) {
            // ignore meta data since it's getting managed by FullSignalK
            return;
        }
        const sourceRef = ensureHasDollarSource(msg);
        const leaf = getLeafObject(this.cache, this.getContextAndPathParts(msg), true);
        if (msg.path.length !== 0) {
            leaf[sourceRef] = msg;
        }
        else if (msg.value) {
            lodash_1.default.keys(msg.value).forEach((key) => {
                if (!leaf[key]) {
                    leaf[key] = {};
                }
                leaf[key][sourceRef] = msg;
            });
        }
        this.lastModifieds[msg.context] = Date.now();
    }
    setSourceDelta(key, delta) {
        this.sourceDeltas[key] = delta;
        this.app.signalk.addDelta(delta);
    }
    deleteContext(contextKey) {
        debug('Deleting context ' + contextKey);
        const contextParts = contextKey.split('.');
        if (contextParts.length === 2) {
            delete this.cache[contextParts[0]][contextParts[1]];
        }
    }
    pruneContexts(seconds) {
        debug('pruning contexts...');
        const threshold = Date.now() - seconds * 1000;
        for (const contextKey in this.lastModifieds) {
            if (this.lastModifieds[contextKey] < threshold) {
                this.deleteContext(contextKey);
                delete this.lastModifieds[contextKey];
            }
        }
    }
    buildFull(user, path) {
        const leaf = getLeafObject(this.cache, pathToProcessForFull(path), false, true);
        let deltas;
        if (leaf) {
            deltas = findDeltas(leaf).map(streambundle_1.toDelta);
        }
        return this.buildFullFromDeltas(user, deltas, path.length === 0 || path[0] === 'sources');
    }
    getSources() {
        const signalk = new signalk_schema_1.FullSignalK(this.app.selfId, this.app.selfType);
        const addDelta = signalk.addDelta.bind(signalk);
        lodash_1.default.values(this.sourceDeltas).forEach(addDelta);
        return signalk.retrieve().sources;
    }
    buildFullFromDeltas(user, deltas, includeSources) {
        const signalk = new signalk_schema_1.FullSignalK(this.app.selfId, this.app.selfType);
        const addDelta = signalk.addDelta.bind(signalk);
        if (includeSources) {
            lodash_1.default.values(this.sourceDeltas).forEach(addDelta);
        }
        if (deltas && deltas.length) {
            const secFilter = this.app.securityStrategy.shouldFilterDeltas()
                ? (delta) => this.app.securityStrategy.filterReadDelta(user, delta)
                : () => true;
            deltas.filter(secFilter).forEach(addDelta);
        }
        return signalk.retrieve();
    }
    getCachedDeltas(contextFilter, user, key) {
        const contexts = [];
        lodash_1.default.keys(this.cache).forEach((type) => {
            lodash_1.default.keys(this.cache[type]).forEach((id) => {
                const context = `${type}.${id}`;
                if (contextFilter({ context })) {
                    contexts.push(this.cache[type][id]);
                }
            });
        });
        const deltas = contexts.reduce((acc, context) => {
            let deltasToProcess;
            if (key) {
                deltasToProcess = lodash_1.default.get(context, key);
            }
            else {
                deltasToProcess = findDeltas(context);
            }
            if (deltasToProcess) {
                acc = acc.concat(lodash_1.default.values(lodash_1.default.pickBy(deltasToProcess, (val, akey) => {
                    return akey !== 'meta';
                })));
            }
            return acc;
        }, []);
        // ISO 8601 Zulu timestamps (as produced by new Date().toISOString() in
        // handleMessage) are lexicographically sortable, so we can avoid
        // allocating two Date objects per comparison.
        deltas.sort((left, right) => left.timestamp < right.timestamp
            ? -1
            : left.timestamp > right.timestamp
                ? 1
                : 0);
        return deltas.map(streambundle_1.toDelta).filter((delta) => {
            return this.app.securityStrategy.filterReadDelta(user, delta);
        });
    }
}
exports.default = DeltaCache;
function pathToProcessForFull(pathArray) {
    if (pathArray.length > 0 && pathArray[0] === 'sources') {
        return [];
    }
    return pathArray;
}
function pickDeltasFromBranch(acc, obj) {
    if (typeof obj === 'object') {
        if ((0, lodash_1.isUndefined)(obj.path) || (0, lodash_1.isUndefined)(obj.value)) {
            // not a delta, so process possible children
            lodash_1.default.values(obj).reduce(pickDeltasFromBranch, acc);
        }
        else {
            acc.push(obj);
        }
    }
    return acc;
}
function findDeltas(branchOrLeaf) {
    return lodash_1.default.values(branchOrLeaf).reduce(pickDeltasFromBranch, []);
}
function ensureHasDollarSource(normalizedDelta) {
    let dollarSource = normalizedDelta.$source;
    if (!dollarSource) {
        dollarSource = (0, signalk_schema_1.getSourceId)(normalizedDelta.source);
        normalizedDelta.$source = dollarSource;
    }
    return dollarSource;
}
function getLeafObject(start, contextAndPathParts, createIfMissing = false, returnLast = false) {
    let current = start;
    for (let i = 0; i < contextAndPathParts.length; i++) {
        const p = contextAndPathParts[i];
        if ((0, lodash_1.isUndefined)(current[p])) {
            if (createIfMissing) {
                current[p] = {};
            }
            else {
                return returnLast ? current : null;
            }
        }
        current = current[p];
    }
    return current;
}
//# sourceMappingURL=deltacache.js.map