"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 });
exports.DialogueTurnLog = void 0;
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const events = __importStar(require("events"));
const ThingTalk = __importStar(require("thingtalk"));
const I18n = __importStar(require("../i18n"));
const value_category_1 = __importDefault(require("./value-category"));
const dialogue_loop_1 = __importDefault(require("./dialogue-loop"));
const protocol_1 = require("./protocol");
const ThingTalkUtils = __importStar(require("../utils/thingtalk"));
const StreamUtils = __importStar(require("../utils/stream-utils"));
const parsers_1 = require("../dataset-tools/parsers");
const DummyStatistics = {
    hit() {
    }
};
const DEFAULT_CONVERSATION_TTL = 600000; // 10 minutes
class DialogueTurnLog {
    constructor() {
        this._turn = {
            context: null,
            agent: null,
            agent_target: null,
            intermediate_context: null,
            user: '',
            user_target: ''
        };
        this._done = false;
    }
    get turn() {
        return this._turn;
    }
    get done() {
        return this._done;
    }
    finish() {
        this._done = true;
    }
    update(field, value) {
        this._turn[field] = this._turn[field] ? this._turn[field] + '\n' + value : value;
    }
}
exports.DialogueTurnLog = DialogueTurnLog;
class DialogueLog {
    constructor() {
        this._turns = [];
        this._done = false;
    }
    get turns() {
        return this._turns;
    }
    get done() {
        return this._done;
    }
    finish() {
        this._done = true;
        if (this.turns.length) {
            const lastTurn = this.turns[this.turns.length - 1];
            lastTurn.finish();
        }
    }
    append(turn) {
        this._turns.push(turn);
    }
}
/**
 * A single session of conversation in Almond.
 *
 * This object is responsible for maintaining the history of the conversation
 * to support clients reconnecting to the same conversation later, as well
 * as tracking connected clients and inactivity timeouts.
 *
 * The actual conversation logic is in {@link DialogueLoop}.
 */
class Conversation extends events.EventEmitter {
    constructor(engine, conversationId, user, options = {}) {
        super();
        this._engine = engine;
        this._user = user;
        this._conversationId = conversationId;
        this._locale = this._engine.platform.locale;
        this._ = I18n.get(this._locale).gettext;
        const stats = this._engine.platform.getCapability('statistics');
        if (stats === null)
            this._stats = DummyStatistics;
        else
            this._stats = stats;
        this._options = options;
        this._debug = !!this._options.debug;
        this.rng = options.rng || Math.random;
        this._loop = new dialogue_loop_1.default(this, this._engine, {
            nluServerUrl: options.nluServerUrl,
            nlgServerUrl: options.nlgServerUrl,
            debug: this._debug
        });
        this._expecting = null;
        this._context = { code: ['null'], entities: {} };
        this._delegates = new Set;
        this._history = [];
        this._nextMsgId = 0;
        this._inactivityTimeout = null;
        this._inactivityTimeoutSec = options.inactivityTimeout || DEFAULT_CONVERSATION_TTL;
        this._contextResetTimeout = null;
        this._contextResetTimeoutSec = options.contextResetTimeout || this._inactivityTimeoutSec;
        this._log = [];
    }
    get isAnonymous() {
        return !!this._options.anonymous;
    }
    get id() {
        return this._conversationId;
    }
    get user() {
        return this._user;
    }
    get platform() {
        return this._engine.platform;
    }
    get locale() {
        return this._engine.platform.locale;
    }
    get timezone() {
        return this._engine.platform.timezone;
    }
    get stats() {
        return this._stats;
    }
    get apps() {
        return this._engine.apps;
    }
    get schemas() {
        return this._engine.schemas;
    }
    get thingpedia() {
        return this._engine.thingpedia;
    }
    get history() {
        return this._history;
    }
    get inRecordingMode() {
        return !!this._options.log;
    }
    startRecording() {
        this._options.log = true;
    }
    endRecording() {
        this._options.log = false;
    }
    notify(appId, icon, outputType, outputValue) {
        return this._loop.dispatchNotify(appId, icon, outputType, outputValue);
    }
    notifyError(appId, icon, error) {
        return this._loop.dispatchNotifyError(appId, icon, error);
    }
    setExpected(expecting, context) {
        this._expecting = expecting;
        this._context = context;
    }
    async start() {
        this._resetInactivityTimeout();
        return this._loop.start(!!this._options.showWelcome);
    }
    async stop() {
        return this._loop.stop();
    }
    _resetInactivityTimeout() {
        // after "options.inactivityTimeout" milliseconds we inform the app that the conversation
        // is inactive (turn off LEDs, close the microphone, etc.)
        if (this._inactivityTimeout)
            clearTimeout(this._inactivityTimeout);
        if (this._inactivityTimeoutSec > 0) {
            this._inactivityTimeout = setTimeout(() => {
                this.emit('inactive');
            }, this._inactivityTimeoutSec);
        }
        // after "options.contextResetTimeout", we reset the context, forgetting the state of the
        // conversation
        if (this._contextResetTimeout)
            clearTimeout(this._contextResetTimeout);
        if (this._contextResetTimeoutSec) {
            this._contextResetTimeout = setTimeout(() => {
                this._loop.reset();
            }, this._contextResetTimeoutSec);
        }
    }
    async addOutput(out, replayHistory = true) {
        this._delegates.add(out);
        if (replayHistory) {
            for (const msg of this._history)
                await out.addMessage(msg);
        }
    }
    async removeOutput(out) {
        this._delegates.delete(out);
    }
    async _addMessage(msg) {
        msg.id = this._nextMsgId++;
        this._history.push(msg);
        if (this._history.length > 30)
            this._history.shift();
        await Promise.all(Array.from(this._delegates).map((out) => out.addMessage(msg)));
    }
    async handleCommand(command, platformData = {}) {
        this.stats.hit('sabrina-command');
        this.emit('active');
        this._resetInactivityTimeout();
        await this._addMessage({ type: protocol_1.MessageType.COMMAND, command });
        if (this._debug)
            console.log('Received assistant command ' + command);
        return this._loop.handleCommand({ type: 'command', utterance: command, platformData });
    }
    async handleParsedCommand(root, title, platformData = {}) {
        const command = `\\r ${typeof root === 'string' ? root : JSON.stringify(root)}`;
        this.stats.hit('sabrina-parsed-command');
        this.emit('active');
        this._resetInactivityTimeout();
        if (typeof root === 'string')
            root = JSON.parse(root);
        await this._addMessage({ type: protocol_1.MessageType.COMMAND, command: title || command, json: root });
        if (this._debug)
            console.log('Received pre-parsed assistant command');
        if (root.example_id) {
            this.thingpedia.clickExample(root.example_id).catch((e) => {
                console.error('Failed to record example click: ' + e.message);
            });
        }
        if ('program' in root)
            return this.handleThingTalk(root.program, platformData);
        const { code, entities } = root;
        for (const name in entities) {
            if (name.startsWith('SLOT_')) {
                const slotname = root.slots[parseInt(name.substring('SLOT_'.length))];
                const slotType = ThingTalk.Type.fromString(root.slotTypes[slotname]);
                const value = ThingTalk.Ast.Value.fromJSON(slotType, entities[name]);
                entities[name] = value;
            }
        }
        const parsed = await ThingTalkUtils.parsePrediction(code, entities, {
            thingpediaClient: this._engine.thingpedia,
            schemaRetriever: this._engine.schemas,
            loadMetadata: true
        }, true);
        return this._loop.handleCommand({ type: 'thingtalk', parsed, platformData });
    }
    async handleThingTalk(program, platformData = {}) {
        const command = `\\t ${program}`;
        this.stats.hit('sabrina-thingtalk-command');
        this.emit('active');
        this._resetInactivityTimeout();
        await this._addMessage({ type: protocol_1.MessageType.COMMAND, command });
        if (this._debug)
            console.log('Received ThingTalk program');
        const parsed = await ThingTalkUtils.parse(program, {
            thingpediaClient: this._engine.thingpedia,
            schemaRetriever: this._engine.schemas,
            loadMetadata: true
        });
        return this._loop.handleCommand({ type: 'thingtalk', parsed, platformData });
    }
    async setHypothesis(hypothesis) {
        await Promise.all(Array.from(this._delegates).map((out) => out.setHypothesis(hypothesis)));
    }
    async sendAskSpecial() {
        const what = value_category_1.default.toAskSpecial(this._expecting);
        if (this._debug) {
            if (what !== null && what !== 'generic')
                console.log('Genie sends a special request');
            else if (what !== null)
                console.log('Genie expects an answer');
        }
        await Promise.all(Array.from(this._delegates).map((out) => out.setExpected(what, this._context)));
    }
    sendReply(message, icon) {
        if (this._debug)
            console.log('Genie says: ' + message);
        return this._addMessage({ type: protocol_1.MessageType.TEXT, text: message, icon });
    }
    sendResult(message, icon) {
        return this._addMessage({ type: protocol_1.MessageType.RESULT, text: message.toLocaleString(this._locale), result: message, icon });
    }
    sendPicture(url, icon) {
        if (this._debug)
            console.log('Genie sends picture: ' + url);
        return this._addMessage({ type: protocol_1.MessageType.PICTURE, url, icon });
    }
    sendRDL(rdl, icon) {
        if (this._debug)
            console.log('Genie sends RDL: ' + rdl.callback);
        return this._addMessage({ type: protocol_1.MessageType.RDL, rdl, icon });
    }
    sendChoice(idx, title) {
        if (this._expecting !== value_category_1.default.MultipleChoice)
            console.log('UNEXPECTED: sendChoice while not expecting a MultipleChoice');
        if (this._debug)
            console.log('Genie sends multiple choice button: ' + title);
        return this._addMessage({ type: protocol_1.MessageType.CHOICE, idx, title });
    }
    sendButton(title, json) {
        if (this._debug)
            console.log('Genie sends generic button: ' + title);
        return this._addMessage({ type: protocol_1.MessageType.BUTTON, json, title });
    }
    sendLink(title, url) {
        if (this._debug)
            console.log('Almond sends link: ' + url);
        return this._addMessage({ type: protocol_1.MessageType.LINK, url, title });
    }
    get _lastDialogue() {
        if (this._log.length === 0)
            return null;
        return this._log[this._log.length - 1];
    }
    get _lastTurn() {
        const lastDialogue = this._lastDialogue;
        if (!lastDialogue || lastDialogue.turns.length === 0)
            return null;
        return lastDialogue.turns[lastDialogue.turns.length - 1];
    }
    appendNewDialogue() {
        this._log.push(new DialogueLog());
    }
    dialogueFinished() {
        const last = this._lastDialogue;
        if (last)
            last.finish();
    }
    turnFinished() {
        const last = this._lastTurn;
        if (last)
            last.finish();
    }
    appendNewTurn(turn) {
        const last = this._lastDialogue;
        if (!last || last.done)
            this.appendNewDialogue();
        const dialogue = this._lastDialogue;
        dialogue.append(turn);
    }
    voteLast(vote) {
        const last = this._lastTurn;
        if (!last)
            throw new Error('No dialogue is logged');
        last.turn.vote = vote;
    }
    commentLast(comment) {
        const last = this._lastTurn;
        if (!last)
            throw new Error('No dialogue is logged');
        last.turn.comment = comment;
    }
    updateLog(field, value) {
        let last = this._lastTurn;
        if (!last || last.done) {
            last = new DialogueTurnLog();
            this.appendNewTurn(last);
        }
        last.update(field, value);
    }
    async saveLog() {
        const dir = path_1.default.join(this._engine.platform.getWritableDir(), 'logs');
        try {
            fs_1.default.mkdirSync(dir);
        }
        catch (e) {
            if (e.code !== 'EEXIST')
                throw e;
        }
        const logfile = path_1.default.join(dir, this.id + '.txt');
        const serializer = new parsers_1.DialogueSerializer({ annotations: true });
        const output = fs_1.default.createWriteStream(logfile);
        serializer.pipe(output);
        for (const dialogue of this._log)
            serializer.write({ id: this.id, turns: dialogue.turns.map((log) => log.turn) });
        serializer.end();
        await StreamUtils.waitFinish(output);
    }
    get log() {
        const log = path_1.default.join(this._engine.platform.getWritableDir(), 'logs', this.id + '.txt');
        if (!fs_1.default.existsSync(log))
            return null;
        return log;
    }
}
exports.default = Conversation;
//# sourceMappingURL=conversation.js.map