"use strict";
/*
 * 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;
    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 debug_1 = __importDefault(require("debug"));
const debug = debug_1.default('signalk-server:deltacache');
const signalk_schema_1 = require("@signalk/signalk-schema");
const lodash_1 = __importStar(require("lodash"));
const streambundle_1 = require("./streambundle");
class DeltaCache {
    constructor(app, streambundle) {
        this.cache = {};
        this.lastModifieds = {};
        this.sourceDeltas = {};
        this.app = app;
        streambundle.keys.onValue(key => {
            streambundle.getBus(key).onValue(this.onValue.bind(this));
        });
    }
    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);
        let contextAndPathParts = msg.context.split('.');
        if (msg.path.length !== 0) {
            contextAndPathParts = contextAndPathParts.concat(msg.path.split('.'));
        }
        // debug(JSON.stringify(parts))
        const leaf = getLeafObject(this.cache, contextAndPathParts, 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)
                : (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]);
                }
            });
        });
        let deltas = contexts.reduce((acc, context) => {
            let deltasToProcess;
            if (key) {
                deltasToProcess = lodash_1.default.get(context, key);
            }
            else {
                deltasToProcess = findDeltas(context);
            }
            if (deltasToProcess) {
                // acc.push(_.reduce(deltas, ((delta, acc) => !acc ? delta : (new Date(delta.timestamp).getTime() > new Date(acc.timestamp).getTime() ? delta : acc))))
                acc = acc.concat(lodash_1.default.values(lodash_1.default.pickBy(deltasToProcess, (val, akey) => {
                    return akey !== 'meta';
                })));
            }
            return acc;
        }, []);
        deltas.sort((left, right) => {
            return (new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime());
        });
        deltas = deltas.map(streambundle_1.toDelta);
        if (user) {
            deltas = deltas.filter((delta) => {
                return this.app.securityStrategy.filterReadDelta(user, delta);
            });
        }
        return deltas;
    }
}
exports.default = DeltaCache;
function pathToProcessForFull(pathArray) {
    if (pathArray.length > 0 && pathArray[0] === 'sources') {
        return [];
    }
    return pathArray;
}
function pickDeltasFromBranch(acc, obj) {
    if (lodash_1.isUndefined(obj.path) || 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 = signalk_schema_1.getSourceId(normalizedDelta.source);
        normalizedDelta.$source = dollarSource;
    }
    return dollarSource;
}
function getLeafObject(start, contextAndPathParts, createIfMissing = false, returnLast = false) {
    let current = start;
    // tslint:disable-next-line: forin
    for (const i in contextAndPathParts) {
        const p = contextAndPathParts[i];
        if (lodash_1.isUndefined(current[p])) {
            if (createIfMissing) {
                current[p] = {};
            }
            else {
                return returnLast ? current : null;
            }
        }
        current = current[p];
    }
    return current;
}
module.exports = DeltaCache;
