"use strict";
// -*- mode: typescript; indent-tabs-mode: nil; js-basic-offset: 4 -*-
//
// This file is part of Genie
//
// Copyright 2020 The Board of Trustees of the Leland Stanford Junior University
//
// 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.
//
// Author: Giovanni Campagna <gcampagn@cs.stanford.edu>
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.prototype.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 events = __importStar(require("events"));
const uuid = __importStar(require("uuid"));
const ThingTalk = __importStar(require("thingtalk"));
const icons_1 = require("../../utils/icons");
const describe_1 = require("../../utils/thingtalk/describe");
const I18n = __importStar(require("../../i18n"));
const app_1 = __importDefault(require("../db/app"));
const app_executor_1 = __importDefault(require("./app_executor"));
/**
 * The collection of all running and configured ThingTalk programs.
 */
class AppDatabase extends events.EventEmitter {
    /**
     * Construct the app database for this engine.
     *
     * There is only one app database instance per engine,
     * and it is accessible as {@link Engine#apps}.
     * @package
     */
    constructor(engine) {
        super();
        this._apps = {};
        this._engine = engine;
        this._platform = engine.platform;
        this._db = new app_1.default(engine.platform);
    }
    _getAll() {
        return this._db.getAll();
    }
    _insertOne(row) {
        return this._db.insertOne(row.uniqueId, row);
    }
    _deleteOne(uniqueId) {
        return this._db.deleteOne(uniqueId);
    }
    async _doStartApp(app, isNewApp) {
        try {
            if (isNewApp)
                await app.runCommand();
            // only start and save into db apps that actually have some rules
            if (app.hasRule) {
                this._enableApp(app);
                if (isNewApp)
                    await this._saveApp(app);
            }
            else {
                await this._removeAppInternal(app.uniqueId);
            }
        }
        catch (e) {
            if (e.code === 'ECANCELLED')
                return;
            console.error('Failed to add app: ' + e);
            console.error(e.stack);
            app.reportError(e);
        }
    }
    async createApp(program, options = {}) {
        const uniqueId = options.uniqueId || 'uuid-' + uuid.v4();
        const name = options.name || describe_1.getProgramName(program);
        delete options.name;
        let description = options.description;
        if (!description) {
            // if we don't have a description already, compute one using
            // the Describer
            const allocator = new ThingTalk.Syntax.SequentialEntityAllocator({});
            const describer = new describe_1.Describer(this._platform.locale, this._platform.timezone, allocator);
            // retrieve the relevant primitive templates
            const kinds = new Set();
            for (const [, prim] of program.iteratePrimitives(false))
                kinds.add(prim.selector.kind);
            for (const kind of kinds)
                describer.setDataset(kind, await this._engine.schemas.getExamplesByKind(kind));
            description = describer.describeProgram(program);
            // apply the usual postprocessing
            const langPack = I18n.get(this._platform.locale);
            // treat it as an agent sentence for purposes of postprocessing
            // (which disables randomization)
            // even though it is a user-side sentence (ie, it says "my")
            description = langPack.postprocessNLG(langPack.postprocessSynthetic(description, program, null, 'agent'), allocator.entities, {
                timezone: this._platform.timezone,
                getPreferredUnit: (type) => {
                    const pref = this._platform.getSharedPreferences();
                    return pref.get('preferred-' + type);
                }
            });
        }
        delete options.description;
        const icon = options.icon || icons_1.getProgramIcon(program);
        const conversation = options.conversation;
        return this._loadOneApp(program.prettyprint(), { icon, conversation }, uniqueId, name, description, true);
    }
    async _loadOneApp(code, metadata, uniqueId, name, description, isNewApp) {
        const app = new app_executor_1.default(this._engine, code, metadata, name, description);
        try {
            this._addAppInternal(app, uniqueId);
            await app.compile();
            // run the rest of app loading asynchronously
            this._doStartApp(app, isNewApp);
        }
        catch (e) {
            console.error('Failed to add app: ' + e);
            app.reportError(e);
            this._removeAppInternal(app.uniqueId);
            if (!isNewApp)
                await this._deleteOne(uniqueId);
        }
        return app;
    }
    async start() {
        await this._getAll().then((rows) => Promise.all(rows.map((row) => {
            const code = row.code;
            const metadata = JSON.parse(row.state);
            return this._loadOneApp(code, metadata, row.uniqueId, row.name, row.description, false);
        })));
    }
    async stop() {
    }
    async _removeAppInternal(uniqueId) {
        const app = this._apps[uniqueId];
        delete this._apps[uniqueId];
        if (app !== undefined) {
            this.emit('app-removed', app);
            try {
                await app.destroy();
            }
            catch (e) {
                console.error('Failed to destroy app ' + uniqueId + ': ' + e.message);
            }
        }
    }
    _addAppInternal(app, uniqueId) {
        if (app.uniqueId === undefined) {
            app.uniqueId = uniqueId;
        }
        else {
            if (uniqueId !== undefined && app.uniqueId !== uniqueId)
                throw new Error('App unique id is different from stored value');
        }
        if (this._apps[app.uniqueId])
            throw new Error('Multiple apps with the same ID, delete one first');
        this._apps[app.uniqueId] = app;
    }
    _enableApp(app) {
        app.isEnabled = true;
        this.emit('app-added', app);
    }
    _saveApp(app) {
        return this._insertOne({
            uniqueId: app.uniqueId,
            state: JSON.stringify(app.metadata),
            code: app.code,
            name: app.name,
            description: app.description
        });
    }
    async removeApp(app) {
        await this._removeAppInternal(app.uniqueId);
        await this._deleteOne(app.uniqueId);
    }
    getAllApps() {
        const apps = [];
        for (const id in this._apps)
            apps.push(this._apps[id]);
        return apps;
    }
    getApp(id) {
        return this._apps[id];
    }
    hasApp(id) {
        return this._apps[id] !== undefined;
    }
}
exports.default = AppDatabase;
//# sourceMappingURL=database.js.map