"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 assert_1 = __importDefault(require("assert"));
const thingtalk_1 = require("thingtalk");
const string_interp_1 = __importDefault(require("string-interp"));
const consumer_queue_1 = __importDefault(require("consumer-queue"));
const icons_1 = require("../utils/icons");
const ThingTalkUtils = __importStar(require("../utils/thingtalk"));
const ParserClient = __importStar(require("../prediction/parserclient"));
const I18n = __importStar(require("../i18n"));
const value_category_1 = __importDefault(require("./value-category"));
const dialogue_queue_1 = __importDefault(require("./dialogue_queue"));
const errors_1 = require("./errors");
const Helpers = __importStar(require("./helpers"));
const dialogue_policy_1 = __importDefault(require("./dialogue_policy"));
const text_formatter_1 = __importDefault(require("./card-output/text-formatter"));
const card_formatter_1 = __importDefault(require("./card-output/card-formatter"));
const execution_dialogue_agent_1 = __importDefault(require("./execution_dialogue_agent"));
// TODO: load the policy.yaml file instead
const POLICY_NAME = 'org.thingpedia.dialogue.transaction';
const TERMINAL_STATES = [
    'sys_end', 'sys_action_success'
];
// Confidence thresholds:
//
// The API returns two global scores associated with
// the utterance (the "command" score and the "ignore" score),
// and a confidence score on each candidate parse.
// (There is a third score, the "other" score, which is exactly
// 1-command-ignore)
//
// (See LocalParserClient for how these scores are computed
// from the raw confidence scores produced by genienlp)
//
// - If the "ignore" score is greater than IGNORE_THRESHOLD
//   we ignore this command entirely, do nothing and say
//   nothing. Typically, this means a spurious wakeword activation,
//   or a command that was truncated midway by the microphone.
//   In Alexa, the ring would light up, turn off, and nothing would happen.
//
// - If the "command" score is less than IN_DOMAIN_THRESHOLD,
//   we ship this command out to other backends silently, or tell
//   the user that the command is not supported.
//
// - If we have a best valid parse, and the confidence of that parse
//   is greater than CONFIDENCE_CONFIRM_THRESHOLD, we run the command
//   without further confirmation.
//
// - If we have a best valid parse, and the confidence of that parse
//   is greater than CONFIDENCE_FAILURE_THRESHOLD, we ask the user
//   for additional confirmation before executing.
//
// - In all other cases, we tell the user we did not understand.
const CONFIDENCE_CONFIRM_THRESHOLD = 0.5;
const CONFIDENCE_FAILURE_THRESHOLD = 0.25;
const IN_DOMAIN_THRESHOLD = 0.5;
const IGNORE_THRESHOLD = 0.5;
var CommandAnalysisType;
(function (CommandAnalysisType) {
    // special commands - these are generated by the exact matcher, or
    // by UI buttons like the "X" button
    CommandAnalysisType[CommandAnalysisType["STOP"] = 0] = "STOP";
    CommandAnalysisType[CommandAnalysisType["NEVERMIND"] = 1] = "NEVERMIND";
    CommandAnalysisType[CommandAnalysisType["WAKEUP"] = 2] = "WAKEUP";
    CommandAnalysisType[CommandAnalysisType["DEBUG"] = 3] = "DEBUG";
    // some sort of command
    CommandAnalysisType[CommandAnalysisType["CONFIDENT_IN_DOMAIN_COMMAND"] = 4] = "CONFIDENT_IN_DOMAIN_COMMAND";
    CommandAnalysisType[CommandAnalysisType["NONCONFIDENT_IN_DOMAIN_COMMAND"] = 5] = "NONCONFIDENT_IN_DOMAIN_COMMAND";
    CommandAnalysisType[CommandAnalysisType["OUT_OF_DOMAIN_COMMAND"] = 6] = "OUT_OF_DOMAIN_COMMAND";
    CommandAnalysisType[CommandAnalysisType["PARSE_FAILURE"] = 7] = "PARSE_FAILURE";
    // ignore this command and do nothing
    CommandAnalysisType[CommandAnalysisType["IGNORE"] = 8] = "IGNORE";
})(CommandAnalysisType || (CommandAnalysisType = {}));
class DialogueLoop {
    constructor(conversation, engine, options) {
        this._raw = false;
        this._stopped = false;
        this._userInputQueue = new consumer_queue_1.default();
        this._notifyQueue = new consumer_queue_1.default();
        this._debug = options.debug;
        this.conversation = conversation;
        this.engine = engine;
        this._prefs = engine.platform.getSharedPreferences();
        this._nlu = ParserClient.get(options.nluServerUrl || undefined, engine.platform.locale, engine.platform, undefined, engine.thingpedia);
        this._nlg = ParserClient.get(options.nlgServerUrl || undefined, engine.platform.locale, engine.platform);
        this._langPack = I18n.get(engine.platform.locale);
        this._textFormatter = new text_formatter_1.default(engine.platform.locale, engine.platform.timezone, engine.schemas);
        this._cardFormatter = new card_formatter_1.default(engine.platform.locale, engine.platform.timezone, engine.schemas);
        this.icon = null;
        this.expecting = null;
        this._choices = [];
        this.platformData = {};
        this._mgrResolve = null;
        this._mgrPromise = null;
        this._agent = new execution_dialogue_agent_1.default(engine, this, options.debug);
        this._policy = new dialogue_policy_1.default({
            thingpedia: conversation.thingpedia,
            schemas: conversation.schemas,
            locale: conversation.locale,
            timezone: engine.platform.timezone,
            rng: conversation.rng,
            debug: this._debug
        });
        this._dialogueState = null; // thingtalk dialogue state
        this._executorState = undefined; // private object managed by DialogueExecutor
        this._lastNotificationApp = undefined;
    }
    get _() {
        return this.conversation._;
    }
    get isAnonymous() {
        return this.conversation.isAnonymous;
    }
    get hasDebug() {
        return this._debug;
    }
    debug(...args) {
        if (!this._debug)
            return;
        console.log(...args);
    }
    interpolate(msg, args) {
        return string_interp_1.default(msg, args, {
            locale: this.conversation.locale,
            timezone: this.conversation.timezone
        }) || '';
    }
    async nextCommand() {
        await this.conversation.sendAskSpecial();
        this._mgrPromise = null;
        this._mgrResolve();
        const intent = await this._userInputQueue.pop();
        this.platformData = intent.platformData;
        return intent;
    }
    _checkPolicy(policyName) {
        if (policyName !== POLICY_NAME) {
            // TODO we should download the policy from Thingpedia
            throw new Error(`Invalid dialogue policy ${policyName}`);
        }
    }
    _getSpecialThingTalkType(input) {
        if (input instanceof thingtalk_1.Ast.ControlCommand) {
            if (input.intent instanceof thingtalk_1.Ast.SpecialControlIntent) {
                switch (input.intent.type) {
                    case 'stop':
                        return CommandAnalysisType.STOP;
                    case 'nevermind':
                        return CommandAnalysisType.NEVERMIND;
                    case 'wakeup':
                        return CommandAnalysisType.WAKEUP;
                    case 'debug':
                        return CommandAnalysisType.DEBUG;
                    case 'failed':
                        return CommandAnalysisType.PARSE_FAILURE;
                }
            }
        }
        // anything else is automatically in-domain
        return CommandAnalysisType.CONFIDENT_IN_DOMAIN_COMMAND;
    }
    _maybeGetThingTalkAnswer(input) {
        if (input instanceof thingtalk_1.Ast.ControlCommand) {
            if (input.intent instanceof thingtalk_1.Ast.SpecialControlIntent) {
                switch (input.intent.type) {
                    case 'yes':
                    case 'no':
                        return new thingtalk_1.Ast.Value.Boolean(input.intent.type === 'yes');
                }
            }
            else if (input.intent instanceof thingtalk_1.Ast.AnswerControlIntent
                || input.intent instanceof thingtalk_1.Ast.ChoiceControlIntent) {
                return input.intent.value;
            }
        }
        return null;
    }
    _prepareContextForPrediction(state, forSide) {
        const prepared = ThingTalkUtils.prepareContextForPrediction(state, forSide);
        return ThingTalkUtils.serializeNormalized(prepared);
    }
    async _analyzeCommand(command) {
        if (command.type === 'thingtalk') {
            const type = this._getSpecialThingTalkType(command.parsed);
            return {
                type,
                utterance: `\\t ${command.parsed.prettyprint()}`,
                answer: this._maybeGetThingTalkAnswer(command.parsed),
                parsed: command.parsed
            };
        }
        // ok so this was a natural language
        if (this._raw) {
            // in "raw mode", all natural language becomes an answer
            let value;
            if (this.expecting === value_category_1.default.Location)
                value = new thingtalk_1.Ast.LocationValue(new thingtalk_1.Ast.UnresolvedLocation(command.utterance));
            else
                value = new thingtalk_1.Ast.Value.String(command.utterance);
            return {
                type: CommandAnalysisType.CONFIDENT_IN_DOMAIN_COMMAND,
                utterance: command.utterance,
                answer: value,
                parsed: new thingtalk_1.Ast.ControlCommand(null, new thingtalk_1.Ast.AnswerControlIntent(null, value))
            };
        }
        // alright, let's ask parser first then
        let nluResult;
        try {
            const [contextCode, contextEntities] = this._prepareContextForPrediction(this._dialogueState, 'user');
            nluResult = await this._nlu.sendUtterance(command.utterance, contextCode, contextEntities, {
                expect: this.expecting ? String(this.expecting) : undefined,
                choices: this._choices,
                store: this._prefs.get('sabrina-store-log') || 'no'
            });
        }
        catch (e) {
            if (e.code === 'EHOSTUNREACH' || e.code === 'ETIMEDOUT') {
                await this.reply(this._("Sorry, I cannot contact the Almond service. Please check your Internet connection and try again later."), null);
                throw new errors_1.CancellationError();
            }
            else if (typeof e.code === 'number' && (e.code === 404 || e.code >= 500)) {
                await this.reply(this._("Sorry, there seems to be a problem with the Almond service at the moment. Please try again later."), null);
                throw new errors_1.CancellationError();
            }
            else {
                throw e;
            }
        }
        if (nluResult.intent.ignore >= IGNORE_THRESHOLD) {
            this.debug('Ignored likely spurious command');
            return {
                type: CommandAnalysisType.IGNORE,
                utterance: command.utterance,
                answer: null,
                parsed: new thingtalk_1.Ast.ControlCommand(null, new thingtalk_1.Ast.SpecialControlIntent(null, 'failed'))
            };
        }
        if (nluResult.intent.command < IN_DOMAIN_THRESHOLD) {
            this.debug('Analyzed as out-of-domain command');
            return {
                type: CommandAnalysisType.OUT_OF_DOMAIN_COMMAND,
                utterance: command.utterance,
                answer: null,
                parsed: new thingtalk_1.Ast.ControlCommand(null, new thingtalk_1.Ast.SpecialControlIntent(null, 'failed'))
            };
        }
        // parse all code sequences into an Intent
        // this will correctly filter out anything that does not parse
        const candidates = await Promise.all(nluResult.candidates.map(async (candidate, beamposition) => {
            let parsed;
            try {
                parsed = await ThingTalkUtils.parsePrediction(candidate.code, nluResult.entities, {
                    thingpediaClient: this.engine.thingpedia,
                    schemaRetriever: this.engine.schemas,
                    loadMetadata: true,
                }, true);
            }
            catch (e) {
                // Likely, a type error in the ThingTalk code; not a big deal, but we still log it
                console.log(`Failed to parse beam ${beamposition}: ${e.message}`);
                parsed = new thingtalk_1.Ast.ControlCommand(null, new thingtalk_1.Ast.SpecialControlIntent(null, 'failed'));
            }
            return { parsed, score: candidate.score };
        }));
        // ensure that we always have at least one candidate by pushing $failed at the end
        candidates.push({ parsed: new thingtalk_1.Ast.ControlCommand(null, new thingtalk_1.Ast.SpecialControlIntent(null, 'failed')), score: 0 });
        // ignore all candidates with score==Infinity that we failed to parse
        // (these are exact matches that correspond to skills not available for
        // this user)
        let i = 0;
        let choice = candidates[i];
        let type = this._getSpecialThingTalkType(choice.parsed);
        while (i < candidates.length - 1 && type === CommandAnalysisType.PARSE_FAILURE && choice.score === 'Infinity') {
            i++;
            choice = candidates[i];
            type = this._getSpecialThingTalkType(choice.parsed);
        }
        if (type === CommandAnalysisType.PARSE_FAILURE ||
            choice.score < CONFIDENCE_FAILURE_THRESHOLD) {
            type = CommandAnalysisType.PARSE_FAILURE;
            this.debug('Failed to analyze message');
            this.conversation.stats.hit('sabrina-failure');
        }
        else if (choice.score < CONFIDENCE_CONFIRM_THRESHOLD) {
            type = CommandAnalysisType.NONCONFIDENT_IN_DOMAIN_COMMAND;
            this.debug('Dubiously analyzed message into ' + choice.parsed.prettyprint());
            this.conversation.stats.hit('sabrina-command-maybe');
        }
        else {
            this.debug('Confidently analyzed message into ' + choice.parsed.prettyprint());
            this.conversation.stats.hit('sabrina-command-good');
        }
        return {
            type,
            utterance: command.utterance,
            answer: this._maybeGetThingTalkAnswer(choice.parsed),
            parsed: choice.parsed
        };
    }
    _useNeuralNLG() {
        return this._prefs.get('experimental-use-neural-nlg');
    }
    async _doAgentReply() {
        const oldState = this._dialogueState;
        if (oldState)
            this.conversation.updateLog('context', oldState.prettyprint());
        const policyResult = await this._policy.chooseAction(this._dialogueState);
        if (!policyResult) {
            await this.fail();
            throw new errors_1.CancellationError();
        }
        let expect, utterance, entities, numResults;
        [this._dialogueState, expect, utterance, entities, numResults] = policyResult;
        const policyPrediction = ThingTalkUtils.computePrediction(oldState, this._dialogueState, 'agent');
        const agentTarget = policyPrediction.prettyprint();
        this.conversation.updateLog('agent_target', agentTarget);
        this.debug(`Agent act:`);
        this.debug(agentTarget);
        if (this._useNeuralNLG()) {
            const [contextCode, contextEntities] = this._prepareContextForPrediction(this._dialogueState, 'agent');
            const [targetAct,] = ThingTalkUtils.serializeNormalized(policyPrediction, contextEntities);
            const result = await this._nlg.generateUtterance(contextCode, contextEntities, targetAct);
            utterance = result[0].answer;
        }
        utterance = this._langPack.postprocessNLG(utterance, entities, this._agent);
        this.icon = icons_1.getProgramIcon(this._dialogueState);
        await this.reply(utterance);
        if (expect === null && TERMINAL_STATES.includes(this._dialogueState.dialogueAct))
            throw new errors_1.CancellationError();
        return [expect, numResults];
    }
    async _handleUICommand(type) {
        switch (type) {
            case CommandAnalysisType.STOP:
                // stop means cancel, but without a failure message
                throw new errors_1.CancellationError();
            case CommandAnalysisType.NEVERMIND:
                await this.reply(this._("Sorry I couldn't help on that."));
                throw new errors_1.CancellationError();
            case CommandAnalysisType.DEBUG:
                await this.reply("Current State:\n" + (this._dialogueState ? this._dialogueState.prettyprint() : "null"));
                break;
            case CommandAnalysisType.WAKEUP:
                // "wakeup" means the user said "hey almond" without anything else,
                // or said "hey almond wake up", or triggered one of the LaunchIntents
                // in Google Assistant or Alexa, or similar "opening" statements
                // we show the welcome message if the current state is null,
                // and do nothing otherwise
                if (this._dialogueState === null) {
                    this._showWelcome();
                    // keep the microphone open for a while
                    await this.setExpected(value_category_1.default.Command);
                }
            case CommandAnalysisType.IGNORE:
                // do exactly nothing
                break;
        }
    }
    async _describeProgram(program) {
        const allocator = new thingtalk_1.Syntax.SequentialEntityAllocator({});
        const describer = new ThingTalkUtils.Describer(this.conversation.locale, this.conversation.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));
        return this._langPack.postprocessNLG(describer.describe(program), allocator.entities, this._agent);
    }
    async _handleUserInput(command) {
        for (;;) {
            const analyzed = await this._analyzeCommand(command);
            if (analyzed.type !== CommandAnalysisType.IGNORE) {
                // save the utterance and complete the turn
                // skip the log if the command was ignored
                this.conversation.updateLog('user', analyzed.utterance);
                this.conversation.updateLog('user_target', analyzed.parsed.prettyprint());
                this.conversation.turnFinished();
            }
            switch (analyzed.type) {
                case CommandAnalysisType.STOP:
                case CommandAnalysisType.NEVERMIND:
                case CommandAnalysisType.DEBUG:
                case CommandAnalysisType.WAKEUP:
                case CommandAnalysisType.IGNORE:
                    await this._handleUICommand(analyzed.type);
                    break;
                case CommandAnalysisType.PARSE_FAILURE:
                    await this.fail();
                    break;
                case CommandAnalysisType.OUT_OF_DOMAIN_COMMAND:
                    // TODO dispatch this out
                    await this.reply(this._("Sorry, I don't know how to do that yet."));
                    throw new errors_1.CancellationError();
                case CommandAnalysisType.NONCONFIDENT_IN_DOMAIN_COMMAND: {
                    // TODO move this to the state machine, not here
                    const confirmation = await this._describeProgram(analyzed.parsed);
                    const question = this.interpolate(this._("Did you mean ${command}?"), { command: confirmation });
                    const yesNo = await this.ask(value_category_1.default.YesNo, question);
                    assert_1.default(yesNo instanceof thingtalk_1.Ast.BooleanValue);
                    if (!yesNo.value) {
                        // preserve the dialogue state and the expecting state here
                        await this.reply(this._("Sorry I couldn't help on that. Would you like to try again?"));
                        continue;
                    }
                    // fallthrough to the confident case
                }
                case CommandAnalysisType.CONFIDENT_IN_DOMAIN_COMMAND:
                default: {
                    // everything else is an in-domain command
                    const prediction = await ThingTalkUtils.inputToDialogueState(this._policy, this._dialogueState, analyzed.parsed);
                    if (prediction === null) {
                        // the command does not make sense in the current state
                        // do nothing and keep the current state
                        // (this can only occur with commands caught by the exact
                        // matcher like "yes" or "no")
                        await this.fail();
                        break;
                    }
                    await this._handleNormalDialogueCommand(prediction);
                }
            }
            // if we're not expecting any more answer from the user,
            // exit this loop
            // note: this does not mean the dialogue is terminated!
            // state is preserved until we call reset() due to context reset
            // timeout, or some command causes a CancellationError
            // (typically, "never mind", or a "no" in sys_anything_else)
            //
            // exiting this loop means that we close the microphone
            // (requiring a wakeword again to continue) and start
            // processing notifications again
            if (this.expecting === null)
                return;
            command = await this.nextCommand();
        }
    }
    async _handleNormalDialogueCommand(prediction) {
        this._dialogueState = ThingTalkUtils.computeNewState(this._dialogueState, prediction, 'user');
        this._checkPolicy(this._dialogueState.policy);
        this.icon = icons_1.getProgramIcon(this._dialogueState);
        //this.debug(`Before execution:`);
        //this.debug(this._dialogueState.prettyprint());
        const { newDialogueState, newExecutorState, newResults } = await this._agent.execute(this._dialogueState, this._executorState);
        this._dialogueState = newDialogueState;
        this._executorState = newExecutorState;
        this.debug(`Execution state:`);
        this.debug(this._dialogueState.prettyprint());
        const [expect, numResults] = await this._doAgentReply();
        for (const [outputType, outputValue] of newResults.slice(0, numResults)) {
            const formatted = await this._cardFormatter.formatForType(outputType, outputValue, { removeText: true });
            for (const card of formatted)
                await this.replyCard(card);
        }
        await this.setExpected(expect);
    }
    async _showNotification(appId, icon, outputType, outputValue) {
        let app;
        if (appId !== undefined)
            app = this.conversation.apps.getApp(appId);
        else
            app = undefined;
        const messages = await this._textFormatter.formatForType(outputType, outputValue, 'messages');
        if (app !== undefined && app.isRunning && appId !== this._lastNotificationApp &&
            (messages.length === 1 && typeof messages[0] === 'string')) {
            await this.replyInterp(this._("Notification from ${app}: ${message}"), {
                app: app.name,
                message: messages[0]
            }, icon);
        }
        else {
            if (app !== undefined && app.isRunning && appId !== this._lastNotificationApp)
                await this.replyInterp(this._("Notification from ${app}"), { app: app.name }, icon);
            for (const msg of messages)
                await this.replyCard(msg, icon);
        }
    }
    async _showAsyncError(appId, icon, error) {
        let app;
        if (appId !== undefined)
            app = this.conversation.apps.getApp(appId);
        else
            app = undefined;
        const errorMessage = Helpers.formatError(this, error);
        console.log('Error from ' + appId, error);
        if (app !== undefined && app.isRunning)
            await this.replyInterp(this._("${app} had an error: ${error}."), { app: app.name, error: errorMessage }, icon);
        else
            await this.replyInterp(this._("Sorry, that did not work: ${error}."), { error: errorMessage }, icon);
    }
    async _handleAPICall(call) {
        if (call instanceof dialogue_queue_1.default.Notification) {
            await this._showNotification(call.appId, call.icon, call.outputType, call.outputValue);
            this._lastNotificationApp = call.appId;
        }
        else if (call instanceof dialogue_queue_1.default.Error) {
            await this._showAsyncError(call.appId, call.icon, call.error);
            this._lastNotificationApp = call.appId;
        }
    }
    async _showWelcome() {
        await this._doAgentReply();
        // reset the dialogue state here; if we don't, we we'll see sys_greet as an agent
        // dialogue act; this is never seen in training, because in training the user speaks
        // first, so it confuses the neural network
        this._dialogueState = null;
        // the utterance ends with "what can i do for you?", which is expect = 'generic'
        // but we don't want to keep the microphone open here, we want to go back to wake-word mode
        // so we unconditionally close the round here
        await this.setExpected(null);
    }
    async _loop(showWelcome) {
        // if we want to show the welcome message, we run the policy on the `null` state, which will return the sys_greet intent
        if (showWelcome)
            await this._showWelcome();
        while (!this._stopped) {
            let item;
            try {
                item = await this.nextQueueItem();
                if (item instanceof dialogue_queue_1.default.UserInput) {
                    this._lastNotificationApp = undefined;
                    await this._handleUserInput(item.command);
                }
                else {
                    await this._handleAPICall(item);
                    this.conversation.dialogueFinished();
                    this._dialogueState = null;
                }
            }
            catch (e) {
                if (e.code === 'ECANCELLED') {
                    this.icon = null;
                    this._dialogueState = null;
                    await this.setExpected(null);
                    // if the dialogue terminated, save the last utterance from the agent
                    // in a new turn with an empty utterance from the user
                    this.conversation.dialogueFinished();
                }
                else {
                    if (item instanceof dialogue_queue_1.default.UserInput) {
                        await this.replyInterp(this._("Sorry, I had an error processing your command: ${error}."), {
                            error: Helpers.formatError(this, e)
                        });
                    }
                    else {
                        await this.replyInterp(this._("Sorry, that did not work: ${error}."), {
                            error: Helpers.formatError(this, e)
                        });
                    }
                    console.error(e);
                }
            }
        }
    }
    get dialogueState() {
        return this._dialogueState;
    }
    set dialogueState(newState) {
        this._dialogueState = newState;
    }
    async nextQueueItem() {
        this.setExpected(null);
        await this.conversation.sendAskSpecial();
        this._mgrPromise = null;
        this._mgrResolve();
        const queueItem = await this._notifyQueue.pop();
        if (queueItem instanceof dialogue_queue_1.default.UserInput)
            this.platformData = queueItem.command.platformData;
        else
            this.platformData = {};
        return queueItem;
    }
    async lookingFor() {
        if (this.expecting === null) {
            await this.reply(this._("In fact, I did not ask for anything at all!"));
        }
        else if (this.expecting === value_category_1.default.YesNo) {
            await this.reply(this._("Sorry, I need you to confirm the last question first."));
        }
        else if (this.expecting === value_category_1.default.MultipleChoice) {
            await this.reply(this._("Could you choose one of the following?"));
            await this._resendChoices();
        }
        else if (this.expecting === value_category_1.default.Measure) {
            await this.reply(this._("Could you give me a measurement?"));
        }
        else if (this.expecting === value_category_1.default.Number) {
            await this.reply(this._("Could you give me a number?"));
        }
        else if (this.expecting === value_category_1.default.Date) {
            await this.reply(this._("Could you give me a date?"));
        }
        else if (this.expecting === value_category_1.default.Time) {
            await this.reply(this._("Could you give me a time of day?"));
        }
        else if (this.expecting === value_category_1.default.Picture) {
            await this.reply(this._("Could you upload a picture?"));
        }
        else if (this.expecting === value_category_1.default.Location) {
            await this.reply(this._("Could you give me a place?"));
        }
        else if (this.expecting === value_category_1.default.PhoneNumber) {
            await this.reply(this._("Could you give me a phone number?"));
        }
        else if (this.expecting === value_category_1.default.EmailAddress) {
            await this.reply(this._("Could you give me an email address?"));
        }
        else if (this.expecting === value_category_1.default.RawString || this.expecting === value_category_1.default.Password) {
            // ValueCategory.RawString puts Almond in raw mode,
            // so we accept almost everything
            // but this will happen if the user clicks a button
            // or upload a picture
            await this.reply(this._("Which is interesting, because I'll take anything at all. Just type your mind!"));
        }
        else if (this.expecting === value_category_1.default.Command) {
            await this.reply(this._("I'm looking for a command."));
        }
        else {
            await this.reply(this._("In fact, I'm not even sure what I asked. Sorry!"));
        }
    }
    async fail(msg) {
        if (this.expecting === null) {
            if (msg) {
                await this.replyInterp(this._("Sorry, I did not understand that: ${error}. Can you rephrase it?"), {
                    error: msg
                });
            }
            else {
                await this.reply(this._("Sorry, I did not understand that. Can you rephrase it?"));
            }
        }
        else {
            if (msg)
                await this.replyInterp(this._("Sorry, I did not understand that: ${error}."), { error: msg });
            else
                await this.reply(this._("Sorry, I did not understand that."));
        }
        throw new errors_1.CancellationError();
    }
    setExpected(expected, raw = (expected === value_category_1.default.RawString || expected === value_category_1.default.Password)) {
        if (expected === undefined)
            throw new TypeError();
        this.expecting = expected;
        this._raw = raw;
        const [contextCode, contextEntities] = this._prepareContextForPrediction(this._dialogueState, 'user');
        this.conversation.setExpected(expected, { code: contextCode, entities: contextEntities });
    }
    /**
     * Ask a question to the user.
     *
     * This is a legacy method used for certain scripted interactions.
     */
    async ask(expected, question, args) {
        await this.replyInterp(question, args);
        // force the question to occur in raw mode for locations
        // because otherwise we send it to the parser and the parser will
        // likely misbehave as it's a state that we've never seen in training
        await this.setExpected(expected, expected === value_category_1.default.Location);
        let analyzed = await this._analyzeCommand(await this.nextCommand());
        while (analyzed.answer === null || typeof analyzed.answer === 'number' ||
            value_category_1.default.fromType(analyzed.answer.getType()) !== expected) {
            switch (analyzed.type) {
                case CommandAnalysisType.STOP:
                case CommandAnalysisType.NEVERMIND:
                case CommandAnalysisType.DEBUG:
                case CommandAnalysisType.WAKEUP:
                case CommandAnalysisType.IGNORE:
                    await this._handleUICommand(analyzed.type);
                    break;
                default:
                    await this.fail();
                    await this.lookingFor();
            }
            analyzed = await this._analyzeCommand(await this.nextCommand());
        }
        return analyzed.answer;
    }
    async askChoices(question, choices) {
        await this.reply(question);
        this.setExpected(value_category_1.default.MultipleChoice);
        this._choices = choices;
        for (let i = 0; i < choices.length; i++)
            await this.conversation.sendChoice(i, choices[i]);
        let analyzed = await this._analyzeCommand(await this.nextCommand());
        while (analyzed.answer === null || typeof analyzed.answer !== 'number'
            || analyzed.answer < 0 || analyzed.answer >= choices.length) {
            switch (analyzed.type) {
                case CommandAnalysisType.STOP:
                case CommandAnalysisType.NEVERMIND:
                case CommandAnalysisType.DEBUG:
                case CommandAnalysisType.WAKEUP:
                case CommandAnalysisType.IGNORE:
                    await this._handleUICommand(analyzed.type);
                    break;
                default:
                    await this.fail();
                    await this.lookingFor();
            }
            analyzed = await this._analyzeCommand(await this.nextCommand());
        }
        return analyzed.answer;
    }
    async _resendChoices() {
        if (this.expecting !== value_category_1.default.MultipleChoice)
            console.log('UNEXPECTED: sendChoice while not expecting a MultipleChoice');
        for (let idx = 0; idx < this._choices.length; idx++)
            await this.conversation.sendChoice(idx, this._choices[idx]);
    }
    async replyInterp(msg, args, icon = null) {
        if (args === undefined)
            return this.reply(msg, icon);
        else
            return this.reply(this.interpolate(msg, args), icon);
    }
    async reply(msg, icon) {
        this.conversation.updateLog('agent', msg);
        await this.conversation.sendReply(msg, icon || this.icon);
    }
    async replyCard(message, icon) {
        if (typeof message === 'string') {
            await this.reply(message, icon);
        }
        else if (message.type === 'picture') {
            if (message.url === undefined)
                return;
            await this.conversation.sendPicture(message.url, icon || this.icon);
        }
        else if (message.type === 'rdl') {
            await this.conversation.sendRDL(message, icon || this.icon);
        }
        else if (message.type === 'button') {
            const loaded = await Helpers.loadSuggestedProgram(message.code, this.conversation.schemas);
            await this.replyButton(message.title, JSON.stringify(loaded));
        }
        else {
            await this.conversation.sendResult(message, icon || this.icon);
        }
    }
    async replyButton(text, json) {
        await this.conversation.sendButton(text, json);
    }
    async replyLink(title, url) {
        await this.conversation.sendLink(title, url);
    }
    _isInDefaultState() {
        return this._notifyQueue.hasWaiter();
    }
    dispatchNotify(appId, icon, outputType, outputValue) {
        const item = new dialogue_queue_1.default.Notification(appId, icon, outputType, outputValue);
        this._pushQueueItem(item);
    }
    dispatchNotifyError(appId, icon, error) {
        const item = new dialogue_queue_1.default.Error(appId, icon, error);
        this._pushQueueItem(item);
    }
    async start(showWelcome) {
        await this._nlu.start();
        await this._nlg.start();
        const promise = this._waitNextCommand();
        this._loop(showWelcome).then(() => {
            throw new Error('Unexpected end of dialog loop');
        }, (err) => {
            console.error('Uncaught error in dialog loop', err);
            throw err;
        });
        return promise;
    }
    async stop() {
        this._stopped = true;
        // wait until the dialog is ready to accept commands, then inject
        // a cancellation error
        await this._mgrPromise;
        assert_1.default(this._mgrPromise === null);
        if (this._isInDefaultState())
            this._notifyQueue.cancelWait(new errors_1.CancellationError());
        else
            this._userInputQueue.cancelWait(new errors_1.CancellationError());
        await this._nlu.stop();
        await this._nlg.stop();
    }
    async reset() {
        // wait until the dialog is ready to accept commands
        await this._mgrPromise;
        assert_1.default(this._mgrPromise === null);
        if (this._isInDefaultState())
            this._notifyQueue.cancelWait(new errors_1.CancellationError());
        else
            this._userInputQueue.cancelWait(new errors_1.CancellationError());
    }
    _pushQueueItem(item) {
        // ensure that we have something to wait on before the next
        // command is handled
        if (!this._mgrPromise)
            this._waitNextCommand();
        this._notifyQueue.push(item);
    }
    /**
     * Returns a promise that will resolve when the dialogue loop is
     * ready to accept the next command from the user.
     */
    _waitNextCommand() {
        const promise = new Promise((callback, errback) => {
            this._mgrResolve = callback;
        });
        this._mgrPromise = promise;
        return promise;
    }
    pushCommand(command) {
        this._pushQueueItem(new dialogue_queue_1.default.UserInput(command));
    }
    async handleCommand(command) {
        // wait until the dialog is ready to accept commands
        await this._mgrPromise;
        assert_1.default(this._mgrPromise === null);
        const promise = this._waitNextCommand();
        if (this._isInDefaultState())
            this.pushCommand(command);
        else
            this._userInputQueue.push(command);
        return promise;
    }
}
exports.default = DialogueLoop;
//# sourceMappingURL=dialogue-loop.js.map