"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 __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ThingTalkSimulatorState = void 0;
const assert_1 = __importDefault(require("assert"));
const thingtalk_1 = require("thingtalk");
const simulation_exec_environment_1 = require("./simulation_exec_environment");
// above MORE_SIZE, we set the "more" bit
const MORE_SIZE = 50;
// above PAGE_SIZE, we set the count but don't actually show the full list of results
const PAGE_SIZE = 10;
class ThingTalkSimulatorState {
    constructor(options) {
        this._locale = options.locale;
        this._schemas = options.schemaRetriever;
        this._rng = options.rng;
        this._database = options.database;
        this._overrides = options.overrides || new Map;
        this._execEnv = new simulation_exec_environment_1.SimulationExecEnvironment(this._locale, options.timezone, this._schemas, this._database, {
            rng: this._rng
        });
    }
    async compile(stmt, cache) {
        const clone = stmt.clone();
        const program = new thingtalk_1.Ast.Program(null, [], [], [clone]);
        const cacheKey = program.prettyprint();
        //console.error(cacheKey);
        let compiled = cache.get(cacheKey);
        if (compiled)
            return compiled;
        try {
            const compiler = new thingtalk_1.Compiler(this._schemas);
            compiled = await compiler.compileProgram(program);
            assert_1.default(compiled.rules.length === 0);
            cache.set(cacheKey, compiled);
        }
        catch (e) {
            console.error(`Failed to compile program: ` + e.message);
            console.error(program.prettyprint());
            throw e;
        }
        return compiled;
    }
    async simulate(stmt, compiled) {
        const results = [];
        const rawResults = [];
        let error = null;
        const generator = new simulation_exec_environment_1.ResultGenerator(this._rng, this._overrides);
        for (const slot of stmt.iterateSlots2()) {
            if (slot instanceof thingtalk_1.Ast.DeviceSelector)
                continue;
            generator.addCandidate(slot.get());
        }
        this._execEnv.generator = generator;
        this._execEnv.output = async (outputType, outputValue) => {
            const mapped = new thingtalk_1.Ast.DialogueHistoryResultItem(null, await this._mapResult(outputType, outputValue));
            results.push(mapped);
            rawResults.push([outputType, outputValue]);
        };
        this._execEnv.reportError = async (msg, err) => {
            if (!(err instanceof simulation_exec_environment_1.SimulatedError)) {
                console.error(`Failed to execute program`);
                console.error(msg, err);
                console.error(new thingtalk_1.Ast.Program(null, [], [], [stmt]).prettyprint());
                process.exit(1);
                return;
            }
            if (err.code)
                error = new thingtalk_1.Ast.Value.Enum(err.code);
            else
                error = new thingtalk_1.Ast.Value.String(err.message);
        };
        try {
            assert_1.default(typeof compiled.command === 'function');
            await compiled.command(this._execEnv);
        }
        catch (e) {
            console.error(`Failed to execute program: ` + e.message);
            console.error(new thingtalk_1.Ast.Program(null, [], [], [stmt]).prettyprint());
            throw e;
        }
        const numResults = results.length;
        return [new thingtalk_1.Ast.DialogueHistoryResultList(null, results.slice(0, PAGE_SIZE), new thingtalk_1.Ast.Value.Number(Math.min(MORE_SIZE, numResults)), numResults > MORE_SIZE, error), rawResults];
    }
    _inferType(jsValue) {
        if (typeof jsValue === 'boolean')
            return thingtalk_1.Type.Boolean;
        if (typeof jsValue === 'string')
            return thingtalk_1.Type.String;
        if (typeof jsValue === 'number')
            return thingtalk_1.Type.Number;
        if (jsValue instanceof thingtalk_1.Builtin.Currency)
            return thingtalk_1.Type.Currency;
        if (jsValue instanceof thingtalk_1.Builtin.Entity)
            return new thingtalk_1.Type.Entity('');
        if (jsValue instanceof thingtalk_1.Builtin.Time)
            return thingtalk_1.Type.Time;
        if (jsValue instanceof Date)
            return thingtalk_1.Type.Date;
        if (Array.isArray(jsValue) && jsValue.length > 0)
            return new thingtalk_1.Type.Array(this._inferType(jsValue[0]));
        if (Array.isArray(jsValue))
            return new thingtalk_1.Type.Array(thingtalk_1.Type.Any);
        return thingtalk_1.Type.Any;
    }
    _outputTypeToSchema(outputType) {
        const [kind, fname] = outputType.split(':');
        let ftype = 'query';
        let fname_ = fname;
        if (fname_.startsWith('action/')) {
            ftype = 'action';
            fname_ = fname_.substring('action/'.length);
        }
        return this._schemas.getSchemaAndNames(kind, ftype, fname_);
    }
    async _mapResult(outputType, outputValue) {
        const mappedResult = {};
        if (outputType === null) {
            // fallback
            for (const key in outputValue) {
                const jsValue = outputValue[key];
                mappedResult[key] = thingtalk_1.Ast.Value.fromJS(this._inferType(jsValue), jsValue);
            }
            return mappedResult;
        }
        if (outputType.indexOf('+') >= 0) {
            const types = outputType.split('+');
            outputType = types[types.length - 1];
        }
        const aggregation = /^([a-zA-Z]+)\(([^)]+)\)$/.exec(outputType);
        if (aggregation !== null) {
            let operator;
            [, operator, outputType] = aggregation;
            const field = Object.keys(outputValue)[0];
            const value = outputValue[field];
            if (operator === 'count') {
                mappedResult[field] = thingtalk_1.Ast.Value.fromJS(thingtalk_1.Type.Number, outputValue[field]);
                return mappedResult;
            }
            const schema = await this._outputTypeToSchema(outputType);
            const type = schema.getArgType(field) || this._inferType(value);
            mappedResult[field] = thingtalk_1.Ast.Value.fromJS(type, value);
        }
        else {
            const schema = await this._outputTypeToSchema(outputType);
            for (const key in outputValue) {
                const value = outputValue[key];
                if (value === null || value === undefined)
                    continue;
                const type = schema.getArgType(key) || this._inferType(value);
                if (type instanceof thingtalk_1.Type.Compound)
                    mappedResult[key] = this._mapCompound(key + '.', schema, value);
                else
                    mappedResult[key] = thingtalk_1.Ast.Value.fromJS(type, value);
            }
        }
        return mappedResult;
    }
    _mapCompound(prefix, schema, object) {
        const result = {};
        for (const key in object) {
            const value = object[key];
            const type = schema.getArgType(prefix + key) || this._inferType(value);
            if (type instanceof thingtalk_1.Type.Compound)
                result[key] = this._mapCompound(prefix + key + '.', schema, object);
            else
                result[key] = thingtalk_1.Ast.Value.fromJS(type, value);
        }
        return new thingtalk_1.Ast.Value.Object(result);
    }
}
exports.ThingTalkSimulatorState = ThingTalkSimulatorState;
/**
 * Simulate the execution of ThingTalk code.
 */
class ThingTalkStatementSimulator {
    constructor(options) {
        this._options = options;
        this.cache = new Map;
    }
    async executeStatement(stmt, execState) {
        if (stmt.stream) {
            // nothing to do, this always returns nothing
            return [new thingtalk_1.Ast.DialogueHistoryResultList(null, [], new thingtalk_1.Ast.Value.Number(0), false, null), [], execState];
        }
        if (execState === undefined)
            execState = new ThingTalkSimulatorState(this._options);
        // there is no way around this, we need to compile and run the program!
        const compiled = await execState.compile(stmt, this.cache);
        const [resultList, rawResults] = await execState.simulate(stmt, compiled);
        return [resultList, rawResults, execState];
    }
}
exports.default = ThingTalkStatementSimulator;
//# sourceMappingURL=statement_simulator.js.map