"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.setOrAddInvocationParam = exports.mergeParameters = exports.replaceAction = exports.addQueryAndAction = exports.addQuery = exports.addAction = exports.addActionParam = exports.addNewItem = exports.makeSimpleState = exports.sortByName = exports.setEndBit = exports.makeAgentReply = exports.getContextPhrases = exports.tagContextForAgent = exports.makeExpressionContextPhrase = exports.makeContextPhrase = exports.isUserAskingResultQuestion = exports.getContextInfo = exports.initialContextInfo = exports.contextKeyFn = exports.ContextInfo = exports.NextStatementInfo = exports.ResultInfo = exports.POLICY_NAME = void 0;
const assert_1 = __importDefault(require("assert"));
const ThingTalk = __importStar(require("thingtalk"));
const thingtalk_1 = require("thingtalk");
const C = __importStar(require("./ast_manip"));
const keyfns = __importStar(require("./keyfns"));
const slot_bag_1 = require("./slot_bag");
// NOTE: this version of arraySubset uses ===
// the one in array_utils uses .equals()
// this one is called on array of strings, so === is appropriate
function arraySubset(small, big) {
    for (const element of small) {
        let good = false;
        for (const candidate of big) {
            if (candidate === element) {
                good = true;
                break;
            }
        }
        if (!good)
            return false;
    }
    return true;
}
// Helper classes for info that we extract from the current context
// These exist to minimize AST traversals during expansion
// NOTE: while ast_manip is mostly just about ThingTalk semantics, with
// a few heuristics sprinkled out, this is really only about the "transaction"
// dialogue policy
// hence we hard-code the policy name here, and check it before doing anything
// in the templates
// templates can be combined though
exports.POLICY_NAME = 'org.thingpedia.dialogue.transaction';
const LARGE_RESULT_THRESHOLD = 50;
function isLargeResultSet(result) {
    return result.more || !(result.count instanceof thingtalk_1.Ast.Value.Number) || result.count.value >= LARGE_RESULT_THRESHOLD;
}
function invertDirection(dir) {
    if (dir === 'asc')
        return 'desc';
    else
        return 'asc';
}
function getSortName(value) {
    if (value instanceof thingtalk_1.Ast.VarRefValue)
        return value.name;
    return C.getScalarExpressionName(value);
}
function getTableArgMinMax(table) {
    while (table instanceof thingtalk_1.Ast.ProjectionExpression)
        table = table.expression;
    // either an index of a sort, where the index is exactly 1 or -1
    // or a slice of a sort, with a base of 1 or -1, and a limit of 1
    // (both are equivalent and get normalized, but sometimes we don't run the normalization)
    if (table instanceof thingtalk_1.Ast.IndexExpression && table.expression instanceof thingtalk_1.Ast.SortExpression && table.indices.length === 1) {
        const index = table.indices[0];
        if (index instanceof thingtalk_1.Ast.Value.Number &&
            (index.value === 1 || index.value === -1))
            return [getSortName(table.expression.value), index.value === -1 ? invertDirection(table.expression.direction) : table.expression.direction];
    }
    if (table instanceof thingtalk_1.Ast.SliceExpression && table.expression instanceof thingtalk_1.Ast.SortExpression) {
        const { base, limit } = table;
        if (base instanceof thingtalk_1.Ast.Value.Number && (base.value === 1 || base.value === -1) &&
            limit instanceof thingtalk_1.Ast.Value.Number && limit.value === 1)
            return [getSortName(table.expression.value), base.value === -1 ? invertDirection(table.expression.direction) : table.expression.direction];
    }
    return null;
}
class ResultInfo {
    constructor(state, item) {
        assert_1.default(item.results !== null);
        const stmt = item.stmt;
        assert_1.default(stmt.stream === null);
        this.isTable = stmt.last.schema.functionType === 'query';
        if (this.isTable) {
            const table = stmt.lastQuery;
            this.isQuestion = !!(table instanceof thingtalk_1.Ast.ProjectionExpression
                || table instanceof thingtalk_1.Ast.IndexExpression
                || table instanceof thingtalk_1.Ast.AggregationExpression);
            this.isAggregation = table instanceof thingtalk_1.Ast.AggregationExpression;
            this.isList = table.schema.is_list;
            this.argMinMaxField = getTableArgMinMax(table);
            assert_1.default(this.argMinMaxField === null || this.isQuestion);
            this.projection = table instanceof thingtalk_1.Ast.ProjectionExpression ?
                C.getProjectionArguments(table) : null;
            if (this.projection)
                this.projection.sort();
        }
        else {
            this.isQuestion = false;
            this.isAggregation = false;
            this.isList = false;
            this.argMinMaxField = null;
            this.projection = null;
            if (state.dialogueAct === 'action_question')
                this.projection = state.dialogueActParam;
        }
        this.hasError = item.results.error !== null;
        this.hasEmptyResult = item.results.results.length === 0;
        this.hasSingleResult = item.results.results.length === 1;
        this.hasLargeResult = isLargeResultSet(item.results);
        const id = stmt.last.schema.getArgument('id');
        this.idType = id && !id.is_input ? id.type : null;
    }
}
exports.ResultInfo = ResultInfo;
class NextStatementInfo {
    constructor(currentItem, resultInfo, nextItem) {
        const nextstmt = nextItem.stmt;
        this.isAction = !nextstmt.lastQuery;
        this.chainParameter = null;
        this.chainParameterFilled = false;
        this.isComplete = nextItem.isExecutable();
        if (!this.isAction)
            return;
        assert_1.default(nextItem.stmt.expression.expressions.length === 1);
        const action = nextItem.stmt.first;
        assert_1.default(action instanceof thingtalk_1.Ast.InvocationExpression);
        if (!currentItem || !resultInfo || !resultInfo.isTable)
            return;
        const currentstmt = currentItem.stmt;
        const tableschema = currentstmt.expression.schema;
        const idType = tableschema.getArgType('id');
        if (!idType)
            return;
        const invocation = action.invocation;
        const actionschema = invocation.schema;
        for (const arg of actionschema.iterateArguments()) {
            if (!arg.is_input)
                continue;
            if (arg.type.equals(idType)) {
                this.chainParameter = arg.name;
                break;
            }
        }
        if (this.chainParameter === null)
            return;
        for (const in_param of invocation.in_params) {
            if (in_param.name === this.chainParameter && !in_param.value.isUndefined) {
                this.chainParameterFilled = true;
                break;
            }
        }
    }
}
exports.NextStatementInfo = NextStatementInfo;
function toID(value) {
    if (value === undefined)
        return null;
    const jsValue = value.toJS();
    if (typeof jsValue === 'number')
        return jsValue;
    else if (jsValue === null || jsValue === undefined)
        return null;
    else
        return String(jsValue);
}
class ContextInfo {
    constructor(loader, contextTable, state, currentTableSchema, currentFunctionSchema, resultInfo, previousDomainIdx, currentIdx, nextIdx, nextFunctionSchema, nextInfo, aux = null) {
        this.loader = loader;
        this.contextTable = contextTable;
        this.state = state;
        assert_1.default(currentFunctionSchema === null || currentFunctionSchema instanceof thingtalk_1.Ast.FunctionDef);
        this.currentFunction = currentFunctionSchema;
        assert_1.default(currentTableSchema === null || currentTableSchema instanceof thingtalk_1.Ast.FunctionDef);
        this.currentTableFunction = currentTableSchema;
        this.resultInfo = resultInfo;
        this.isMultiDomain = previousDomainIdx !== null;
        this.previousDomainIdx = previousDomainIdx;
        this.currentIdx = currentIdx;
        assert_1.default(nextFunctionSchema === null || nextFunctionSchema instanceof thingtalk_1.Ast.FunctionDef);
        this.nextFunction = nextFunctionSchema;
        this.nextIdx = nextIdx;
        this.nextInfo = nextInfo;
        this.aux = aux;
        this.key = {
            currentFunction: this.currentFunction ? this.currentFunction.qualifiedName : null,
            nextFunction: this.nextFunction ? this.nextFunction.qualifiedName : null,
            currentTableFunction: this.currentTableFunction ? this.currentTableFunction.qualifiedName : null,
            idType: null,
            id0: null,
            id1: null,
            id2: null,
            resultLength: 0
        };
        if (this.resultInfo) {
            this.key.idType = this.resultInfo.idType;
            const results = this.results;
            this.key.resultLength = results.length;
            if (results.length > 0)
                this.key.id0 = toID(results[0].value.id);
            if (results.length > 1)
                this.key.id1 = toID(results[1].value.id);
            if (results.length > 2)
                this.key.id2 = toID(results[2].value.id);
        }
    }
    toString() {
        return `ContextInfo(${this.state.prettyprint()})`;
    }
    get results() {
        if (this.currentIdx !== null)
            return this.state.history[this.currentIdx].results.results;
        return null;
    }
    get error() {
        if (this.currentIdx !== null)
            return this.state.history[this.currentIdx].results.error;
        return null;
    }
    get previousDomain() {
        return this.previousDomainIdx !== null ? this.state.history[this.previousDomainIdx] : null;
    }
    get current() {
        return this.currentIdx !== null ? this.state.history[this.currentIdx] : null;
    }
    get next() {
        return this.nextIdx !== null ? this.state.history[this.nextIdx] : null;
    }
    clone() {
        return new ContextInfo(this.loader, this.contextTable, this.state.clone(), this.currentTableFunction, this.currentFunction, this.resultInfo, this.previousDomainIdx, this.currentIdx, this.nextIdx, this.nextFunction, this.nextInfo, this.aux);
    }
}
exports.ContextInfo = ContextInfo;
function contextKeyFn(ctx) {
    return ctx.key;
}
exports.contextKeyFn = contextKeyFn;
function initialContextInfo(loader, contextTable) {
    return new ContextInfo(loader, contextTable, new thingtalk_1.Ast.DialogueState(null, exports.POLICY_NAME, 'sys_init', [], []), null, null, null, null, null, null, null, null);
}
exports.initialContextInfo = initialContextInfo;
function getContextInfo(loader, state, contextTable) {
    let nextItemIdx = null, nextInfo = null, currentFunction = null, currentTableFunction = null, nextFunction = null, currentDevice = null, currentResultInfo = null, previousDomainItemIdx = null, currentItemIdx = null;
    let proposedSkip = 0;
    for (let idx = 0; idx < state.history.length; idx++) {
        const item = state.history[idx];
        const functions = C.getFunctions(item.stmt);
        const device = functions[functions.length - 1].class.name;
        assert_1.default(typeof device === 'string');
        if (currentDevice && device !== currentDevice)
            previousDomainItemIdx = currentItemIdx;
        if (item.confirm === 'proposed') {
            proposedSkip++;
            continue;
        }
        if (item.results === null) {
            nextItemIdx = idx;
            nextFunction = functions[functions.length - 1];
            nextInfo = new NextStatementInfo(currentItemIdx !== null ? state.history[currentItemIdx] : null, currentResultInfo, item);
            break;
        }
        // proposed items must come after the current item
        // (but they can come before or after the next item, depending on what we're proposing)
        assert_1.default(proposedSkip === 0);
        currentDevice = device;
        currentFunction = functions[functions.length - 1];
        const stmt = item.stmt;
        const lastQuery = stmt.lastQuery;
        if (lastQuery) {
            const tablefunctions = C.getFunctions(lastQuery);
            currentTableFunction = tablefunctions[tablefunctions.length - 1];
        }
        currentItemIdx = idx;
        currentResultInfo = new ResultInfo(state, item);
    }
    if (nextItemIdx !== null)
        assert_1.default(nextInfo);
    if (nextItemIdx !== null && currentItemIdx !== null)
        assert_1.default(nextItemIdx === currentItemIdx + 1 + proposedSkip);
    if (previousDomainItemIdx !== null)
        assert_1.default(currentItemIdx !== null && previousDomainItemIdx <= currentItemIdx);
    return new ContextInfo(loader, contextTable, state, currentTableFunction, currentFunction, currentResultInfo, previousDomainItemIdx, currentItemIdx, nextItemIdx, nextFunction, nextInfo);
}
exports.getContextInfo = getContextInfo;
function isUserAskingResultQuestion(ctx) {
    // is the user asking a question about the result (or a specific element), or refining a search?
    // we say it's a question if the user is asking a projection question, and it's not the first turn,
    // and the projection was different at the previous turn
    // we also treat it as a question for all compute questions because that simplifies
    // writing the templates
    if (ctx.state.dialogueAct === 'action_question')
        return true;
    if (ctx.currentIdx === null)
        return false;
    const currentStmt = ctx.current.stmt;
    const currentTable = currentStmt.lastQuery;
    if (!currentTable)
        return false;
    if (currentTable instanceof thingtalk_1.Ast.ProjectionExpression && currentTable.computations.length > 0)
        return true;
    if (ctx.currentIdx === 0) {
        const filterTable = C.findFilterExpression(currentStmt.expression);
        if (!filterTable)
            return false;
        return C.filterUsesParam(filterTable.filter, 'id');
    }
    const currentProjection = ctx.resultInfo.projection;
    if (!currentProjection)
        return false;
    const previous = ctx.state.history[ctx.currentIdx - 1];
    // only complete (executed) programs make it to the history, so this must be true
    assert_1.default(previous.results !== null);
    const previousResultInfo = new ResultInfo(ctx.state, previous);
    if (!previousResultInfo.projection)
        return true;
    // it's a question if the current projection is not a subset of the previous one
    // (for a search refinement: it might be exactly the same as before, or we might have
    // lost some parameters because we put a filter on it)
    return !arraySubset(currentProjection, previousResultInfo.projection);
}
exports.isUserAskingResultQuestion = isUserAskingResultQuestion;
function addNewItem(ctx, dialogueAct, dialogueActParam, confirm, ...newHistoryItem) {
    newHistoryItem = newHistoryItem.map(C.adjustDefaultParameters);
    const newState = new thingtalk_1.Ast.DialogueState(null, exports.POLICY_NAME, dialogueAct, dialogueActParam, []);
    if (confirm === 'proposed') {
        // find the first item that was not confirmed or accepted, and replace everything after that
        for (let i = 0; i < ctx.state.history.length; i++) {
            if (ctx.state.history[i].confirm === 'proposed')
                break;
            newState.history.push(ctx.state.history[i]);
        }
        newState.history.push(...newHistoryItem);
    }
    else {
        // wipe everything from state after the current program
        // this will remove all previously accepted and/or proposed actions
        //
        // XXX is the right thing to do?
        if (ctx.currentIdx !== null) {
            for (let i = 0; i <= ctx.currentIdx; i++)
                newState.history.push(ctx.state.history[i]);
        }
        newState.history.push(...newHistoryItem);
    }
    return newState;
}
exports.addNewItem = addNewItem;
function makeSimpleState(ctx, dialogueAct, dialogueActParam) {
    // a "simple state" carries the current executed/confirmed/accepted items, but not the
    // proposed ones
    const newState = new thingtalk_1.Ast.DialogueState(null, exports.POLICY_NAME, dialogueAct, dialogueActParam, []);
    for (let i = 0; i < ctx.state.history.length; i++) {
        if (ctx.state.history[i].confirm === 'proposed')
            break;
        newState.history.push(ctx.state.history[i]);
    }
    return newState;
}
exports.makeSimpleState = makeSimpleState;
function sortByName(p1, p2) {
    if (p1.name < p2.name)
        return -1;
    if (p1.name > p2.name)
        return 1;
    return 0;
}
exports.sortByName = sortByName;
function setOrAddInvocationParam(newInvocation, pname, value) {
    let found = false;
    for (const in_param of newInvocation.in_params) {
        if (in_param.name === pname) {
            found = true;
            in_param.value = value;
            break;
        }
    }
    if (!found) {
        newInvocation.in_params.push(new thingtalk_1.Ast.InputParam(null, pname, value));
        newInvocation.in_params.sort(sortByName);
    }
}
exports.setOrAddInvocationParam = setOrAddInvocationParam;
function mergeParameters(toInvocation, fromInvocation) {
    for (const in_param of fromInvocation.in_params) {
        if (in_param.value.isUndefined)
            continue;
        setOrAddInvocationParam(toInvocation, in_param.name, in_param.value);
    }
    return toInvocation;
}
exports.mergeParameters = mergeParameters;
function addActionParam(ctx, dialogueAct, action, pname, value, confirm) {
    assert_1.default(action instanceof thingtalk_1.Ast.Invocation);
    assert_1.default(['accepted', 'confirmed', 'proposed'].indexOf(confirm) >= 0);
    let newHistoryItem;
    if (ctx.nextInfo) {
        const next = ctx.next;
        assert_1.default(next);
        const nextInvocation = C.getInvocation(next);
        const isSameFunction = C.isSameFunction(nextInvocation.schema, action.schema);
        if (isSameFunction) {
            // we want to modify the existing action in case:
            // - case 1: we're currently accepting/confirming the action (perhaps with the same or
            //   a different parameter)
            // - case 2: we're proposing the same action that was proposed before
            //
            // to carry over parameters, we actually clone the statement and set the parameter
            // if confirm == "proposed":
            //   addNewItem() will add at the end, after the currently accepted
            //   item, and we'll have two actions (one "accepted" and one "proposed"), or just one "proposed" action
            // if confirm == "accepted":
            //   addNewItem() will wipe everything and we'll only one
            newHistoryItem = next.clone();
            const newInvocation = C.getInvocation(newHistoryItem);
            setOrAddInvocationParam(newInvocation, pname, value);
            // also add the new parameters from this action, if any
            for (const param of action.in_params) {
                if (param.value.isUndefined)
                    continue;
                setOrAddInvocationParam(newInvocation, param.name, param.value);
            }
            newHistoryItem.confirm = confirm;
        }
    }
    if (!newHistoryItem) {
        const in_params = [new thingtalk_1.Ast.InputParam(null, pname, value)];
        const setparams = new Set;
        setparams.add(pname);
        for (const param of action.in_params) {
            if (param.value.isUndefined)
                continue;
            if (param.name !== pname)
                in_params.push(param.clone());
            setparams.add(param.name);
        }
        const schema = action.schema;
        // make sure we add all $undefined values, otherwise we'll fail
        // to recognize that the statement is not yet executable, and we'll
        // crash in the compiler
        for (const arg of schema.iterateArguments()) {
            if (arg.is_input && arg.required && !setparams.has(arg.name))
                in_params.push(new thingtalk_1.Ast.InputParam(null, arg.name, new thingtalk_1.Ast.Value.Undefined(true)));
        }
        const newInvocation = new thingtalk_1.Ast.Invocation(null, action.selector, action.channel, in_params, schema);
        const newStmt = new thingtalk_1.Ast.ExpressionStatement(null, new thingtalk_1.Ast.InvocationExpression(null, newInvocation, schema.removeArgument(pname)));
        newHistoryItem = new thingtalk_1.Ast.DialogueHistoryItem(null, newStmt, null, confirm);
    }
    return addNewItem(ctx, dialogueAct, null, confirm, newHistoryItem);
}
exports.addActionParam = addActionParam;
function replaceAction(ctx, dialogueAct, action, confirm) {
    const newStmt = new thingtalk_1.Ast.ExpressionStatement(null, new thingtalk_1.Ast.InvocationExpression(null, action, action.schema));
    const newHistoryItem = new thingtalk_1.Ast.DialogueHistoryItem(null, newStmt, null, confirm);
    return addNewItem(ctx, dialogueAct, null, confirm, newHistoryItem);
}
exports.replaceAction = replaceAction;
function addAction(ctx, dialogueAct, action, confirm) {
    assert_1.default(action instanceof thingtalk_1.Ast.Invocation);
    // note: parameters from the action are ignored altogether!
    let newHistoryItem;
    if (ctx.nextInfo) {
        const next = ctx.next;
        assert_1.default(next);
        const nextInvocation = C.getInvocation(next);
        if (C.isSameFunction(nextInvocation.schema, action.schema)) {
            assert_1.default(next.results === null);
            // case 1:
            // - we trying to propose an action that the user has already introduced
            // earlier
            // in that case, we want to remember the action as accepted, not proposed
            // case 2:
            // - we trying to accept or confirm the action that was previously proposed
            // in that case, we want to change the action to accepted or confirmed
            if (confirm === 'proposed' || confirm === next.confirm)
                return new thingtalk_1.Ast.DialogueState(null, exports.POLICY_NAME, dialogueAct, null, ctx.state.history);
            newHistoryItem = new thingtalk_1.Ast.DialogueHistoryItem(null, next.stmt, null, confirm);
        }
    }
    if (!newHistoryItem) {
        const newInvocation = new thingtalk_1.Ast.Invocation(null, action.selector, action.channel, [], action.schema);
        const newStmt = new thingtalk_1.Ast.ExpressionStatement(null, new thingtalk_1.Ast.InvocationExpression(null, newInvocation, action.schema));
        newHistoryItem = new thingtalk_1.Ast.DialogueHistoryItem(null, newStmt, null, confirm);
    }
    return addNewItem(ctx, dialogueAct, null, confirm, newHistoryItem);
}
exports.addAction = addAction;
function addQuery(ctx, dialogueAct, newTable, confirm) {
    newTable = C.adjustDefaultParameters(newTable);
    const newStmt = new thingtalk_1.Ast.ExpressionStatement(null, newTable);
    const newHistoryItem = new thingtalk_1.Ast.DialogueHistoryItem(null, newStmt, null, confirm);
    // add the new history item right after the current one, and remove all proposed elements
    assert_1.default(ctx.currentIdx !== null);
    const newState = new thingtalk_1.Ast.DialogueState(null, exports.POLICY_NAME, dialogueAct, null, []);
    for (let i = 0; i <= ctx.currentIdx; i++)
        newState.history.push(ctx.state.history[i]);
    newState.history.push(newHistoryItem);
    for (let i = ctx.currentIdx + 1; i < ctx.state.history.length; i++) {
        if (ctx.state.history[i].confirm === 'proposed')
            continue;
        newState.history.push(ctx.state.history[i]);
    }
    return newState;
}
exports.addQuery = addQuery;
function addQueryAndAction(ctx, dialogueAct, newTable, newAction, confirm) {
    const newTableStmt = new thingtalk_1.Ast.ExpressionStatement(null, newTable);
    const newTableHistoryItem = new thingtalk_1.Ast.DialogueHistoryItem(null, newTableStmt, null, confirm);
    // add the new table history item right after the current one, and replace everything after that
    const newActionStmt = new thingtalk_1.Ast.ExpressionStatement(null, new thingtalk_1.Ast.InvocationExpression(null, newAction, newAction.schema));
    const newActionHistoryItem = new thingtalk_1.Ast.DialogueHistoryItem(null, newActionStmt, null, confirm);
    return addNewItem(ctx, dialogueAct, null, confirm, newTableHistoryItem, newActionHistoryItem);
}
exports.addQueryAndAction = addQueryAndAction;
function makeContextPhrase(symbol, value, utterance = '', priority = 0) {
    return { symbol, utterance, value, priority, key: value.key };
}
exports.makeContextPhrase = makeContextPhrase;
function makeExpressionContextPhrase(symbol, value, utterance, priority = 0) {
    return { symbol, utterance, value, priority, key: keyfns.expressionKeyFn(value) };
}
exports.makeExpressionContextPhrase = makeExpressionContextPhrase;
/**
 * Construct a full formal reply from the agent.
 *
 * The reply contains:
 * - the agent state (a ThingTalk dialogue state passed to the NLU and NLG networks)
 * - the agent reply tags (a list of strings that define the context tags on the user side)
 * - the interaction state (the expected type of the reply, if any, and a boolean indicating raw mode)
 * - extra information for the new context
 */
function makeAgentReply(ctx, state, aux = null, expectedType = null, options = {}) {
    const contextTable = ctx.contextTable;
    assert_1.default(state instanceof thingtalk_1.Ast.DialogueState);
    assert_1.default(state.dialogueAct.startsWith('sys_'));
    assert_1.default(expectedType === null || expectedType instanceof ThingTalk.Type);
    const newContext = getContextInfo(ctx.loader, state, contextTable);
    // set the auxiliary information, which is used by the semantic functions of the user
    // to see if the continuation is compatible with the specific reply from the agent
    newContext.aux = aux;
    let mainTag;
    if (state.dialogueAct === 'sys_generic_search_question')
        mainTag = contextTable.ctx_sys_search_question;
    else if (state.dialogueAct.endsWith('_question') && state.dialogueAct !== 'sys_search_question')
        mainTag = contextTable['ctx_' + state.dialogueAct.substring(0, state.dialogueAct.length - '_question'.length)];
    else if (state.dialogueAct.startsWith('sys_recommend_') && state.dialogueAct !== 'sys_recommend_one')
        mainTag = contextTable.ctx_sys_recommend_many;
    else
        mainTag = contextTable['ctx_' + state.dialogueAct];
    // if true, the interaction is done and the agent should stop listening
    // these dialogue acts are considered to "end" the conversation:
    // sys_recommend_*, sys_action_success, sys_action_error
    // provided no thingtalk statement is left to do (accepted or proposed)
    // the user can still continue, but the agent won't be listening unless woken up
    // (specific semantic functions can override)
    let end = options.end;
    if (end === undefined) {
        end = !state.history.some((item) => item.results === null) &&
            (state.dialogueAct.startsWith('sys_recommend_') ||
                ['sys_action_success', 'sys_action_error', 'sys_end', 'sys_display_result'].includes(state.dialogueAct));
    }
    return {
        state,
        context: newContext,
        contextPhrases: [
            makeContextPhrase(ctx.contextTable.ctx_sys_any, newContext),
            makeContextPhrase(mainTag, newContext),
            ...getContextPhrases(newContext)
        ],
        expect: expectedType,
        end: end,
        // if true, enter raw mode for this user's turn
        // (this is used for slot filling free-form strings)
        raw: !!options.raw,
        // the number of results we're describing at this turn
        // (this affects the number of result cards to show)
        numResults: options.numResults || 0,
    };
}
exports.makeAgentReply = makeAgentReply;
function setEndBit(reply, value) {
    const newReply = {};
    Object.assign(newReply, reply);
    newReply.end = value;
    return newReply;
}
exports.setEndBit = setEndBit;
function actionShouldHaveResult(ctx) {
    const schema = ctx.currentFunction;
    return Object.keys(schema.out).length > 0;
}
function tagContextForAgent(ctx) {
    const contextTable = ctx.contextTable;
    switch (ctx.state.dialogueAct) {
        case 'end':
            // no continuations are possible after explicit "end" (which means the user said
            // "no thanks" after the agent asked "is there anything else I can do for you")
            // but we still tag the context to generate something in inference mode
            return [contextTable.ctx_end];
        case 'greet':
            return [contextTable.ctx_greet];
        case 'reinit':
            return [contextTable.ctx_reinit];
        case 'cancel':
            return [contextTable.ctx_cancel];
        case 'action_question':
            return [contextTable.ctx_completed_action_success];
        case 'learn_more':
            assert_1.default(ctx.results);
            return [contextTable.ctx_learn_more];
        case 'execute':
        case 'ask_recommend':
            if (ctx.nextInfo !== null) {
                // we have an action we want to execute, or a query that needs confirmation
                if (ctx.nextInfo.chainParameter === null || ctx.nextInfo.chainParameterFilled) {
                    // we don't need to fill any parameter from the current query
                    if (ctx.nextInfo.isComplete)
                        return [contextTable.ctx_confirm_action];
                    else
                        return [contextTable.ctx_incomplete_action_after_search];
                }
            }
            // we must have a result
            assert_1.default(ctx.resultInfo, `expected result info`);
            if (!ctx.resultInfo.isTable) {
                if (ctx.resultInfo.hasError)
                    return [contextTable.ctx_completed_action_error];
                else if (ctx.resultInfo.hasEmptyResult && actionShouldHaveResult(ctx))
                    return [contextTable.ctx_empty_search_command];
                else
                    return [contextTable.ctx_completed_action_success];
            }
            if (ctx.resultInfo.hasEmptyResult) {
                // note: aggregation cannot be empty (it would be zero)
                return [contextTable.ctx_empty_search_command];
            }
            if (!ctx.resultInfo.isList) {
                return [contextTable.ctx_display_nonlist_result];
            }
            else if (ctx.resultInfo.isQuestion) {
                if (ctx.resultInfo.isAggregation) {
                    // "how many restaurants nearby have more than 500 reviews?"
                    return [contextTable.ctx_aggregation_question];
                }
                else if (ctx.resultInfo.argMinMaxField !== null) {
                    // these are treated as single result questions, but
                    // the context is tagged as ctx_with_result_argminmax instead of
                    // ctx_with_result_noquestion
                    // so the answer is worded differently
                    return [contextTable.ctx_single_result_search_command, contextTable.ctx_complete_search_command];
                }
                else if (ctx.resultInfo.hasSingleResult) {
                    // "what is the rating of Terun?"
                    // FIXME if we want to answer differently, we need to change this one
                    return [contextTable.ctx_single_result_search_command, contextTable.ctx_complete_search_command];
                }
                else if (ctx.resultInfo.hasLargeResult) {
                    // "what's the food and price range of restaurants nearby?"
                    // we treat these the same as "find restaurants nearby", but we make sure
                    // that the necessary fields are computed
                    return [contextTable.ctx_search_command, contextTable.ctx_complete_search_command];
                }
                else {
                    // "what's the food and price range of restaurants nearby?"
                    // we treat these the same as "find restaurants nearby", but we make sure
                    // that the necessary fields are computed
                    return [contextTable.ctx_complete_search_command];
                }
            }
            else {
                if (ctx.resultInfo.hasSingleResult) // we can recommend
                    return [contextTable.ctx_single_result_search_command, contextTable.ctx_complete_search_command];
                else if (ctx.resultInfo.hasLargeResult && ctx.state.dialogueAct !== 'ask_recommend') // we can refine
                    return [contextTable.ctx_search_command, contextTable.ctx_complete_search_command];
                else
                    return [contextTable.ctx_complete_search_command];
            }
        default:
            throw new Error(`Unexpected user dialogue act ${ctx.state.dialogueAct}`);
    }
}
exports.tagContextForAgent = tagContextForAgent;
function ctxCanHaveRelatedQuestion(ctx) {
    const currentStmt = ctx.current.stmt;
    assert_1.default(currentStmt.stream === null);
    const currentTable = currentStmt.lastQuery;
    if (!currentTable)
        return false;
    if (!(currentTable.schema instanceof thingtalk_1.Ast.FunctionDef)) // FIXME ExpressionSignature that is not a FunctionDef - not sure how it happens...
        return false;
    const related = currentTable.schema.getAnnotation('related');
    return !!(related && related.length);
}
function makeErrorContextPhrase(ctx, error) {
    const contextTable = ctx.contextTable;
    const describer = ctx.loader.describer;
    const currentFunction = ctx.currentFunction;
    const phrases = ctx.loader.getErrorMessages(currentFunction.qualifiedName)[error.value];
    if (!phrases)
        return null;
    const action = C.getInvocation(ctx.current);
    // try all phrases, find the first that we can replace correctly
    for (const candidate of phrases) {
        const bag = new slot_bag_1.SlotBag(currentFunction);
        let utterance = '';
        let good = true;
        for (const chunk of candidate.split(' ')) {
            if (chunk.startsWith('$') && chunk !== '$$') {
                const [, param1, param2,] = /^\$(?:\$|([a-zA-Z0-9_]+(?![a-zA-Z0-9_]))|{([a-zA-Z0-9_]+)(?::([a-zA-Z0-9_-]+))?})$/.exec(chunk);
                const param = param1 || param2;
                assert_1.default(param);
                // TODO use opt
                let found = null;
                for (const in_param of action.in_params) {
                    if (in_param.name === param) {
                        found = in_param.value;
                        break;
                    }
                }
                if (!found) {
                    good = false;
                    break;
                }
                utterance += ' ' + describer.describeArg(found);
                bag.set(param, found);
            }
            else {
                utterance += ' ' + chunk;
            }
        }
        utterance = utterance.trim();
        if (good) {
            const value = { code: error.value, bag };
            return { symbol: contextTable.ctx_thingpedia_error_message, utterance, value, priority: 0, key: keyfns.errorMessageKeyFn(value) };
        }
    }
    return null;
}
function makeResultContextPhrase(ctx, result) {
    const contextTable = ctx.contextTable;
    const describer = ctx.loader.describer;
    const currentFunction = ctx.currentFunction;
    const phrases = ctx.loader.getResultStrings(currentFunction.qualifiedName);
    // try all phrases, find the first that we can replace correctly
    for (const candidate of phrases) {
        const bag = new slot_bag_1.SlotBag(currentFunction);
        let utterance = '';
        let good = true;
        for (const chunk of candidate.split(' ')) {
            if (chunk.startsWith('$') && chunk !== '$$') {
                const [, param1, param2,] = /^\$(?:\$|([a-zA-Z0-9_]+(?![a-zA-Z0-9_]))|{([a-zA-Z0-9_]+)(?::([a-zA-Z0-9_-]+))?})$/.exec(chunk);
                const param = param1 || param2;
                assert_1.default(param);
                // TODO use opt
                const value = result.value[param];
                if (!value) {
                    good = false;
                    break;
                }
                utterance += ' ' + describer.describeArg(value);
                bag.set(param, value);
            }
            else {
                utterance += ' ' + chunk;
            }
        }
        utterance = utterance.trim();
        if (good)
            return { symbol: contextTable.ctx_thingpedia_result, utterance, value: bag, priority: 0, key: keyfns.slotBagKeyFn(bag) };
    }
    return null;
}
function getContextPhrases(ctx) {
    const contextTable = ctx.contextTable;
    const phrases = [];
    const describer = ctx.loader.describer;
    // make phrases that describe the current and next action
    // these are used by the agent to form confirmations
    const current = ctx.current;
    if (current) {
        const lastQuery = current.stmt.lastQuery;
        if (lastQuery) {
            phrases.push(makeExpressionContextPhrase(contextTable.ctx_current_query, lastQuery, describer.describeQuery(lastQuery)));
        }
        if (current.results.error instanceof thingtalk_1.Ast.EnumValue) {
            const phrase = makeErrorContextPhrase(ctx, current.results.error);
            if (phrase)
                phrases.push(phrase);
        }
        else {
            const results = current.results.results;
            if (results.length > 0) {
                const topResult = results[0];
                const phrase = makeResultContextPhrase(ctx, topResult);
                if (phrase)
                    phrases.push(phrase);
            }
        }
    }
    const next = ctx.next;
    if (next) {
        phrases.push(makeContextPhrase(contextTable.ctx_next_statement, ctx, describer.describeExpressionStatement(next.stmt)));
        const lastQuery = next.stmt.lastQuery;
        if (lastQuery) {
            phrases.push(makeExpressionContextPhrase(contextTable.ctx_next_query, lastQuery, describer.describeQuery(lastQuery)));
        }
        const action = next.stmt.last;
        if (action.schema.functionType === 'action') {
            assert_1.default(action instanceof thingtalk_1.Ast.InvocationExpression);
            phrases.push(makeExpressionContextPhrase(contextTable.ctx_next_action, action, describer.describePrimitive(action.invocation)));
        }
    }
    if (ctx.isMultiDomain)
        phrases.push(makeContextPhrase(contextTable.ctx_multidomain, ctx));
    if (ctx.nextInfo !== null) {
        phrases.push(makeContextPhrase(contextTable.ctx_with_action, ctx));
        if (!ctx.nextInfo.isComplete)
            phrases.push(makeContextPhrase(contextTable.ctx_incomplete_action, ctx));
    }
    else {
        if (ctx.resultInfo && ctx.resultInfo.isTable)
            phrases.push(makeContextPhrase(contextTable.ctx_without_action, ctx));
    }
    if (!ctx.resultInfo || ctx.resultInfo.hasEmptyResult)
        return phrases;
    assert_1.default(ctx.results && ctx.results.length > 0);
    phrases.push(makeContextPhrase(contextTable.ctx_with_result, ctx));
    if (ctx.resultInfo.isTable)
        phrases.push(makeContextPhrase(contextTable.ctx_with_table_result, ctx));
    if (ctx.resultInfo.isAggregation)
        phrases.push(makeContextPhrase(contextTable.ctx_with_aggregation_result, ctx));
    if (ctxCanHaveRelatedQuestion(ctx))
        phrases.push(makeContextPhrase(contextTable.ctx_for_related_question, ctx));
    if (isUserAskingResultQuestion(ctx)) {
        phrases.push(makeContextPhrase(contextTable.ctx_with_result_question, ctx));
    }
    else {
        if (ctx.resultInfo.argMinMaxField)
            phrases.push(makeContextPhrase(contextTable.ctx_with_result_argminmax, ctx));
        else
            phrases.push(makeContextPhrase(contextTable.ctx_with_result_noquestion, ctx));
        if (ctx.nextInfo)
            phrases.push(makeContextPhrase(contextTable.ctx_with_result_and_action, ctx));
        if (ctx.resultInfo.projection === null)
            phrases.push(makeContextPhrase(contextTable.ctx_without_projection, ctx));
    }
    return phrases;
}
exports.getContextPhrases = getContextPhrases;
//# sourceMappingURL=state_manip.js.map