"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.combinePreambleAndRequest = exports.proposalReply = exports.refineFilterToChangeFilter = exports.refineFilterToAnswerQuestionOrChangeFilter = exports.refineFilterToAnswerQuestion = exports.queryRefinement = exports.findOrMakeFilterExpression = void 0;
const assert_1 = __importDefault(require("assert"));
const thingtalk_1 = require("thingtalk");
const C = __importStar(require("../ast_manip"));
const state_manip_1 = require("../state_manip");
const common_1 = require("./common");
/**
 * Find the filter expression in the context.
 *
 * Like findFilterExpression, but if we don't have one, make it up right before the invocation.
 *
 * Returns [root, filterTable]
 */
function findOrMakeFilterExpression(root) {
    let expr = root;
    let holder = null;
    while (!(expr instanceof thingtalk_1.Ast.FilterExpression)) {
        // do not touch these with filters
        if (expr instanceof thingtalk_1.Ast.AggregationExpression ||
            expr instanceof thingtalk_1.Ast.FunctionCallExpression)
            return null;
        // go inside these
        if (expr instanceof thingtalk_1.Ast.SortExpression ||
            expr instanceof thingtalk_1.Ast.MonitorExpression ||
            expr instanceof thingtalk_1.Ast.IndexExpression ||
            expr instanceof thingtalk_1.Ast.SliceExpression ||
            expr instanceof thingtalk_1.Ast.ProjectionExpression ||
            expr instanceof thingtalk_1.Ast.AliasExpression) {
            holder = expr;
            expr = expr.expression;
            continue;
        }
        if (expr instanceof thingtalk_1.Ast.ChainExpression) {
            holder = expr;
            // go right on join, but don't go into the action
            const maybeExpr = expr.lastQuery;
            if (!maybeExpr)
                return null;
            expr = maybeExpr;
            continue;
        }
        assert_1.default(expr instanceof thingtalk_1.Ast.InvocationExpression);
        // if we get here, there is no filter table at all
        // make up one
        const newFilterTable = new thingtalk_1.Ast.FilterExpression(null, expr, thingtalk_1.Ast.BooleanExpression.True, expr.schema);
        assert_1.default(holder !== null);
        if (holder instanceof thingtalk_1.Ast.ChainExpression) {
            holder.setLastQuery(newFilterTable);
            return newFilterTable;
        }
        else {
            holder.expression = newFilterTable;
            return newFilterTable;
        }
    }
    return expr;
}
exports.findOrMakeFilterExpression = findOrMakeFilterExpression;
function setsIntersect(s1, s2) {
    for (const el of s1) {
        if (s2.has(el))
            return true;
    }
    return false;
}
function neutralizeIDFilter(ast) {
    // clone a filter and replace "id == ..." atoms with "true"
    if (ast instanceof thingtalk_1.Ast.NotBooleanExpression)
        return new thingtalk_1.Ast.BooleanExpression.Not(null, neutralizeIDFilter(ast.expr));
    if (ast instanceof thingtalk_1.Ast.OrBooleanExpression)
        return new thingtalk_1.Ast.BooleanExpression.Or(null, ast.operands.map(neutralizeIDFilter));
    if (ast instanceof thingtalk_1.Ast.AndBooleanExpression)
        return new thingtalk_1.Ast.BooleanExpression.And(null, ast.operands.map(neutralizeIDFilter));
    if (ast.isTrue || ast.isDontCare || ast.isFalse || ast.isCompute || ast.isExternal)
        return ast;
    assert_1.default(ast instanceof thingtalk_1.Ast.AtomBooleanExpression);
    if (ast.name === 'id' && ast.operator === '==')
        return thingtalk_1.Ast.BooleanExpression.True;
    return ast;
}
function filterToNegatedSlots(filter) {
    filter = filter.optimize();
    const slots = {};
    let operands;
    if (filter instanceof thingtalk_1.Ast.AndBooleanExpression)
        operands = filter.operands;
    else
        operands = [filter];
    for (const operand of operands) {
        if (!(operand instanceof thingtalk_1.Ast.NotBooleanExpression))
            continue;
        const atom = operand.expr;
        if (!(atom instanceof thingtalk_1.Ast.AtomBooleanExpression) &&
            !(atom instanceof thingtalk_1.Ast.DontCareBooleanExpression))
            continue;
        slots[atom.name] = operand;
    }
    return slots;
}
function filterToSlots(filter) {
    filter = filter.optimize();
    const slots = {};
    let operands;
    if (filter instanceof thingtalk_1.Ast.AndBooleanExpression)
        operands = filter.operands;
    else
        operands = [filter];
    for (const operand of operands) {
        if (!(operand instanceof thingtalk_1.Ast.AtomBooleanExpression) &&
            !(operand instanceof thingtalk_1.Ast.DontCareBooleanExpression))
            continue;
        slots[operand.name] = operand;
    }
    return slots;
}
function queryRefinement(ctxExpression, newFilter, refineFilter, newProjection) {
    const cloneExpression = ctxExpression.clone();
    let refinedFilter;
    if (newFilter !== null) {
        assert_1.default(refineFilter);
        const filterExpression = findOrMakeFilterExpression(cloneExpression);
        //if (ctxFilterTable === null)
        //    return null;
        assert_1.default(filterExpression);
        // TODO we need to push down the filter, if possible
        if (!common_1.isSimpleFilterExpression(filterExpression))
            return null;
        const newRefinedFilter = refineFilter(filterExpression.filter, newFilter);
        if (newRefinedFilter === null)
            return null;
        refinedFilter = newRefinedFilter;
        filterExpression.filter = refinedFilter;
    }
    // a projection always applies to the last element in the chain
    // (which must be a query, not an action)
    let last = cloneExpression.last;
    if (newProjection) {
        // if we have a new projection, we first remove the existing one
        if (last instanceof thingtalk_1.Ast.ProjectionExpression)
            last = last.expression;
        // there should be no projection of projection (will be optimized)
        assert_1.default(!(last instanceof thingtalk_1.Ast.ProjectionExpression));
        cloneExpression.last = new thingtalk_1.Ast.ProjectionExpression(null, last, newProjection, [], [], C.resolveProjection(last.schema, newProjection));
    }
    else {
        // otherwise, we remove all fields from the projection that were mentioned in the
        // filter
        let oldProjection = [];
        let oldComputation = [];
        let oldAliases = [];
        if (last instanceof thingtalk_1.Ast.ProjectionExpression) {
            oldProjection = last.args;
            oldComputation = last.computations;
            oldAliases = last.aliases;
            last = last.expression;
        }
        // there should be no projection of projection (will be optimized)
        assert_1.default(!(last instanceof thingtalk_1.Ast.ProjectionExpression));
        // either one of newProjection or newFilter must be provided
        assert_1.default(newFilter !== null);
        if (oldProjection) {
            const newProjection = oldProjection.filter((pname) => !C.filterUsesParam(refinedFilter, pname));
            // if the projection is now empty, we don't add it
            //
            // the projection will be empty if
            // 1. the user asks a question
            // 2. the agent answers that question
            // 3. the user now refines the search indicating they don't like that answer
            //
            // on the other hand, if
            // 1. the user asks a question (eg. asks for "address")
            // 2. the agent answers that question
            // 3. the user now refines the search changing a different parameter
            // we will keep the projection
            if (newProjection.length > 0) {
                cloneExpression.last = new thingtalk_1.Ast.ProjectionExpression(null, last, newProjection, oldComputation, oldAliases, C.resolveProjection(last.schema, newProjection));
            }
        }
    }
    return cloneExpression;
}
exports.queryRefinement = queryRefinement;
class GetParamsVisitor extends thingtalk_1.Ast.NodeVisitor {
    constructor() {
        super(...arguments);
        this.params = new Set();
    }
    visitAtomBooleanExpression(atom) {
        if (atom.name === 'id' && atom.operator === '==')
            return false;
        this.params.add(atom.name);
        return false;
    }
    visitDontCareBooleanExpression(atom) {
        this.params.add(atom.name);
        return false;
    }
    visitExternalBooleanExpression() {
        return false;
    }
}
function getParamsInFilter(filter) {
    const visitor = new GetParamsVisitor();
    filter.visit(visitor);
    return visitor.params;
}
function refineFilterToAnswerQuestion(ctxFilter, refinedFilter) {
    // this function is used when:
    // - the agent asks a search refinement question, and the user answers it
    // - the agent proposes something to refine the question
    //
    // the refinement is allowed only if the parameter was not mentioned before
    // furthermore, "id ==" filters are removed from the refined filter, so a user
    // can choose a restaurant for a while then change their mind
    if (setsIntersect(getParamsInFilter(ctxFilter), getParamsInFilter(refinedFilter)))
        return null;
    const clone = neutralizeIDFilter(ctxFilter);
    return new thingtalk_1.Ast.BooleanExpression.And(null, [clone, refinedFilter]).optimize();
}
exports.refineFilterToAnswerQuestion = refineFilterToAnswerQuestion;
function refineFilterToAnswerQuestionOrChangeFilter(ctxFilter, refinedFilter) {
    // this function is used:
    // - the agent proposes something, and the user replies with a bunch of filters
    //   (e.g. "how about terun?" "nah i'm looking for something chinese")
    //
    // the refinement is allowed only if the parameter was not mentioned before
    // at most one parameter can be mentioned in the context, in which case it must be different
    //
    // the refinement contains all clauses which are not explicitly negated in the refinement,
    // plus all of the refinement
    // furthermore, "id ==" filters are removed from the refined filter, so a user
    // can choose a restaurant for a while then change their mind
    ctxFilter = ctxFilter.optimize();
    refinedFilter = refinedFilter.optimize();
    const ctxSlots = filterToSlots(ctxFilter);
    const refinedSlots = filterToSlots(refinedFilter);
    const negatedRefinedSlots = filterToNegatedSlots(refinedFilter);
    let changedParam = undefined;
    // slots in the context must not mentioned in the refinement, except at most one can, and
    // it must be different operator or value
    //
    // note that both positive and negative filters are killed by this check
    // so neither "I want X food" nor "I don't like X food" are acceptable when "food =~ X" was
    // already in the context
    for (const key in ctxSlots) {
        assert_1.default(ctxSlots[key] instanceof thingtalk_1.Ast.AtomBooleanExpression ||
            ctxSlots[key] instanceof thingtalk_1.Ast.DontCareBooleanExpression);
        if (negatedRefinedSlots[key])
            return null;
        if (refinedSlots[key]) {
            // dont change opinion from a dontcare to a not dontcare
            if (ctxSlots[key] instanceof thingtalk_1.Ast.DontCareBooleanExpression)
                return null;
            if (refinedSlots[key].equals(ctxSlots[key]))
                return null;
            if (changedParam !== undefined)
                return null;
            changedParam = key;
        }
    }
    const newCtxClauses = [];
    for (const clause of (ctxFilter instanceof thingtalk_1.Ast.AndBooleanExpression ? ctxFilter.operands : [ctxFilter])) {
        if (clause instanceof thingtalk_1.Ast.AtomBooleanExpression || clause instanceof thingtalk_1.Ast.DontCareBooleanExpression) {
            if (refinedSlots[clause.name])
                continue;
        }
        newCtxClauses.push(neutralizeIDFilter(clause));
    }
    return new thingtalk_1.Ast.BooleanExpression.And(null, [...newCtxClauses, refinedFilter]).optimize();
}
exports.refineFilterToAnswerQuestionOrChangeFilter = refineFilterToAnswerQuestionOrChangeFilter;
class IsGoodFilterForChangeFilterVisitor extends thingtalk_1.Ast.NodeVisitor {
    constructor(refinedFilter) {
        super();
        this.refinedFilter = refinedFilter;
        this.good = true;
    }
    visitExternalBooleanExpression() {
        // do not recurse
        // get rid of get-predicates in the context, regardless
        this.good = false;
        return false;
    }
    visitValue() {
        // do not recurse
        return false;
    }
    visitAtomBooleanExpression(atom) {
        this.good = this.good && !C.filterUsesParam(this.refinedFilter, atom.name);
        return true;
    }
    visitDontCareBooleanExpression(atom) {
        this.good = this.good && !C.filterUsesParam(this.refinedFilter, atom.name);
        return true;
    }
}
function refineFilterToChangeFilter(ctxFilter, refinedFilter) {
    // this function is used:
    // - when the agent returned zero results, and the user
    //   must change the search
    // - when the agent makes a filter proposal, and the user says no I want something else
    //
    // the refinement is allowed only if no new parameters are introduced (all parameters were
    // mentioned before), and at least one parameter is different than before
    //
    // the resulting filter uses all the parameters in ctxFilter that are not mentioned
    // in refinedFilter, as well as all of refinedFilter
    ctxFilter = ctxFilter.optimize();
    refinedFilter = refinedFilter.optimize();
    const ctxSlots = filterToSlots(ctxFilter);
    const refinedSlots = filterToSlots(refinedFilter);
    // all slots in the context must be either not mentioned in the refinement, or changed
    for (const key in ctxSlots) {
        if (refinedSlots[key] && refinedSlots[key].equals(ctxSlots[key]))
            return null;
    }
    // all slots that are in the refinement must be mentioned in the context
    for (const key in refinedSlots) {
        if (!ctxSlots[key])
            return null;
    }
    const visitor = new IsGoodFilterForChangeFilterVisitor(refinedFilter);
    const ctxClauses = (ctxFilter instanceof thingtalk_1.Ast.AndBooleanExpression ? ctxFilter.operands : [ctxFilter]).filter((clause) => {
        visitor.good = true;
        clause.visit(visitor);
        return visitor.good;
    });
    return new thingtalk_1.Ast.BooleanExpression.And(null, [...ctxClauses, refinedFilter]).optimize();
}
exports.refineFilterToChangeFilter = refineFilterToChangeFilter;
/**
 * User act: in response to any proposal from the agent (refined query, recommendation, list
 * proposal), the user replies with a search.
 */
function proposalReply(ctx, request, refinementFunction) {
    if (!C.isSameFunction(ctx.currentFunction, request.schema))
        return null;
    // TODO we need to push down the filter, if possible
    if (!common_1.isSimpleFilterExpression(request))
        return null;
    const currentStmt = ctx.current.stmt;
    const currentExpression = currentStmt.expression;
    const newTable = queryRefinement(currentExpression, request.filter, refinementFunction, null);
    if (newTable === null)
        return null;
    return state_manip_1.addQuery(ctx, 'execute', newTable, 'accepted');
}
exports.proposalReply = proposalReply;
function isValidNegativePreambleForInfo(info, preamble) {
    // the preamble must match the info provided
    // (and we will negate it later)
    return common_1.isFilterCompatibleWithInfo(info, preamble.filter);
}
function combinePreambleAndRequest(preamble, request, info, proposalType) {
    if (preamble !== null) {
        if (info === null || !isValidNegativePreambleForInfo(info, preamble))
            return null;
    }
    if (preamble !== null && request !== null) {
        if (!C.isSameFunction(preamble.schema, request.schema))
            return null;
        const refined = refineFilterToChangeFilter(preamble.filter, request.filter);
        if (refined === null)
            return null;
        // convert the preamble into a request by negating it, then add the new request
        request = new thingtalk_1.Ast.FilterExpression(null, request.expression, new thingtalk_1.Ast.BooleanExpression.And(null, [
            new thingtalk_1.Ast.BooleanExpression.Not(null, preamble.filter),
            request.filter
        ]), request.schema);
    }
    else if (preamble !== null) {
        // convert the preamble into a request by negating it
        // shallow clone
        assert_1.default(preamble instanceof thingtalk_1.Ast.FilterExpression);
        request = new thingtalk_1.Ast.FilterExpression(null, preamble.expression, preamble.filter, preamble.schema);
        request.filter = new thingtalk_1.Ast.BooleanExpression.Not(null, request.filter);
    }
    assert_1.default(request !== null);
    if (proposalType) {
        const idType = request.schema.getArgType('id');
        if (!idType || !idType.equals(proposalType))
            return null;
    }
    return request;
}
exports.combinePreambleAndRequest = combinePreambleAndRequest;
//# sourceMappingURL=refinement-helpers.js.map