"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 });
const assert_1 = __importDefault(require("assert"));
const thingtalk_1 = require("thingtalk");
const misc_utils_1 = require("../utils/misc-utils");
const value_category_1 = __importDefault(require("./value-category"));
const statement_executor_1 = __importDefault(require("./statement_executor"));
const errors_1 = require("./errors");
const abstract_dialogue_agent_1 = __importDefault(require("./abstract_dialogue_agent"));
/**
 * The execution time dialogue agent.
 *
 * Provides access to the real user's information, stored in the engine.
 */
class ExecutionDialogueAgent extends abstract_dialogue_agent_1.default {
    constructor(engine, dlg, debug) {
        super(engine.schemas, {
            debug: debug,
            locale: engine.platform.locale,
            timezone: engine.platform.timezone
        });
        this._engine = engine;
        this._thingpedia = engine.thingpedia;
        this._platform = engine.platform;
        this._executor = new statement_executor_1.default(engine);
        this._dlg = dlg;
    }
    get _() {
        return this._dlg._;
    }
    get executor() {
        return this._executor;
    }
    getAllDevicesOfKind(kind) {
        return this._engine.getDeviceInfos(kind);
    }
    async disambiguate(type, name, choices, hint) {
        let question;
        if (type === 'device') {
            question = this._dlg.interpolate(this._("You have multiple ${?“${name}” }${device} devices. Which one do you want to use?"), {
                name,
                device: misc_utils_1.cleanKind(hint)
            });
        }
        else {
            question = this._dlg.interpolate(this._("Multiple contacts match “${name}”. Who do you mean?"), { name });
        }
        return this._dlg.askChoices(question, choices);
    }
    async tryConfigureDevice(kind) {
        const factories = await this._thingpedia.getDeviceSetup([kind]);
        const factory = factories[kind];
        if (!factory) {
            await this._dlg.replyInterp(this._("You need to enable ${device} before you can use that command."), {
                device: misc_utils_1.cleanKind(kind)
            });
            await this._dlg.replyLink(this._dlg.interpolate(this._("Configure ${device}"), {
                device: misc_utils_1.cleanKind(kind)
            }), "/devices/create");
            return null;
        }
        if (factory.type === 'none') {
            const device = await this._engine.createDevice({ kind: factory.kind });
            return this._engine.getDeviceInfo(device.uniqueId);
        }
        else {
            if (this._dlg.isAnonymous) {
                await this._dlg.replyInterp(this._("Sorry, to use ${device}, you must log in to your personal account."), {
                    device: factory.text,
                });
                await this._dlg.replyLink(this._("Register for Almond"), "/user/register");
                return null;
            }
            if (factory.type === 'multiple' && factory.choices.length === 0) {
                await this._dlg.replyInterp(this._("You need to enable ${device} before you can use that command."), {
                    device: factory.text
                });
            }
            else if (factory.type === 'multiple') {
                await this._dlg.replyInterp(this._("You do not have a ${device} configured. You will need to enable ${choices:disjunction} before you can use that command."), {
                    device: factory.text,
                    choices: factory.choices.map((f) => f.text)
                });
            }
            else {
                await this._dlg.replyInterp(this._("You need to enable ${device} before you can use that command."), {
                    device: factory.text
                });
            }
            // HACK: home assistant cannot be configured here, override the factory type
            if (factory.type !== 'multiple' && factory.kind === 'io.home-assistant')
                factory.type = 'interactive'; // this code is CHAOTIC EVIL as it exploits the unsoundness of TypeScript :D
            switch (factory.type) {
                case 'oauth2':
                    await this._dlg.replyLink(this._dlg.interpolate(this._("Configure ${device}"), { device: factory.text }), `/devices/oauth2/${factory.kind}?name=${encodeURIComponent(factory.text)}`);
                    break;
                case 'multiple':
                    await this._dlg.replyLink(this._("Configure a new skill"), "/devices/create");
                    break;
                default:
                    await this._dlg.replyLink(this._dlg.interpolate(this._("Configure ${device}"), { device: factory.text }), "/devices/create");
            }
            return null;
        }
    }
    async lookupContact(category, name) {
        if (this._dlg.platformData.contacts) {
            for (const platformContact of this._dlg.platformData.contacts) {
                if (platformContact.value === name) {
                    this.debug(`Mapped @${name} to ${platformContact.principal} using platform data`);
                    return [{
                            value: platformContact.principal,
                            displayName: platformContact.display
                        }];
                }
            }
        }
        const contactApi = this._platform.getCapability('contacts');
        if (contactApi === null)
            return [];
        let what;
        if (category === value_category_1.default.PhoneNumber)
            what = 'phone_number';
        else if (category === value_category_1.default.EmailAddress)
            what = 'email_address';
        else
            what = 'contact';
        return contactApi.lookup(what, name);
    }
    async askMissingContact(category, name) {
        await this._dlg.replyInterp(this._("No contact matches “${name}”."), { name });
        // straight up ask for the target category
        // this ensures we show a contact picker, which is better than
        // repeatedly asking the user
        const value = await this._dlg.ask(category === value_category_1.default.Contact ? value_category_1.default.PhoneNumber : category, this._("Who do you want to contact?"));
        assert_1.default(value instanceof thingtalk_1.Ast.EntityValue);
        return value;
    }
    async addDisplayToContact(contact) {
        const principal = contact.value;
        if (!principal)
            return;
        if (this._dlg.platformData.contacts) {
            for (const platformContact of this._dlg.platformData.contacts) {
                if (platformContact.principal === principal) {
                    contact.display = platformContact.display;
                    return;
                }
            }
        }
        const contactApi = this._platform.getCapability('contacts');
        if (contactApi === null)
            return;
        const addressBookContact = await contactApi.lookupPrincipal(principal);
        if (addressBookContact)
            contact.display = addressBookContact.displayName;
    }
    async _constructEntityQuery(kind, query, entityDisplay) {
        const schema = await this._schemas.getSchemaAndNames(kind, 'query', query);
        const filter = new thingtalk_1.Ast.BooleanExpression.Atom(null, 'id', '=~', new thingtalk_1.Ast.Value.String(entityDisplay));
        const invocation = (new thingtalk_1.Ast.Invocation(null, new thingtalk_1.Ast.DeviceSelector(null, kind, null, null), query, [], schema));
        const invocationTable = new thingtalk_1.Ast.InvocationExpression(null, invocation, schema);
        const filteredTable = new thingtalk_1.Ast.FilterExpression(null, invocationTable, filter, schema);
        return new thingtalk_1.Ast.ExpressionStatement(null, filteredTable);
    }
    async lookupEntityCandidates(entityType, entityDisplay, hints) {
        // HACK this should be made generic with some new Genie annotation
        if (entityType === 'org.freedesktop:app_id') {
            const appLauncher = this._platform.getCapability('app-launcher');
            if (appLauncher)
                return appLauncher.listApps();
        }
        const { data: tpCandidates, meta } = await this._thingpedia.lookupEntity(entityType, entityDisplay);
        if (tpCandidates.length > 0)
            return tpCandidates;
        let stmt;
        try {
            const kind = entityType.split(":")[0];
            const query = entityType.split(":")[1];
            stmt = await this._constructEntityQuery(kind, query, entityDisplay);
        }
        catch (e) {
            // ignore an error here (it indicates the query is not an ID query)
        }
        let candidates = tpCandidates;
        if (stmt) {
            await this._prepareForExecution(stmt, hints);
            const [results,] = await this._executor.executeStatement(stmt);
            candidates = [];
            for (const item of results.results) {
                const id = item.value.id;
                if (!id || !(id instanceof thingtalk_1.Ast.EntityValue))
                    continue;
                const entity = {
                    type: entityType,
                    value: id.value,
                    canonical: id.display.toLowerCase(),
                    name: id.display
                };
                candidates.push(entity);
            }
        }
        if (candidates.length === 0) {
            await this._dlg.replyInterp(this._("Sorry, I cannot find any ${entity_type} matching “${name}”."), {
                entity_type: meta.name,
                name: entityDisplay
            });
            throw new errors_1.CancellationError();
        }
        return candidates;
    }
    async _tryGetCurrentLocation() {
        const gps = this._platform.getCapability('gps');
        if (gps === null)
            return null;
        const location = await gps.getCurrentLocation();
        if (location === null) {
            this.debug('GPS location not available');
            return null;
        }
        else {
            return new thingtalk_1.Ast.Location.Absolute(location.latitude, location.longitude, location.display || null);
        }
    }
    async lookupLocation(searchKey, previousLocations) {
        const currentLocation = await this._tryGetCurrentLocation();
        const lastLocation = previousLocations.length ? previousLocations[previousLocations.length - 1] : undefined;
        let around;
        if (lastLocation)
            around = { latitude: lastLocation.lat, longitude: lastLocation.lon };
        else if (currentLocation)
            around = { latitude: currentLocation.lat, longitude: currentLocation.lon };
        const candidates = await this._thingpedia.lookupLocation(searchKey, around);
        // ignore locations larger than a city
        const mapped = candidates.filter((c) => c.rank >= 16).map((c) => {
            return new thingtalk_1.Ast.Location.Absolute(c.latitude, c.longitude, c.display);
        });
        if (mapped.length === 0) {
            const question = this._dlg.interpolate(this._("Sorry, I cannot find any location matching “${location}”. What location are you looking for?"), {
                location: searchKey,
            });
            const answer = await this._dlg.ask(value_category_1.default.Location, question);
            assert_1.default(answer instanceof thingtalk_1.Ast.LocationValue);
            if (answer.value instanceof thingtalk_1.Ast.UnresolvedLocation) {
                return this.lookupLocation(answer.value.name, previousLocations);
            }
            else if (answer.value instanceof thingtalk_1.Ast.RelativeLocation) {
                const resolved = await this.resolveUserContext('$context.location.' + answer.value.relativeTag);
                assert_1.default(resolved instanceof thingtalk_1.Ast.LocationValue);
                return resolved;
            }
            else {
                return answer;
            }
        }
        return new thingtalk_1.Ast.Value.Location(mapped[0]);
    }
    _tryGetStoredVariable(type, variable) {
        const sharedPrefs = this._platform.getSharedPreferences();
        const value = sharedPrefs.get('context-' + variable);
        if (value === undefined)
            return null;
        return thingtalk_1.Ast.Value.fromJSON(type, value);
    }
    async resolveUserContext(variable) {
        let value = null;
        switch (variable) {
            case '$context.location.current_location': {
                const location = await this._tryGetCurrentLocation();
                if (location)
                    value = new thingtalk_1.Ast.Value.Location(location);
                else
                    value = this._tryGetStoredVariable(thingtalk_1.Type.Location, variable);
                break;
            }
            case '$context.location.home':
            case '$context.location.work':
                value = this._tryGetStoredVariable(thingtalk_1.Type.Location, variable);
                break;
            case '$context.time.morning':
            case '$context.time.evening':
                value = this._tryGetStoredVariable(thingtalk_1.Type.Time, variable);
                break;
            default:
                throw new TypeError('Invalid variable ' + variable);
        }
        if (value !== null)
            return value;
        let question, type;
        switch (variable) {
            case '$context.location.current_location':
                question = this._("Where are you now?");
                type = value_category_1.default.Location;
                break;
            case '$context.location.home':
                question = this._("What is your home address?");
                type = value_category_1.default.Location;
                break;
            case '$context.location.work':
                question = this._("What is your work address?");
                type = value_category_1.default.Location;
                break;
            case '$context.time.morning':
                question = this._("What time does your morning begin?");
                type = value_category_1.default.Time;
                break;
            case '$context.time.evening':
                question = this._("What time does your evening begin?");
                type = value_category_1.default.Time;
                break;
        }
        let answer = await this._dlg.ask(type, question);
        if (type === value_category_1.default.Location) {
            assert_1.default(answer instanceof thingtalk_1.Ast.LocationValue);
            if (answer.value instanceof thingtalk_1.Ast.RelativeLocation)
                answer = await this.resolveUserContext('$context.location.' + answer.value.relativeTag);
            else if (answer.value instanceof thingtalk_1.Ast.UnresolvedLocation)
                answer = await this.lookupLocation(answer.value.name, []);
        }
        const sharedPrefs = this._platform.getSharedPreferences();
        sharedPrefs.set('context-' + variable, answer.toJS());
        return answer;
    }
    getPreferredUnit(type) {
        const pref = this._platform.getSharedPreferences();
        return pref.get('preferred-' + type);
    }
}
exports.default = ExecutionDialogueAgent;
//# sourceMappingURL=execution_dialogue_agent.js.map