"use strict";
// -*- mode: typescript; indent-tabs-mode: nil; js-basic-offset: 4 -*-
//
// This file is part of Genie
//
// Copyright 2019-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 __exportStar = (this && this.__exportStar) || function(m, exports) {
    for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.addFilter = exports.checkFilter = exports.makeAggregateFilterWithFilter = exports.makeAggregateFilter = exports.makeButFilter = exports.makeOrFilter = exports.hasUniqueFilter = exports.addActionInputParam = exports.addInvocationInputParam = exports.checkInvocationInputParam = exports.combineStreamCommand = exports.makeProgram = exports.timeGetPredicate = exports.locationGetPredicate = exports.builtinSayAction = exports.makeMonthDateRange = exports.makeDate = exports.addUnit = exports.adjustDefaultParameters = exports.getInvocation = exports.getFunctions = exports.getFunctionNames = exports.filterUsesParam = exports.isConstantAssignable = exports.hasArgumentOfType = exports.isSameFunction = exports.typeToStringSafe = exports.expressionUsesIDFilter = exports.replacePlaceholderWithUndefined = exports.addSameNameParameterPassing = exports.addParameterPassing = exports.makeChainExpression = exports.resolveChain = exports.combineStreamQuery = exports.toChainExpression = exports.makeTypeBasedStreamProjection = exports.makeTypeBasedTableProjection = exports.getImplicitParameterPassing = exports.getProjectionArguments = exports.getScalarExpressionName = exports.makeDontCareFilter = exports.betaReduceMany = exports.dateOrDatePiece = exports.fixTwoYearNumber = exports.isEntityOfFunction = exports.makeDateRangeFilter = exports.makeAndFilter = exports.makeFilter = exports.makeDomainIndependentFilter = exports.makeInputParamSlot = void 0;
exports.addArrayJoin = exports.addReverseGetPredicateJoin = exports.addGetPredicateJoin = exports.iterateFields = exports.iterateFilters = exports.makeWithinGeoDistanceExpression = exports.makeAggComputeArgMinMaxExpression = exports.makeAggComputeExpression = exports.makeComputeArgMinMaxExpression = exports.makeComputeFilterExpression = exports.makeComputeExpression = exports.hasConflictParam = exports.arrayFilterTableJoin = exports.filterTableJoin = exports.tableToStream = exports.makeEdgeFilterStream = exports.sayProjection = exports.makeMultiFieldProjection = exports.makeSingleFieldProjection = exports.makeProjection = exports.resolveProjection = exports.checkValidQuery = exports.makeArgMaxMinTable = exports.makeSortedTable = exports.makeListExpression = exports.findFilterExpression = exports.makeGetPredicate = exports.hasGetPredicate = void 0;
const assert_1 = __importDefault(require("assert"));
const thingtalk_1 = require("thingtalk");
const Units = __importStar(require("thingtalk-units"));
const utils_1 = require("./utils");
Object.defineProperty(exports, "makeInputParamSlot", { enumerable: true, get: function () { return utils_1.makeInputParamSlot; } });
Object.defineProperty(exports, "makeDomainIndependentFilter", { enumerable: true, get: function () { return utils_1.makeDomainIndependentFilter; } });
Object.defineProperty(exports, "makeFilter", { enumerable: true, get: function () { return utils_1.makeFilter; } });
Object.defineProperty(exports, "makeAndFilter", { enumerable: true, get: function () { return utils_1.makeAndFilter; } });
Object.defineProperty(exports, "makeDateRangeFilter", { enumerable: true, get: function () { return utils_1.makeDateRangeFilter; } });
Object.defineProperty(exports, "typeToStringSafe", { enumerable: true, get: function () { return utils_1.typeToStringSafe; } });
Object.defineProperty(exports, "isSameFunction", { enumerable: true, get: function () { return utils_1.isSameFunction; } });
__exportStar(require("./keyfns"), exports);
function isEntityOfFunction(type, schema) {
    if (!schema.class)
        return false;
    return type.type === schema.class.name + ':' + schema.name;
}
exports.isEntityOfFunction = isEntityOfFunction;
function makeDate(base, operator, offset) {
    if (!(base instanceof thingtalk_1.Ast.Value))
        base = new thingtalk_1.Ast.Value.Date(base);
    if (offset === null)
        return base;
    const value = new thingtalk_1.Ast.Value.Computation(operator, [base, offset], [thingtalk_1.Type.Date, new thingtalk_1.Type.Measure('ms'), thingtalk_1.Type.Date], thingtalk_1.Type.Date);
    return value;
}
exports.makeDate = makeDate;
function fixTwoYearNumber(year) {
    if (year >= 50)
        return 1900 + year;
    else
        return 2000 + year;
}
exports.fixTwoYearNumber = fixTwoYearNumber;
function dateOrDatePiece(year, month) {
    if (year === null)
        return new thingtalk_1.Ast.DatePiece(year, month, null, null);
    else
        return new Date(fixTwoYearNumber(year), month === null ? 0 : month - 1);
}
exports.dateOrDatePiece = dateOrDatePiece;
function makeMonthDateRange(year, month) {
    return [
        makeDate(dateOrDatePiece(year, month), '+', null),
        makeDate(dateOrDatePiece(year, month), '+', new thingtalk_1.Ast.Value.Measure(1, 'mon'))
    ];
}
exports.makeMonthDateRange = makeMonthDateRange;
class GetFunctionVisitor extends thingtalk_1.Ast.NodeVisitor {
    constructor() {
        super(...arguments);
        this.names = [];
        this.functions = [];
    }
    visitInvocation(invocation) {
        this.names.push(invocation.selector.kind + '.' + invocation.channel);
        this.functions.push(invocation.schema);
        return true;
    }
}
function getFunctionNames(ast) {
    const visitor = new GetFunctionVisitor();
    ast.visit(visitor);
    return visitor.names;
}
exports.getFunctionNames = getFunctionNames;
function getFunctions(ast) {
    const visitor = new GetFunctionVisitor();
    ast.visit(visitor);
    return visitor.functions;
}
exports.getFunctions = getFunctions;
function isSelfJoinStream(stream) {
    const functions = getFunctionNames(stream);
    if (functions.length > 1) {
        if (!Array.isArray(functions))
            throw new TypeError('??? ' + functions);
        functions.sort();
        for (let i = 0; i < functions.length - 1; i++) {
            if (functions[i] === functions[i + 1])
                return true;
        }
    }
    return false;
}
function betaReduceMany(ast, replacements) {
    const clone = ast.clone();
    for (const slot of clone.iterateSlots2({})) {
        if (slot instanceof thingtalk_1.Ast.DeviceSelector)
            continue;
        const varref = slot.get();
        if (varref instanceof thingtalk_1.Ast.VarRefValue) {
            const pname = varref.name;
            if (!(pname in replacements))
                continue;
            if (pname in slot.scope) {
                // if the parameter is in scope of the slot, it means we're in a filter and the same parameter name
                // is returned by the stream/table, which shadows the example/declaration parameter we're
                // trying to replace, hence we ignore this slot
                continue;
            }
            const replacement = replacements[pname];
            assert_1.default(replacement instanceof thingtalk_1.Ast.Value);
            // no parameter passing or undefined into device attributes
            if ((replacement.isUndefined || replacement instanceof thingtalk_1.Ast.EventValue
                || (replacement instanceof thingtalk_1.Ast.VarRefValue && !replacement.name.startsWith('__const')))
                && slot.tag.startsWith('attribute.'))
                return null;
            slot.set(replacement);
        }
    }
    return clone;
}
exports.betaReduceMany = betaReduceMany;
function makeDontCareFilter(slot) {
    return { schema: slot.schema, ptype: slot.type, ast: new thingtalk_1.Ast.BooleanExpression.DontCare(null, slot.name) };
}
exports.makeDontCareFilter = makeDontCareFilter;
function makeOrFilter(tpLoader, slot, op, values, negate = false) {
    if (values.length !== 2)
        return null;
    if (values[0].equals(values[1]))
        return null;
    const operands = [
        utils_1.makeFilter(tpLoader, slot, op, values[0], negate),
        utils_1.makeFilter(tpLoader, slot, op, values[1], negate)
    ];
    if (operands[0] === null || operands[1] === null)
        return null;
    let ast = new thingtalk_1.Ast.BooleanExpression.Or(null, [operands[0].ast, operands[1].ast]);
    if (negate)
        ast = new thingtalk_1.Ast.BooleanExpression.Not(null, ast);
    return { schema: slot.schema, ptype: slot.type, ast };
}
exports.makeOrFilter = makeOrFilter;
function makeButFilter(tpLoader, slot, op, values) {
    if (values.length !== 2)
        return null;
    if (values[0].equals(values[1]))
        return null;
    const operands = [
        utils_1.makeFilter(tpLoader, slot, op, values[0]),
        utils_1.makeFilter(tpLoader, slot, op, values[1], true)
    ];
    if (operands[0] === null || operands[1] === null)
        return null;
    const ast = new thingtalk_1.Ast.BooleanExpression.And(null, [operands[0].ast, operands[1].ast]);
    return { schema: slot.schema, ptype: slot.type, ast };
}
exports.makeButFilter = makeButFilter;
function makeListExpression(param, filter) {
    if (!utils_1.isSameFunction(param.schema, filter.schema))
        return null;
    // TODO: handle more complicated filters
    if (!(filter instanceof thingtalk_1.Ast.AtomBooleanExpression))
        return null;
    // TODO check that the filter is valid inside this compound array...
    return null;
    //return { schema: param.schema, ast: new Ast.Value.Filter(param, filter) };
}
exports.makeListExpression = makeListExpression;
function normalizeFilter(filter, schema) {
    if (filter.lhs instanceof thingtalk_1.Ast.ComputationValue &&
        filter.lhs.op === 'count' &&
        filter.lhs.operands.length === 1) {
        const op1 = filter.lhs.operands[0];
        assert_1.default(op1 instanceof thingtalk_1.Ast.VarRefValue);
        const name = op1.name;
        const canonical = schema.getArgCanonical(name);
        if (!canonical)
            return null;
        for (const p of schema.iterateArguments()) {
            if (p.name === name + 'Count' ||
                p.canonical === canonical + ' count' ||
                p.canonical === canonical.slice(0, -1) + ' count')
                return { schema, ptype: schema.getArgType(p.name), ast: new thingtalk_1.Ast.BooleanExpression.Atom(null, p.name, filter.operator, filter.rhs) };
        }
    }
    return { schema, ptype: filter.lhs.getType(), ast: filter };
}
function makeAggregateFilter(param, aggregationOp, field, op, value) {
    if (aggregationOp === 'count') {
        if (!value.getType().isNumber)
            return null;
        assert_1.default(field === null || field === '*');
        const agg = new thingtalk_1.Ast.Value.Computation(aggregationOp, [param.ast], [new thingtalk_1.Type.Array('x'), thingtalk_1.Type.Number], thingtalk_1.Type.Number);
        return normalizeFilter(new thingtalk_1.Ast.BooleanExpression.Compute(null, agg, op, value), param.schema);
    }
    else if (['sum', 'avg', 'max', 'min'].includes(aggregationOp)) {
        const vtype = value.getType();
        const ptype = param.type;
        assert_1.default(field !== '*');
        if (field !== null) {
            if (!utils_1.isSameFunction(param.schema, field.schema))
                return null;
            if (!(ptype instanceof thingtalk_1.Type.Array))
                return null;
            const eltype = ptype.elem;
            if (!(eltype instanceof thingtalk_1.Type.Compound))
                return null;
            if (!(field.name in eltype.fields))
                return null;
        }
        const agg = new thingtalk_1.Ast.Value.Computation(aggregationOp, [
            field ? new thingtalk_1.Ast.Value.ArrayField(param.ast, field.name) : param.ast
        ], [new thingtalk_1.Type.Array(vtype), vtype], vtype);
        return normalizeFilter(new thingtalk_1.Ast.BooleanExpression.Compute(null, agg, op, value), param.schema);
    }
    return null;
}
exports.makeAggregateFilter = makeAggregateFilter;
function makeAggregateFilterWithFilter(param, filter, aggregationOp, field, op, value) {
    if (filter === null)
        return null;
    if (!utils_1.isSameFunction(param.schema, filter.schema))
        return null;
    const list = makeListExpression(param, filter);
    if (!list)
        return null;
    if (aggregationOp === 'count') {
        if (!value.getType().isNumber)
            return null;
        assert_1.default(field === '*');
        const agg = new thingtalk_1.Ast.Value.Computation(aggregationOp, [list.ast], [new thingtalk_1.Type.Array('x'), thingtalk_1.Type.Number], thingtalk_1.Type.Number);
        return normalizeFilter(new thingtalk_1.Ast.BooleanExpression.Compute(null, agg, op, value), param.schema);
    }
    else if (['sum', 'avg', 'max', 'min'].includes(aggregationOp)) {
        const vtype = value.getType();
        const ptype = param.type;
        assert_1.default(field !== '*');
        if (field !== null) {
            if (!utils_1.isSameFunction(param.schema, field.schema))
                return null;
            if (!(ptype instanceof thingtalk_1.Type.Array))
                return null;
            const eltype = ptype.elem;
            if (!(eltype instanceof thingtalk_1.Type.Compound))
                return null;
            if (!(field.name in eltype.fields))
                return null;
        }
        const agg = new thingtalk_1.Ast.Value.Computation(aggregationOp, [
            field ? new thingtalk_1.Ast.Value.ArrayField(param.ast, field.name) : param.ast
        ], [new thingtalk_1.Type.Array(vtype), vtype], vtype);
        return normalizeFilter(new thingtalk_1.Ast.BooleanExpression.Compute(null, agg, op, value), param.schema);
    }
    return null;
}
exports.makeAggregateFilterWithFilter = makeAggregateFilterWithFilter;
function makeEdgeFilterStream(loader, proj, op, value) {
    if (!(proj instanceof thingtalk_1.Ast.ProjectionExpression))
        return null;
    if (proj.args[0] === '$event')
        return null;
    const args = getProjectionArguments(proj);
    assert_1.default(args.length > 0);
    const f = {
        schema: proj.schema,
        ptype: proj.schema.getArgType(args[0]),
        ast: new thingtalk_1.Ast.BooleanExpression.Atom(null, args[0], op, value)
    };
    if (!checkFilter(proj.expression, f))
        return null;
    if (!proj.schema.is_monitorable || proj.schema.is_list)
        return null;
    const outParams = Object.keys(proj.expression.schema.out);
    if (outParams.length === 1 && loader.flags.turking)
        return null;
    return new thingtalk_1.Ast.FilterExpression(null, new thingtalk_1.Ast.MonitorExpression(null, proj.expression, null, proj.expression.schema), f.ast, proj.expression.schema);
}
exports.makeEdgeFilterStream = makeEdgeFilterStream;
function addUnit(unit, num) {
    if (num instanceof thingtalk_1.Ast.VarRefValue) {
        const v = new thingtalk_1.Ast.Value.VarRef(num.name + '__' + unit);
        v.unit = unit;
        v.getType = () => new thingtalk_1.Type.Measure(unit);
        return v;
    }
    else {
        return new thingtalk_1.Ast.Value.Measure(num.value, unit);
    }
}
exports.addUnit = addUnit;
function getScalarExpressionName(ast) {
    if (ast instanceof thingtalk_1.Ast.VarRefValue)
        return ast.name;
    if (ast instanceof thingtalk_1.Ast.ComputationValue && /^[a-zA-Z0-9]+$/.test(ast.op))
        return ast.op;
    else if (ast instanceof thingtalk_1.Ast.FilterValue || ast instanceof thingtalk_1.Ast.ArrayFieldValue)
        return getScalarExpressionName(ast.value);
    else
        return 'result';
}
exports.getScalarExpressionName = getScalarExpressionName;
function getComputationNames(computations = [], aliases = []) {
    const names = [];
    for (let i = 0; i < computations.length; i++) {
        const comp = computations[i];
        names.push(aliases[i] || getScalarExpressionName(comp));
    }
    return names;
}
/**
 * Return all names that are explicitly projected by a projection.
 *
 * This is equivalent to all the output parameters minus the minimal projection.
 */
function getProjectionArguments(table) {
    assert_1.default(table.args.length + table.computations.length > 0);
    return table.args.concat(getComputationNames(table.computations, table.aliases));
}
exports.getProjectionArguments = getProjectionArguments;
function resolveProjection(schema, args, computations = [], aliases = []) {
    assert_1.default(args.length >= 1 || computations.length > 0);
    const argset = new Set(args);
    for (const argname of schema.minimal_projection || [])
        argset.add(argname);
    for (const argname of argset) {
        const arg = schema.getArgument(argname);
        if (!arg || arg.is_input)
            throw new TypeError('Invalid field name ' + argname);
    }
    let clone = schema.filterArguments((a) => a.is_input || argset.has(a.name));
    const newArgs = [];
    for (let i = 0; i < computations.length; i++) {
        const comp = computations[i];
        const name = aliases[i] || getScalarExpressionName(comp);
        const type = comp.getType();
        newArgs.push(new thingtalk_1.Ast.ArgumentDef(schema.location, thingtalk_1.Ast.ArgDirection.OUT, name, type));
    }
    clone = clone.addArguments(newArgs);
    clone.default_projection = [];
    assert_1.default(Array.isArray(clone.minimal_projection));
    return clone;
}
exports.resolveProjection = resolveProjection;
function makeProjection(table, pname) {
    return new thingtalk_1.Ast.ProjectionExpression(null, table, [pname], [], [], resolveProjection(table.schema, [pname]));
}
exports.makeProjection = makeProjection;
/**
 * Compute the parameter passing to use from a table if a parameter name is
 * not spefified explicitly.
 */
function getImplicitParameterPassing(schema) {
    // if there is only one parameter, that's the one
    const outParams = Object.keys(schema.out);
    if (outParams.length === 1)
        return outParams[0];
    // if there is an ID, we pick that one
    const id = schema.getArgument('id');
    if (id && !id.is_input)
        return 'id';
    // if there is a picture, we pick that one
    const picture_url = schema.getArgument('picture_url');
    if (picture_url && !picture_url.is_input)
        return 'picture_url';
    // failing everything, return a string representation of the table
    return '$event';
}
exports.getImplicitParameterPassing = getImplicitParameterPassing;
function makeTypeBasedTableProjection(tpLoader, table, intotype = thingtalk_1.Type.Any) {
    if (table instanceof thingtalk_1.Ast.ProjectionExpression)
        return null;
    const pname = getImplicitParameterPassing(table.schema);
    if (pname === '$event') {
        if (!thingtalk_1.Type.isAssignable(thingtalk_1.Type.String, intotype, {}, tpLoader.entitySubTypeMap))
            return null;
        // FIXME this is bogus on various levels, because $event is not an argument
        // because the schema is not modified correctly...
        return new thingtalk_1.Ast.ProjectionExpression(null, table, ['$event'], [], [], table.schema);
    }
    else {
        if (!thingtalk_1.Type.isAssignable(table.schema.getArgType(pname), intotype, {}, tpLoader.entitySubTypeMap))
            return null;
        return makeProjection(table, pname);
    }
}
exports.makeTypeBasedTableProjection = makeTypeBasedTableProjection;
function makeTypeBasedStreamProjection(table) {
    if (table instanceof thingtalk_1.Ast.ProjectionExpression)
        return null;
    if (!table.schema.is_monitorable)
        return null;
    const pname = getImplicitParameterPassing(table.schema);
    if (pname === '$event')
        return null;
    return makeProjection(new thingtalk_1.Ast.MonitorExpression(null, table, null, table.schema), pname);
}
exports.makeTypeBasedStreamProjection = makeTypeBasedStreamProjection;
function isEqualityFilteredOnParameter(table, pname) {
    for (const [, filter] of iterateFilters(table)) {
        for (const field of iterateFields(filter)) {
            if (field.name === pname && field instanceof thingtalk_1.Ast.AtomBooleanExpression &&
                (field.operator === '==' || field.operator === '=~'))
                return true;
        }
    }
    return false;
}
function makeSingleFieldProjection(loader, ftype, ptype, table, param) {
    assert_1.default(table);
    assert_1.default(ftype === 'table' || ftype === 'stream');
    let pname;
    if (param === 'geo') {
        pname = 'geo';
    }
    else {
        if (!utils_1.isSameFunction(table.schema, param.schema))
            return null;
        pname = param.name;
    }
    const arg = table.schema.getArgument(pname);
    if (!arg || arg.is_input)
        return null;
    const outParams = Object.keys(table.schema.out);
    if (outParams.length === 1)
        return table;
    if (ptype && !thingtalk_1.Type.isAssignable(arg.type, ptype, {}, loader.entitySubTypeMap))
        return null;
    if (ftype === 'table') {
        if (pname === 'picture_url' && loader.flags.turking)
            return null;
        if (isEqualityFilteredOnParameter(table, pname))
            return null;
        return makeProjection(table, pname);
    }
    else {
        if (!table.schema.is_monitorable)
            return null;
        const stream = new thingtalk_1.Ast.MonitorExpression(null, table, null, table.schema);
        return makeProjection(stream, pname);
    }
}
exports.makeSingleFieldProjection = makeSingleFieldProjection;
function makeMultiFieldProjection(loader, ftype, table, outParams) {
    const names = [];
    for (const outParam of outParams) {
        if (!utils_1.isSameFunction(table.schema, outParam.schema))
            return null;
        const name = outParam.name;
        const arg = table.schema.getArgument(name);
        if (!arg || arg.is_input)
            return null;
        if (ftype === 'table') {
            if (name === 'picture_url' && loader.flags.turking)
                return null;
        }
        else {
            if (!table.schema.is_monitorable)
                return null;
        }
        names.push(name);
    }
    if (ftype === 'table') {
        for (const pname of names) {
            if (isEqualityFilteredOnParameter(table, pname))
                return null;
        }
        return new thingtalk_1.Ast.ProjectionExpression(null, table, names, [], [], resolveProjection(table.schema, names));
    }
    else {
        const stream = new thingtalk_1.Ast.MonitorExpression(null, table, null, table.schema);
        return new thingtalk_1.Ast.ProjectionExpression(null, stream, names, [], [], resolveProjection(stream.schema, names));
    }
}
exports.makeMultiFieldProjection = makeMultiFieldProjection;
function makeArgMaxMinTable(table, pname, direction, count) {
    const t_sort = makeSortedTable(table, pname, direction);
    if (!t_sort)
        return null;
    count = count || new thingtalk_1.Ast.Value.Number(1);
    if (count instanceof thingtalk_1.Ast.Value.Number && count.value <= 0)
        return null;
    return new thingtalk_1.Ast.SliceExpression(null, t_sort, new thingtalk_1.Ast.Value.Number(1), count, t_sort.schema);
}
exports.makeArgMaxMinTable = makeArgMaxMinTable;
function makeSortedTable(table, pname, direction = 'desc') {
    assert_1.default(typeof pname === 'string');
    assert_1.default(direction === 'asc' || direction === 'desc');
    const type = table.schema.out[pname];
    // String are comparable but we don't want to sort alphabetically here
    // (we need to use isComparable because Date/Time are comparable but not numeric)
    if (!type || !type.isComparable() || type.isString)
        return null;
    if (!table.schema.is_list || table instanceof thingtalk_1.Ast.IndexExpression) //avoid conflict with primitives
        return null;
    if (hasUniqueFilter(table))
        return null;
    for (const [, filter] of iterateFilters(table)) {
        for (const atom of iterateFields(filter)) {
            if (atom.name === pname)
                return null;
        }
    }
    if (hasUniqueFilter(table))
        return null;
    return new thingtalk_1.Ast.SortExpression(null, table, new thingtalk_1.Ast.Value.VarRef(pname), direction, table.schema);
}
exports.makeSortedTable = makeSortedTable;
class HasIDFilterVisitor extends thingtalk_1.Ast.NodeVisitor {
    constructor() {
        super(...arguments);
        this.hasIDFilter = false;
    }
    visitAtomBooleanExpression(expr) {
        if (expr.name === 'id' && expr.operator === '==')
            this.hasIDFilter = true;
        return true;
    }
}
function checkValidQuery(table) {
    // check that the query does not include "id ==" (it should be "id =~")
    // this check is only applied at the first turn (or first turn of a new domain)
    const filterExpression = findFilterExpression(table);
    if (!filterExpression)
        return true;
    const visitor = new HasIDFilterVisitor();
    filterExpression.filter.visit(visitor);
    return !visitor.hasIDFilter;
}
exports.checkValidQuery = checkValidQuery;
function toChainExpression(expr) {
    if (expr instanceof thingtalk_1.Ast.ChainExpression)
        return expr;
    else
        return new thingtalk_1.Ast.ChainExpression(null, [expr], expr.schema);
}
exports.toChainExpression = toChainExpression;
function makeProgram(loader, rule) {
    if (!checkValidQuery(rule))
        return null;
    const chain = toChainExpression(rule);
    if (chain.first.schema.functionType === 'stream' && loader.flags.nostream)
        return null;
    return adjustDefaultParameters(new thingtalk_1.Ast.Program(null, [], [], [new thingtalk_1.Ast.ExpressionStatement(null, chain)]));
}
exports.makeProgram = makeProgram;
function combineStreamCommand(stream, command) {
    const join = makeChainExpression(stream, command);
    if (isSelfJoinStream(join))
        return null;
    return join;
}
exports.combineStreamCommand = combineStreamCommand;
function combineStreamQuery(loader, stream, table) {
    if (table instanceof thingtalk_1.Ast.ProjectionExpression) {
        if (!loader.flags.projection)
            return null;
        if (table.args[0] === 'picture_url' || table.args[0] === '$event')
            return null;
        const outParams = Object.keys(table.expression.schema.out);
        if (outParams.length === 1)
            return null;
    }
    if (utils_1.isSameFunction(stream.schema, table.schema))
        return null;
    return new thingtalk_1.Ast.ChainExpression(null, [stream, table], table.schema);
}
exports.combineStreamQuery = combineStreamQuery;
function checkComputeFilter(table, filter) {
    if (!(filter.lhs instanceof thingtalk_1.Ast.ComputationValue))
        return false;
    // distance
    if (filter.lhs.op === 'distance') {
        assert_1.default.strictEqual(filter.lhs.operands.length, 2);
        if (!(filter.rhs instanceof thingtalk_1.Ast.MeasureValue) || Units.normalizeUnit(filter.rhs.unit) !== 'm')
            return false;
        for (const operand of filter.lhs.operands) {
            if (operand instanceof thingtalk_1.Ast.VarRefValue && !table.schema.hasArgument(operand.name))
                return false;
            if (!(operand.isVarRef || operand.isLocation))
                return false;
        }
        return true;
    }
    // count, sum, avg, min, max
    if (filter.lhs.operands.length !== 1)
        return false;
    const param = filter.lhs.operands[0];
    if (!(param instanceof thingtalk_1.Ast.VarRefValue))
        return false;
    if (!table.schema.out[param.name])
        return false;
    let vtype, ftype;
    const ptype = table.schema.out[param.name];
    if (!(ptype instanceof thingtalk_1.Type.Array))
        return false;
    if (filter.lhs.op === 'count') {
        vtype = thingtalk_1.Type.Number;
        const canonical = table.schema.getArgCanonical(param.name);
        for (const p of table.schema.iterateArguments()) {
            if (p.name === param.name + 'Count')
                return false;
            if (p.canonical === canonical + 'count' || p.canonical === canonical.slice(0, -1) + ' count')
                return false;
        }
    }
    else {
        // ???
        //if (param.field && param.field in ptype.elem.fields)
        //    ftype = ptype.elem.fields[param.field].type;
        //else
        ftype = ptype.elem;
        vtype = ftype;
    }
    return filter.rhs.getType().equals(vtype);
}
function checkAtomFilter(table, filter) {
    const arg = table.schema.getArgument(filter.name);
    if (!arg || arg.is_input)
        return false;
    if (arg.getAnnotation('filterable') === false)
        return false;
    const ptype = table.schema.out[filter.name];
    const vtype = ptype;
    let vtypes = [ptype];
    if (filter.operator === 'contains') {
        if (ptype instanceof thingtalk_1.Type.Array)
            vtypes = [ptype.elem];
        else if (ptype === thingtalk_1.Type.RecurrentTimeSpecification)
            vtypes = [thingtalk_1.Type.Date, thingtalk_1.Type.Time];
        else
            return false;
    }
    else if (filter.operator === 'contains~') {
        if (!(vtype instanceof thingtalk_1.Type.Array) || (!(vtype.elem instanceof thingtalk_1.Type.Entity) && vtype.elem !== thingtalk_1.Type.String))
            return false;
        vtypes = [thingtalk_1.Type.String];
    }
    else if (filter.operator === 'in_array') {
        vtypes = [new thingtalk_1.Type.Array(ptype)];
    }
    else if (filter.operator === 'in_array~') {
        if (!vtype.isEntity && !vtype.isString)
            return false;
        vtypes = [new thingtalk_1.Type.Array(thingtalk_1.Type.String)];
    }
    else if (filter.operator === '=~') {
        if (!ptype.isEntity && !ptype.isString)
            return false;
        if (ptype.isEntity && filter.name !== 'id')
            return false;
        vtypes = [thingtalk_1.Type.String];
    }
    let typeMatch = false;
    for (const type of vtypes) {
        if (filter.value.getType().equals(type))
            typeMatch = true;
    }
    if (!typeMatch)
        return false;
    if (vtype.isNumber || vtype.isMeasure) {
        let min = -Infinity;
        const minArg = arg.getImplementationAnnotation('min_number');
        if (minArg !== undefined)
            min = minArg;
        const maxArg = arg.getImplementationAnnotation('max_number');
        let max = Infinity;
        if (maxArg !== undefined)
            max = maxArg;
        if (filter.value.isNumber) {
            const value = filter.value.toJS();
            if (value < min || value > max)
                return false;
        }
    }
    return true;
}
function internalCheckFilter(table, filter) {
    while (table instanceof thingtalk_1.Ast.ProjectionExpression)
        table = table.expression;
    if (filter instanceof thingtalk_1.Ast.NotBooleanExpression)
        filter = filter.expr;
    if (filter instanceof thingtalk_1.Ast.ExternalBooleanExpression) // FIXME
        return true;
    if (filter instanceof thingtalk_1.Ast.AndBooleanExpression ||
        filter instanceof thingtalk_1.Ast.OrBooleanExpression) {
        for (const operands of filter.operands) {
            if (!internalCheckFilter(table, operands))
                return false;
        }
        return true;
    }
    if (filter instanceof thingtalk_1.Ast.ComputeBooleanExpression)
        return checkComputeFilter(table, filter);
    if (filter instanceof thingtalk_1.Ast.AtomBooleanExpression)
        return checkAtomFilter(table, filter);
    if (filter instanceof thingtalk_1.Ast.DontCareBooleanExpression) {
        const arg = table.schema.getArgument(filter.name);
        if (!arg || arg.is_input)
            return false;
        if (arg.getAnnotation('filterable') === false)
            return false;
        return true;
    }
    throw new Error(`Unexpected filter type ${filter}`);
}
function checkFilter(table, filter) {
    if (filter.schema !== null && !utils_1.isSameFunction(table.schema, filter.schema))
        return false;
    return internalCheckFilter(table, filter.ast);
}
exports.checkFilter = checkFilter;
function* iterateFilters(table) {
    if (table instanceof thingtalk_1.Ast.InvocationExpression ||
        table instanceof thingtalk_1.Ast.FunctionCallExpression)
        return;
    if (table instanceof thingtalk_1.Ast.FilterExpression) {
        yield [table.schema, table.filter];
    }
    else if (table instanceof thingtalk_1.Ast.ChainExpression) {
        for (const expr of table.expressions)
            yield* iterateFilters(expr);
    }
    else {
        yield* iterateFilters(table.expression);
    }
}
exports.iterateFilters = iterateFilters;
function* iterateFields(filter) {
    assert_1.default(filter instanceof thingtalk_1.Ast.BooleanExpression);
    if (filter instanceof thingtalk_1.Ast.AndBooleanExpression) {
        for (const operand of filter.operands)
            yield* iterateFields(operand);
    }
    else if (filter instanceof thingtalk_1.Ast.NotBooleanExpression) {
        yield* iterateFields(filter.expr);
    }
    else if (filter instanceof thingtalk_1.Ast.AtomBooleanExpression || filter instanceof thingtalk_1.Ast.DontCareBooleanExpression) {
        yield filter;
    }
    else {
        assert_1.default(filter.isTrue || filter.isFalse || filter.isOr || filter.isCompute || filter.isExternal);
    }
}
exports.iterateFields = iterateFields;
function hasUniqueFilter(table) {
    for (const [, filter] of iterateFilters(table)) {
        if (checkFilterUniqueness(table, filter))
            return true;
    }
    return false;
}
exports.hasUniqueFilter = hasUniqueFilter;
function checkFilterUniqueness(table, filter) {
    if (filter instanceof thingtalk_1.Ast.AndBooleanExpression)
        return filter.operands.some((f) => checkFilterUniqueness(table, f));
    // note: a filter of the form
    // (id == "foo" || id == "bar")
    // is treated as "unique" because it defines the set of elements
    // and we should not filter further
    if (filter instanceof thingtalk_1.Ast.OrBooleanExpression)
        return filter.operands.every((f) => checkFilterUniqueness(table, f));
    if (filter instanceof thingtalk_1.Ast.ExternalBooleanExpression)
        return false;
    if (filter instanceof thingtalk_1.Ast.NotBooleanExpression)
        return true;
    if (filter.isTrue || filter.isFalse)
        return false;
    if (filter instanceof thingtalk_1.Ast.ComputeBooleanExpression ||
        filter instanceof thingtalk_1.Ast.DontCareBooleanExpression)
        return false;
    assert_1.default(filter instanceof thingtalk_1.Ast.AtomBooleanExpression);
    if (filter.operator !== '==' && filter.operator !== 'in_array')
        return false;
    const arg = table.schema.getArgument(filter.name);
    if (!arg)
        return false;
    return arg.unique;
}
function addFilterInternal(table, filter, options) {
    // when an "unique" parameter has been used in the table
    if (table.schema.no_filter)
        return null;
    // if the query is single result, only add "if" filters, not "with" filters
    // ("if" filters are only used with streams)
    if (!table.schema.is_list && !options.ifFilter)
        return null;
    // go inside these to add a filter, so we can attach a filter to a primitive
    // template that uses some of these expressions
    //
    // note: optimize() will take care of projection and sort, but not index
    // and slice, because index of a filter is different than filter of a index
    // the semantics in natural language are always of index of a filter!
    if (table instanceof thingtalk_1.Ast.ProjectionExpression ||
        table instanceof thingtalk_1.Ast.SortExpression ||
        table instanceof thingtalk_1.Ast.IndexExpression ||
        table instanceof thingtalk_1.Ast.SliceExpression) {
        const added = addFilterInternal(table.expression, filter, options);
        if (added === null)
            return null;
        if (table instanceof thingtalk_1.Ast.ProjectionExpression)
            return new thingtalk_1.Ast.ProjectionExpression(null, added, table.args, table.computations, table.aliases, table.schema);
        else if (table instanceof thingtalk_1.Ast.SortExpression)
            return new thingtalk_1.Ast.SortExpression(null, added, table.value, table.direction, table.schema);
        else if (table instanceof thingtalk_1.Ast.IndexExpression)
            return new thingtalk_1.Ast.IndexExpression(null, added, table.indices, table.schema);
        else
            return new thingtalk_1.Ast.SliceExpression(null, added, table.base, table.limit, table.schema);
    }
    if (table instanceof thingtalk_1.Ast.FilterExpression) {
        // if we already have a filter, don't add a new complex filter
        if (!filter.isAtom && !(filter instanceof thingtalk_1.Ast.NotBooleanExpression && filter.expr.isAtom))
            return null;
        if (checkFilterUniqueness(table, filter))
            return null;
        if (hasUniqueFilter(table))
            return null;
        const existing = table.filter;
        const atom = filter instanceof thingtalk_1.Ast.NotBooleanExpression ? filter.expr : filter;
        assert_1.default(atom instanceof thingtalk_1.Ast.AtomBooleanExpression);
        // check that we don't create a non-sensical filter, eg.
        // p == X && p == Y, or p > X && p > Y
        const operands = existing instanceof thingtalk_1.Ast.AndBooleanExpression ? existing.operands : [existing];
        for (const operand of operands) {
            if (operand instanceof thingtalk_1.Ast.AtomBooleanExpression &&
                operand.name === atom.name &&
                (operand.operator === atom.operator ||
                    operand.operator === '==' ||
                    atom.operator === '==' ||
                    operand.operator === 'in_array' ||
                    atom.operator === 'in_array'))
                return null;
        }
        const arg = table.schema.getArgument(atom.name);
        const conflict = arg.getImplementationAnnotation('conflict_filter');
        if (conflict !== undefined) {
            for (const atom2 of iterateFields(existing)) {
                if (conflict.includes(atom2.name))
                    return null;
            }
        }
        const newFilter = new thingtalk_1.Ast.BooleanExpression.And(null, [existing, filter]).optimize();
        return new thingtalk_1.Ast.FilterExpression(null, table.expression, newFilter, table.schema);
    }
    // FIXME deal with the other table types (maybe)
    const schema = table.schema.clone();
    if (checkFilterUniqueness(table, filter)) {
        schema.is_list = false;
        schema.no_filter = true;
    }
    return new thingtalk_1.Ast.FilterExpression(null, table, filter, schema);
}
function addFilter(table, filter, options = {}) {
    if (!checkFilter(table, filter))
        return null;
    return addFilterInternal(table, filter.ast, options);
}
exports.addFilter = addFilter;
function tableToStream(table) {
    if (!table.schema.is_monitorable)
        return null;
    let projArg = null;
    // remove a projection without computation, if present, and use to limit
    // which fields to monitor on
    //
    // (this means the user will monitor only the named fields, but see all
    // the fields in the notification)
    if (table instanceof thingtalk_1.Ast.ProjectionExpression && table.computations.length === 0) {
        projArg = table.args;
        table = table.expression;
        if (projArg[0] === '$event')
            return null;
    }
    let stream;
    if (table instanceof thingtalk_1.Ast.FilterExpression && !table.schema.is_list)
        stream = new thingtalk_1.Ast.FilterExpression(null, new thingtalk_1.Ast.MonitorExpression(null, table.expression, projArg, table.expression.schema), table.filter, table.expression.schema);
    else
        stream = new thingtalk_1.Ast.MonitorExpression(null, table, projArg, table.schema);
    return stream;
}
exports.tableToStream = tableToStream;
function builtinSayAction(loader, pname) {
    if (!loader.standardSchemas.say)
        return null;
    const selector = new thingtalk_1.Ast.DeviceSelector(null, 'org.thingpedia.builtin.thingengine.builtin', null, null);
    if (pname instanceof thingtalk_1.Ast.Value) {
        const param = new thingtalk_1.Ast.InputParam(null, 'message', pname);
        return new thingtalk_1.Ast.InvocationExpression(null, new thingtalk_1.Ast.Invocation(null, selector, 'say', [param], loader.standardSchemas.say), loader.standardSchemas.say.removeArgument('message'));
    }
    else if (pname) {
        const param = new thingtalk_1.Ast.InputParam(null, 'message', new thingtalk_1.Ast.Value.VarRef(pname));
        return new thingtalk_1.Ast.InvocationExpression(null, new thingtalk_1.Ast.Invocation(null, selector, 'say', [param], loader.standardSchemas.say), loader.standardSchemas.say.removeArgument('message'));
    }
    else {
        return new thingtalk_1.Ast.InvocationExpression(null, new thingtalk_1.Ast.Invocation(null, selector, 'say', [], loader.standardSchemas.say), loader.standardSchemas.say.removeArgument('message'));
    }
}
exports.builtinSayAction = builtinSayAction;
function locationGetPredicate(loader, loc, negate = false) {
    if (!loader.standardSchemas.get_gps)
        return null;
    let filter = new thingtalk_1.Ast.BooleanExpression.Atom(null, 'location', '==', loc);
    if (negate)
        filter = new thingtalk_1.Ast.BooleanExpression.Not(null, filter);
    return { schema: null, ptype: null,
        ast: new thingtalk_1.Ast.BooleanExpression.External(null, new thingtalk_1.Ast.DeviceSelector(null, 'org.thingpedia.builtin.thingengine.builtin', null, null), 'get_gps', [], filter, loader.standardSchemas.get_gps) };
}
exports.locationGetPredicate = locationGetPredicate;
function timeGetPredicate(loader, low, high) {
    if (!loader.standardSchemas.get_time)
        return null;
    const operands = [];
    if (low)
        operands.push(new thingtalk_1.Ast.BooleanExpression.Atom(null, 'time', '>=', low));
    if (high)
        operands.push(new thingtalk_1.Ast.BooleanExpression.Atom(null, 'time', '<=', high));
    const filter = new thingtalk_1.Ast.BooleanExpression.And(null, operands);
    return { schema: null, ptype: null,
        ast: new thingtalk_1.Ast.BooleanExpression.External(null, new thingtalk_1.Ast.DeviceSelector(null, 'org.thingpedia.builtin.thingengine.builtin', null, null), 'get_time', [], filter, loader.standardSchemas.get_time) };
}
exports.timeGetPredicate = timeGetPredicate;
function hasGetPredicate(filter) {
    if (filter instanceof thingtalk_1.Ast.AndBooleanExpression || filter instanceof thingtalk_1.Ast.OrBooleanExpression) {
        for (const op of filter.operands) {
            if (hasGetPredicate(op))
                return true;
        }
        return false;
    }
    if (filter instanceof thingtalk_1.Ast.NotBooleanExpression)
        return hasGetPredicate(filter.expr);
    return filter instanceof thingtalk_1.Ast.ExternalBooleanExpression;
}
exports.hasGetPredicate = hasGetPredicate;
function makeGetPredicate(proj, op, value, negate = false) {
    if (!(proj instanceof thingtalk_1.Ast.ProjectionExpression) || proj.args.length === 0)
        return null;
    if (!(proj.expression instanceof thingtalk_1.Ast.InvocationExpression))
        return null;
    const arg = proj.args[0];
    if (arg === '$event')
        return null;
    let filter = new thingtalk_1.Ast.BooleanExpression.Atom(null, arg, op, value);
    if (negate)
        filter = new thingtalk_1.Ast.BooleanExpression.Not(null, filter);
    const selector = proj.expression.invocation.selector;
    const channel = proj.expression.invocation.channel;
    const schema = proj.expression.invocation.schema;
    if (!schema.out[arg].equals(value.getType()))
        return null;
    return { schema: null, ptype: null,
        ast: new thingtalk_1.Ast.BooleanExpression.External(null, selector, channel, proj.expression.invocation.in_params, filter, proj.expression.invocation.schema) };
}
exports.makeGetPredicate = makeGetPredicate;
function resolveChain(expressions) {
    // the schema of a chain is just the schema of the last function in
    // the chain, nothing special about it - no joins, no merging, no
    // nothing
    const last = expressions[expressions.length - 1];
    // except the schema is monitorable if the _every_ schema is monitorable
    // and the schema is a list if _any_ schema is a list
    const clone = last.schema.clone();
    clone.is_list = expressions.some((exp) => exp.schema.is_list);
    clone.is_monitorable = expressions.every((exp) => exp.schema.is_monitorable);
    return clone;
}
exports.resolveChain = resolveChain;
function filterTableJoin(into, filteredTable) {
    // FIXME joins need to use subqueries not chains, otherwise parameters won't be available
    return null;
    /*
    if (filteredTable === null)
        return null;
    if (!(filteredTable instanceof Ast.FilterExpression))
        return null;
    let tableName;
    for (const [, invocation] of filteredTable.iteratePrimitives(false))
        tableName = invocation.channel;
    let passign;
    for (const arg of into.schema!.iterateArguments()) {
        if (arg.name !== 'id' && arg.type instanceof Type.Entity && arg.type.type.substring(arg.type.type.indexOf(':') + 1) === tableName)
            passign = arg;
    }
    if (!passign)
        return null;

    const newSchema = resolveChain(filteredTable.schema!, into.schema!);

    // TODO this should be a subquery not a chain expression

    const join = new Ast.ChainExpression(null, [filteredTable, into], newSchema);
    const filter = new Ast.BooleanExpression.Atom(null,
        passign.name, '==', new Ast.Value.VarRef('id')
    );
    return new Ast.FilterExpression(null, join, filter, newSchema);
    */
}
exports.filterTableJoin = filterTableJoin;
function arrayFilterTableJoin(into, filteredTable) {
    // FIXME joins need to use subqueries not chains, otherwise parameters won't be available
    return null;
    /*
    if (filteredTable === null)
        return null;
    if (!(filteredTable instanceof Ast.FilterExpression))
        return null;
    let tableName;
    for (const [, invocation] of filteredTable.iteratePrimitives(false))
        tableName = invocation.channel;
    let passign;
    for (const arg of into.schema!.iterateArguments()) {
        if (arg.type instanceof Type.Array && arg.type.elem instanceof Type.Entity && arg.type.elem.type.substring(arg.type.elem.type.indexOf(':') + 1) === tableName)
            passign = arg;
    }
    if (!passign)
        return null;

    const newSchema = resolveChain(filteredTable.schema!, into.schema!);

    // TODO this should be a subquery not a chain expression

    const join = new Ast.ChainExpression(null, [filteredTable, into], newSchema);
    const filter = new Ast.BooleanExpression.Atom(null,
        passign.name, 'contains', new Ast.Value.VarRef('id')
    );
    return new Ast.FilterExpression(null, join, filter, newSchema);
    */
}
exports.arrayFilterTableJoin = arrayFilterTableJoin;
function makeChainExpression(first, second) {
    // flatten chains and compute the schema
    const expressions = [];
    if (first instanceof thingtalk_1.Ast.ChainExpression)
        expressions.push(...first.expressions);
    else
        expressions.push(first);
    if (second instanceof thingtalk_1.Ast.ChainExpression)
        expressions.push(...second.expressions);
    else
        expressions.push(second);
    return new thingtalk_1.Ast.ChainExpression(null, expressions, resolveChain(expressions));
}
exports.makeChainExpression = makeChainExpression;
function addParameterPassing(first, second) {
    // no self-joins
    if (utils_1.isSameFunction(first.schema, second.expression.schema))
        return null;
    if (second.slot !== null) {
        // specific parameter passing
        if (!utils_1.isSameFunction(second.slot.schema, first.schema))
            return null;
        // all we need to do is to check compatibility, the rest follows
        // (we need to check both function and type in case of projections/aggregations
        // or in case the parameter is a nested parameter of a compound type
        const lhsType = first.schema.getArgType(second.slot.name);
        if (!lhsType || !lhsType.equals(second.type))
            return null;
        return makeChainExpression(first, second.expression);
    }
    else {
        // implicit parameter passing, or param passing by projection
        assert_1.default(second.pname);
        let lhsName, lhsType;
        let table = first;
        if (table instanceof thingtalk_1.Ast.MonitorExpression)
            table = table.expression;
        if (table instanceof thingtalk_1.Ast.ProjectionExpression) {
            const args = getProjectionArguments(table);
            assert_1.default(args.length > 0);
            if (args.length > 1)
                return null;
            lhsName = args[0];
            lhsType = table.schema.getArgType(lhsName);
        }
        else {
            lhsName = getImplicitParameterPassing(table.schema);
            if (lhsName === '$event')
                lhsType = thingtalk_1.Type.String;
            else
                lhsType = table.schema.getArgType(lhsName);
        }
        assert_1.default(lhsType);
        if (!lhsType.equals(second.type))
            return null;
        const joinArg = lhsName === '$event' ? new thingtalk_1.Ast.Value.Event(null) : new thingtalk_1.Ast.Value.VarRef(lhsName);
        const reduced = betaReduceMany(second.expression, { [second.pname]: joinArg });
        if (reduced === null)
            return null;
        return makeChainExpression(first, reduced);
    }
}
exports.addParameterPassing = addParameterPassing;
function addSameNameParameterPassing(loader, chain, joinArg) {
    const action = chain.last;
    assert_1.default(action instanceof thingtalk_1.Ast.InvocationExpression);
    const table = chain.lastQuery;
    if (!utils_1.isSameFunction(action.schema, joinArg.schema))
        return null;
    // prevent self-joins
    if (utils_1.isSameFunction(action.schema, table.schema))
        return null;
    const actiontype = action.schema.inReq[joinArg.name];
    if (!actiontype)
        return null;
    if (action.invocation.in_params.some((p) => p.name === joinArg.name))
        return null;
    const commandtype = table.schema.out[joinArg.name];
    if (!commandtype || !thingtalk_1.Type.isAssignable(commandtype, actiontype, {}, loader.entitySubTypeMap))
        return null;
    // FIXME
    //if (joinArg.isEvent && (stream instanceof Ast.FunctionCallExpression)) // timer
    //    return null;
    const clone = action.clone();
    clone.invocation.in_params.push(new thingtalk_1.Ast.InputParam(null, joinArg.name, new thingtalk_1.Ast.Value.VarRef(joinArg.name)));
    const newExpressions = chain.expressions.slice(0, chain.expressions.length - 1);
    newExpressions.push(clone);
    return new thingtalk_1.Ast.ChainExpression(null, newExpressions, resolveChain(newExpressions));
}
exports.addSameNameParameterPassing = addSameNameParameterPassing;
function isConstantAssignable(loader, value, ptype) {
    if (!ptype)
        return false;
    const vtype = value.getType();
    if (!thingtalk_1.Type.isAssignable(vtype, ptype, {}, loader.entitySubTypeMap))
        return false;
    // prevent mixing date and type (ThingTalk allows it to support certain time get predicates)
    if ((vtype.isDate && ptype.isTime) || (vtype.isTime && ptype.isDate))
        return false;
    if (value instanceof thingtalk_1.Ast.EnumValue && (!(ptype instanceof thingtalk_1.Type.Enum) || ptype.entries.indexOf(value.value) < 0))
        return false;
    return true;
}
exports.isConstantAssignable = isConstantAssignable;
function replacePlaceholderWithUndefined(lhs, param) {
    return betaReduceMany(lhs, { [param]: new thingtalk_1.Ast.Value.Undefined(true) });
}
exports.replacePlaceholderWithUndefined = replacePlaceholderWithUndefined;
function sayProjection(loader, maybeProj) {
    if (maybeProj === null)
        return null;
    // this function is also used for aggregation
    if (maybeProj instanceof thingtalk_1.Ast.ProjectionExpression) {
        const proj = maybeProj;
        assert_1.default(proj.args.length > 0 || proj.computations.length > 0);
        if (proj.args.length === 1 && proj.args[0] === 'picture_url')
            return null;
        if (proj.args.length === 1 && proj.args[0] === '$event')
            return null;
        // if the function only contains one parameter, do not generate projection for it
        if (proj.computations.length === 0 && Object.keys(proj.expression.schema.out).length === 1)
            return null;
        if (!loader.flags.projection)
            return null;
        // remove all projection args that are part of the minimal projection
        const newArgs = proj.args.filter((a) => !proj.expression.schema.minimal_projection.includes(a));
        // note: the schema does not change! that's the whole point of minimal projection
        if (newArgs.length === 0 && proj.computations.length === 0) {
            maybeProj = proj.expression;
        }
        else {
            newArgs.sort();
            maybeProj.args = newArgs;
        }
    }
    return maybeProj;
}
exports.sayProjection = sayProjection;
function hasConflictParam(table, pname, operation) {
    function cleanName(name) {
        if (name.endsWith(' value'))
            name = name.substring(0, name.length - ' value'.length);
        if (name.includes('.')) {
            const components = name.split('.');
            name = components[components.length - 1];
        }
        return name;
    }
    const pcleaned = cleanName(pname);
    for (const arg in table.schema.out) {
        if (!table.schema.out[arg].isNumber)
            continue;
        if (cleanName(table.schema.getArgCanonical(arg)) === `${pcleaned} ${operation}`)
            return arg;
    }
    return null;
}
exports.hasConflictParam = hasConflictParam;
function maybeGetIdFilter(filter) {
    for (const atom of iterateFields(filter)) {
        if (atom.name === 'id' && atom instanceof thingtalk_1.Ast.AtomBooleanExpression)
            return atom.value;
    }
    return undefined;
}
function addReverseGetPredicateJoin(table, get_predicate_table, pname, negate = false) {
    if (!(get_predicate_table instanceof thingtalk_1.Ast.InvocationExpression) &&
        !(get_predicate_table instanceof thingtalk_1.Ast.FilterExpression &&
            get_predicate_table.expression instanceof thingtalk_1.Ast.InvocationExpression))
        return null;
    const idType = table.schema.getArgType('id');
    if (!idType || !idType.isEntity)
        return null;
    let lhsArg = undefined;
    assert_1.default(pname);
    lhsArg = get_predicate_table.schema.getArgument(pname);
    if (!lhsArg)
        return null;
    if (!(lhsArg.type.equals(idType) ||
        (lhsArg.type instanceof thingtalk_1.Type.Array && lhsArg.type.elem.equals(idType))))
        return null;
    if (lhsArg.name === 'id')
        return null;
    const invocation = get_predicate_table instanceof thingtalk_1.Ast.FilterExpression ? get_predicate_table.expression.invocation : get_predicate_table.invocation;
    const newAtom = new thingtalk_1.Ast.BooleanExpression.Atom(null, pname, (lhsArg.type.isArray ? 'contains' : '=='), new thingtalk_1.Ast.Value.VarRef('id'));
    let get_predicate = new thingtalk_1.Ast.BooleanExpression.External(null, invocation.selector, invocation.channel, invocation.in_params, new thingtalk_1.Ast.BooleanExpression.And(null, [
        get_predicate_table instanceof thingtalk_1.Ast.FilterExpression ? get_predicate_table.filter : thingtalk_1.Ast.BooleanExpression.True,
        newAtom
    ]), invocation.schema);
    if (negate)
        get_predicate = new thingtalk_1.Ast.BooleanExpression.Not(null, get_predicate);
    return addFilterInternal(table, get_predicate, {});
}
exports.addReverseGetPredicateJoin = addReverseGetPredicateJoin;
function addGetPredicateJoin(table, get_predicate_table, pname, negate = false) {
    if (!(get_predicate_table instanceof thingtalk_1.Ast.FilterExpression) ||
        !(get_predicate_table.expression instanceof thingtalk_1.Ast.InvocationExpression))
        return null;
    const idType = get_predicate_table.schema.getArgType('id');
    if (!idType || !idType.isEntity)
        return null;
    let lhsArg = undefined;
    if (pname) {
        lhsArg = table.schema.getArgument(pname);
        if (!lhsArg)
            return null;
        if (!(lhsArg.type.equals(idType) ||
            (lhsArg.type instanceof thingtalk_1.Type.Array && lhsArg.type.elem.equals(idType))))
            return null;
    }
    else {
        for (const arg of table.schema.iterateArguments()) {
            if (arg.type.equals(idType) ||
                (arg.type instanceof thingtalk_1.Type.Array && arg.type.elem.equals(idType))) {
                lhsArg = arg;
                break;
            }
        }
        if (!lhsArg)
            return null;
    }
    if (lhsArg.name === 'id')
        return null;
    const idFilter = maybeGetIdFilter(get_predicate_table.filter);
    if (idFilter) {
        if (idFilter.isString)
            return null;
        let newAtom = new thingtalk_1.Ast.BooleanExpression.Atom(null, lhsArg.name, lhsArg.type.isArray ? 'contains' : '==', idFilter);
        if (negate)
            newAtom = new thingtalk_1.Ast.BooleanExpression.Not(null, newAtom);
        return addFilterInternal(table, newAtom, {});
    }
    let newAtom = new thingtalk_1.Ast.BooleanExpression.Atom(null, 'id', (lhsArg.type.isArray ? 'in_array' : '=='), new thingtalk_1.Ast.Value.VarRef(lhsArg.name));
    if (negate)
        newAtom = new thingtalk_1.Ast.BooleanExpression.Not(null, newAtom);
    const get_predicate = new thingtalk_1.Ast.BooleanExpression.External(null, get_predicate_table.expression.invocation.selector, get_predicate_table.expression.invocation.channel, get_predicate_table.expression.invocation.in_params, new thingtalk_1.Ast.BooleanExpression.And(null, [get_predicate_table.filter, newAtom]), get_predicate_table.expression.invocation.schema);
    return addFilterInternal(table, get_predicate, {});
}
exports.addGetPredicateJoin = addGetPredicateJoin;
function addArrayJoin(lhs, rhs) {
    // FIXME joins need to use subqueries not chains, otherwise parameters won't be available
    return null;
    /*
    if (!(lhs instanceof Ast.FilterExpression))
        return null;

    const idType = rhs.schema!.getArgType('id');
    if (!idType || !idType.isEntity)
        return null;
    let lhsArg = undefined;
    for (const arg of lhs.schema!.iterateArguments()) {
        if (arg.type.equals(idType) ||
            (arg.type instanceof Type.Array && (arg.type.elem as Type).equals(idType))) {
            lhsArg = arg;
            break;
        }
    }
    if (!lhsArg)
        return null;
    if (lhsArg.name === 'id')
        return null;
    // if rhs has the same argument, lhsArg will be overridden
    if (rhs.schema!.hasArgument(lhsArg.name))
        return null;

    const newSchema = resolveChain(lhs.schema!, rhs.schema!);
    return new Ast.FilterExpression(null,
        new Ast.ChainExpression(null, [lhs, rhs], newSchema),
        new Ast.BooleanExpression.Atom(null, 'id', (lhsArg.type.isArray ? 'in_array' : '=='), new Ast.Value.VarRef(lhsArg.name)),
        newSchema);
    */
}
exports.addArrayJoin = addArrayJoin;
function makeComputeExpression(table, operation, operands, resultType) {
    const expression = new thingtalk_1.Ast.Value.Computation(operation, operands, null, resultType);
    if (operation === 'distance')
        expression.overload = [thingtalk_1.Type.Location, thingtalk_1.Type.Location, resultType];
    return new thingtalk_1.Ast.ProjectionExpression(null, table, [], [expression], [null], resolveProjection(table.schema, [], [expression]));
}
exports.makeComputeExpression = makeComputeExpression;
function makeComputeFilterExpression(table, operation, operands, resultType, filterOp, filterValue) {
    // do not compute on a computed table
    if (table instanceof thingtalk_1.Ast.ProjectionExpression && table.computations.length > 0)
        return null;
    const expression = new thingtalk_1.Ast.Value.Computation(operation, operands);
    if (operation === 'distance') {
        expression.overload = [thingtalk_1.Type.Location, thingtalk_1.Type.Location, new thingtalk_1.Type.Measure('m')];
        expression.type = new thingtalk_1.Type.Measure('m');
    }
    const filter = {
        schema: null,
        ptype: expression.type,
        ast: new thingtalk_1.Ast.BooleanExpression.Compute(null, expression, filterOp, filterValue)
    };
    return addFilter(table, filter);
}
exports.makeComputeFilterExpression = makeComputeFilterExpression;
function makeWithinGeoDistanceExpression(table, location, filterValue) {
    if (!table.schema.out.geo || !table.schema.out.geo.isLocation)
        return null;
    const filterType = filterValue.getType();
    if (!(filterType instanceof thingtalk_1.Type.Measure))
        return null;
    const unit = filterType.unit;
    assert_1.default(unit);
    if (Units.normalizeUnit(unit) !== 'm')
        return null;
    // the unit should be at least feet
    if (Units.transformToBaseUnit(1, unit) < Units.transformToBaseUnit(1, 'ft'))
        return null;
    // the distance should be at least 100 meters (if the value is small number)
    if (filterValue instanceof thingtalk_1.Ast.MeasureValue && Units.transformToBaseUnit(filterValue.value, unit) < 100)
        return null;
    return makeComputeFilterExpression(table, 'distance', [new thingtalk_1.Ast.Value.VarRef('geo'), location], new thingtalk_1.Type.Measure('m'), '<=', filterValue);
}
exports.makeWithinGeoDistanceExpression = makeWithinGeoDistanceExpression;
function makeComputeArgMinMaxExpression(table, operation, operands, resultType, direction = 'desc') {
    if (hasUniqueFilter(table))
        return null;
    for (const [, filter] of iterateFilters(table)) {
        for (const atom of iterateFields(filter)) {
            if (atom.name === operands[0].name)
                return null;
        }
    }
    const expression = new thingtalk_1.Ast.Value.Computation(operation, operands, null, resultType);
    if (operation === 'distance')
        expression.overload = [thingtalk_1.Type.Location, thingtalk_1.Type.Location, resultType];
    const sort = new thingtalk_1.Ast.SortExpression(null, table, expression, direction, table.schema);
    return new thingtalk_1.Ast.IndexExpression(null, sort, [new thingtalk_1.Ast.Value.Number(1)], table.schema);
}
exports.makeComputeArgMinMaxExpression = makeComputeArgMinMaxExpression;
function makeAggComputeValue(table, operation, field, slot, resultType) {
    if (!utils_1.isSameFunction(table.schema, slot.schema))
        return null;
    if (hasUniqueFilter(table))
        return null;
    const list = slot.ast;
    if (list instanceof thingtalk_1.Ast.VarRefValue) {
        const name = list.name;
        assert_1.default(typeof name === 'string');
        const canonical = table.schema.getArgCanonical(name);
        for (const p of table.schema.iterateArguments()) {
            if (p.name === name + 'Count' || p.canonical === canonical + 'count' || p.canonical === canonical.slice(0, -1) + ' count')
                return new thingtalk_1.Ast.Value.VarRef(p.name);
        }
    }
    const expression = new thingtalk_1.Ast.Value.Computation(operation, [field ? new thingtalk_1.Ast.Value.ArrayField(list, field) : list]);
    if (operation === 'count') {
        expression.overload = [new thingtalk_1.Type.Array('x'), thingtalk_1.Type.Number];
        expression.type = thingtalk_1.Type.Number;
    }
    else {
        expression.overload = [new thingtalk_1.Type.Array(resultType), resultType];
        expression.type = resultType;
    }
    return expression;
}
function makeAggComputeExpression(table, operation, field, list, resultType) {
    const value = makeAggComputeValue(table, operation, field, list, resultType);
    if (!value)
        return null;
    if (value instanceof thingtalk_1.Ast.Value.VarRef)
        return new thingtalk_1.Ast.ProjectionExpression(null, table, [value.name], [], [], resolveProjection(table.schema, [value.name]));
    else
        return new thingtalk_1.Ast.ProjectionExpression(null, table, [], [value], [null], resolveProjection(table.schema, [], [value]));
}
exports.makeAggComputeExpression = makeAggComputeExpression;
function makeAggComputeArgMinMaxExpression(table, operation, field, list, resultType, direction = 'desc') {
    const value = makeAggComputeValue(table, operation, field, list, resultType);
    if (!value)
        return null;
    const sort = new thingtalk_1.Ast.SortExpression(null, table, value, direction, table.schema);
    return new thingtalk_1.Ast.IndexExpression(null, sort, [new thingtalk_1.Ast.Value.Number(1)], table.schema);
}
exports.makeAggComputeArgMinMaxExpression = makeAggComputeArgMinMaxExpression;
function hasArgumentOfType(invocation, type) {
    for (const arg of invocation.schema.iterateArguments()) {
        if (!arg.is_input)
            continue;
        if (arg.type.equals(type))
            return true;
    }
    return false;
}
exports.hasArgumentOfType = hasArgumentOfType;
class UsesParamVisitor extends thingtalk_1.Ast.NodeVisitor {
    constructor(pname) {
        super();
        this.pname = pname;
        this.used = false;
    }
    visitExternalBooleanExpression() {
        // do not recurse
        return false;
    }
    visitValue() {
        // do not recurse
        return false;
    }
    visitAtomBooleanExpression(atom) {
        this.used = this.used || this.pname === atom.name;
        return true;
    }
}
function filterUsesParam(filter, pname) {
    const visitor = new UsesParamVisitor(pname);
    filter.visit(visitor);
    return visitor.used;
}
exports.filterUsesParam = filterUsesParam;
function checkInvocationInputParam(loader, invocation, param, options = {}) {
    assert_1.default(invocation instanceof thingtalk_1.Ast.Invocation);
    const arg = invocation.schema.getArgument(param.ast.name);
    if (!arg || (!arg.is_input && !options.allowOutput) || !isConstantAssignable(loader, param.ast.value, arg.type))
        return false;
    if (!utils_1.isSameFunction(invocation.schema, param.schema))
        return false;
    if (arg.type.isNumber || arg.type.isMeasure) {
        // __const varref, likely
        if (!param.ast.value.isNumber && !param.ast.value.isMeasure)
            return false;
        let min = -Infinity;
        const minArg = arg.getImplementationAnnotation('min_number');
        if (minArg !== undefined)
            min = minArg;
        const maxArg = arg.getImplementationAnnotation('max_number');
        let max = Infinity;
        if (maxArg !== undefined)
            max = maxArg;
        const value = param.ast.value.toJS();
        if (value < min || value > max)
            return false;
    }
    return true;
}
exports.checkInvocationInputParam = checkInvocationInputParam;
function addInvocationInputParam(loader, invocation, param, options) {
    if (!checkInvocationInputParam(loader, invocation, param, options))
        return null;
    const clone = invocation.clone();
    for (const existing of clone.in_params) {
        if (existing.name === param.ast.name) {
            if (existing.value.isUndefined) {
                existing.value = param.ast.value;
                return clone;
            }
            else {
                return null;
            }
        }
    }
    clone.in_params.push(param.ast);
    return clone;
}
exports.addInvocationInputParam = addInvocationInputParam;
function addActionInputParam(loader, action, param, options) {
    if (action instanceof thingtalk_1.Ast.ChainExpression) {
        const added = addActionInputParam(loader, action.last, param, options);
        if (!added)
            return null;
        const clone = new thingtalk_1.Ast.ChainExpression(null, action.expressions.slice(0, action.expressions.length - 1).concat([added]), added.schema);
        return clone;
    }
    if (!(action instanceof thingtalk_1.Ast.InvocationExpression))
        return null;
    const newInvocation = addInvocationInputParam(loader, action.invocation, param, options);
    if (newInvocation === null)
        return null;
    return new thingtalk_1.Ast.InvocationExpression(null, newInvocation, action.schema);
}
exports.addActionInputParam = addActionInputParam;
/**
 * Find the filter expression in the context.
 *
 * Returns filter expression
 */
function findFilterExpression(root) {
    let expr = root;
    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) {
            expr = expr.expression;
            continue;
        }
        if (expr instanceof thingtalk_1.Ast.ChainExpression) {
            // go right on join, always, but don't cross 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
        return null;
    }
    return expr;
}
exports.findFilterExpression = findFilterExpression;
class GetInvocationVisitor extends thingtalk_1.Ast.NodeVisitor {
    constructor() {
        super(...arguments);
        this.invocation = undefined;
    }
    visitInvocation(inv) {
        this.invocation = inv;
        return false; // no need to recurse
    }
}
function getInvocation(historyItem) {
    assert_1.default(historyItem instanceof thingtalk_1.Ast.DialogueHistoryItem);
    const visitor = new GetInvocationVisitor();
    historyItem.visit(visitor);
    assert_1.default(visitor.invocation);
    return visitor.invocation;
}
exports.getInvocation = getInvocation;
class AdjustDefaultParametersVisitor extends thingtalk_1.Ast.NodeVisitor {
    visitInvocation(invocation) {
        invocation.in_params = invocation.in_params.filter((ip) => {
            const arg = invocation.schema.getArgument(ip.name);
            assert_1.default(arg && arg.is_input);
            const _default = arg.impl_annotations.default;
            if (_default && ip.value.equals(_default))
                return false;
            return true;
        });
        return false;
    }
}
function adjustDefaultParameters(stmt) {
    stmt.visit(new AdjustDefaultParametersVisitor());
    return stmt;
}
exports.adjustDefaultParameters = adjustDefaultParameters;
function expressionUsesIDFilter(expr) {
    const filterExpression = findFilterExpression(expr);
    if (!filterExpression)
        return false;
    return filterUsesParam(filterExpression.filter, 'id');
}
exports.expressionUsesIDFilter = expressionUsesIDFilter;
//# sourceMappingURL=ast_manip.js.map