"use strict";
// -*- mode: typescript; indent-tabs-mode: nil; js-basic-offset: 4 -*-
//
// This file is part of Genie
//
// Copyright 2020 The Board of Trustees of the Leland Stanford Junior University
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: Giovanni Campagna <gcampagn@cs.stanford.edu>
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const assert_1 = __importDefault(require("assert"));
const ThingTalk = __importStar(require("thingtalk"));
const Utils = __importStar(require("../utils/misc-utils"));
const I18n = __importStar(require("../i18n"));
const edit_distance_1 = __importDefault(require("../utils/edit-distance"));
const entity_utils_1 = require("../utils/entity-utils");
const ThingTalkUtils = __importStar(require("../utils/thingtalk"));
const predictor_1 = __importDefault(require("./predictor"));
const exactbuilder_1 = __importDefault(require("./exactbuilder"));
const SEMANTIC_PARSING_TASK = 'almond';
const NLU_TASK = 'almond_dialogue_nlu';
const NLG_TASK = 'almond_dialogue_nlg';
const NLG_QUESTION = 'what should the agent say ?';
function compareScore(a, b) {
    if (a.score === b.score)
        return 0;
    if (a.score === 'Infinity')
        return -1;
    if (b.score === 'Infinity')
        return 1;
    return b.score - a.score;
}
class LocalParserClient {
    constructor(modeldir, locale, platform, exactmatcher, tpClient = null, options = {}) {
        this._locale = locale;
        this._langPack = I18n.get(locale);
        this._tokenizer = this._langPack.getTokenizer();
        this._predictor = new predictor_1.default(modeldir, options);
        this._exactmatcher = exactmatcher;
        this._tpClient = tpClient;
    }
    async start() {
        await Promise.all([
            this._predictor.start(),
            this._startExactMatcher()
        ]);
    }
    async stop() {
        await this._predictor.stop();
    }
    async _startExactMatcher() {
        if (this._exactmatcher)
            return;
        if (!this._platform || !this._tpClient)
            return;
        const prefs = this._platform.getSharedPreferences();
        const developerDir = prefs.get('developer-dir');
        if (!developerDir)
            return;
        const builder = new exactbuilder_1.default({
            locale: this._locale,
            timezone: this._platform.timezone,
            cachedir: this._platform.getCacheDir(),
            developerdir: Array.isArray(developerDir) ? developerDir : [developerDir],
            thingpediaClient: this._tpClient,
        });
        this._exactmatcher = await builder.load();
    }
    _applyPreHeuristics(contextCode) {
        // remove "attribute:id = GENERIC_ENTITY_tt:device_id_*" from the context because
        // we never generate that in training
        const newCode = [];
        let inString = false;
        for (let i = 0; i < contextCode.length; i++) {
            const token = contextCode[i];
            if (token === '"')
                inString = !inString;
            if (inString) {
                newCode.push(token);
                continue;
            }
            if (token === 'attribute:id') {
                i += 2; // skip "attribute:id" and "=", the loop increment will skip GENERIC_ENTITY
                continue;
            }
            newCode.push(token);
        }
        return newCode;
    }
    _applyPostHeuristics(programs, contextCode) {
        // only work on contextual
        if (!contextCode)
            return;
        if (contextCode[0] === '$dialogue' &&
            contextCode[1] === '@org.thingpedia.dialogue.transaction' &&
            contextCode[3] === 'sys_anything_else') {
            for (const prog of programs) {
                if (prog.code[0] === '$dialogue' &&
                    prog.code[1] === '@org.thingpedia.dialogue.transaction' &&
                    prog.code[3] === 'cancel')
                    prog.code[3] = 'end';
            }
        }
    }
    async sendUtterance(utterance, contextCode, contextEntities, options = {}) {
        let tokens, entities;
        if (options.tokenized) {
            tokens = utterance.split(' ');
            entities = Utils.makeDummyEntities(utterance);
            if (contextEntities) {
                // safety against weird properties
                for (const key of Object.getOwnPropertyNames(contextEntities)) {
                    if (/^(.+)_([0-9]+)$/.test(key))
                        entities[key] = contextEntities[key];
                }
            }
        }
        else {
            const tokenized = await this._tokenizer.tokenize(utterance);
            if (contextEntities)
                entity_utils_1.renumberEntities(tokenized, contextEntities);
            tokens = tokenized.tokens;
            entities = tokenized.entities;
        }
        const answer = options.answer;
        let result = null;
        let exact = null;
        const intent = {
            command: 1,
            other: 0,
            ignore: 0
        };
        if (tokens.length === 0) {
            result = [{
                    code: ['$failed', ';'],
                    score: 'Infinity'
                }];
        }
        else if (tokens.length === 1 && (/^[A-Z]/.test(tokens[0]) || tokens[0] === '1' || tokens[0] === '0')) {
            // if the whole input is just an entity, return that as an answer
            result = [{
                    code: ['$answer', '(', tokens[0], ')', ';'],
                    score: 'Infinity'
                }];
        }
        else if (options.expect === 'MultipleChoice') {
            const choices = await Promise.all((options.choices || []).map((choice) => this._tokenizer.tokenize(choice)));
            result = choices.map((choice, i) => {
                return {
                    code: ['$choice', '(', String(i), ')', ';'],
                    score: -edit_distance_1.default(tokens, choice.tokens)
                };
            });
            result.sort(compareScore);
        }
        else {
            if (this._exactmatcher)
                exact = this._exactmatcher.get(tokens);
        }
        if (result === null) {
            if (contextCode)
                contextCode = this._applyPreHeuristics(contextCode);
            let candidates;
            if (contextCode)
                candidates = await this._predictor.predict(contextCode.join(' '), tokens.join(' '), answer, NLU_TASK, options.example_id);
            else
                candidates = await this._predictor.predict(tokens.join(' '), undefined, answer, SEMANTIC_PARSING_TASK, options.example_id);
            assert_1.default(candidates.length > 0);
            result = candidates.map((c) => {
                var _a, _b;
                // convert is_correct and is_probably_correct scores into
                // a single scale such that >0.5 is correct and >0.25 is
                // probably correct
                const score = ((_a = c.score.is_correct) !== null && _a !== void 0 ? _a : 1) >= 0.5 ? 1 :
                    ((_b = c.score.is_probably_correct) !== null && _b !== void 0 ? _b : 1) >= 0.5 ? 0.35 : 0.15;
                return {
                    code: c.answer.split(' '),
                    score: score
                };
            });
            if (candidates[0].score.is_junk >= 0.5)
                intent.ignore = 1;
            else
                intent.ignore = 0;
            if (intent.ignore < 0.5 && candidates[0].score.is_ood >= 0.5)
                intent.other = 1;
            else
                intent.other = 0;
            intent.command = 1 - intent.ignore - intent.other;
        }
        let result2 = result; // guaranteed not null
        if (exact !== null)
            result2 = exact.map((code) => ({ code, score: 'Infinity' })).concat(result2);
        this._applyPostHeuristics(result2, contextCode);
        if (!options.skip_typechecking) {
            const schemas = new ThingTalk.SchemaRetriever(this._tpClient, null, true);
            result2 = (await Promise.all(result2.map(async (c) => {
                const parsed = await ThingTalkUtils.parsePrediction(c.code, entities, {
                    thingpediaClient: this._tpClient,
                    schemaRetriever: schemas
                });
                if (parsed) {
                    return {
                        code: ThingTalkUtils.serializePrediction(parsed, tokens, entities, {
                            locale: this._locale,
                            compatibility: options.thingtalk_version,
                            ignoreSentence: true
                        }),
                        score: c.score
                    };
                }
                else {
                    return null;
                }
            }))).filter((c) => c !== null);
        }
        return {
            result: 'ok',
            tokens: tokens,
            candidates: result2,
            entities: entities,
            intent
        };
    }
    async generateUtterance(contextCode, contextEntities, targetAct) {
        const candidates = await this._predictor.predict(contextCode.join(' ') + ' ' + targetAct.join(' '), NLG_QUESTION, undefined, NLG_TASK);
        return candidates.map((cand) => {
            var _a;
            return {
                answer: cand.answer,
                score: (_a = cand.score.confidence) !== null && _a !== void 0 ? _a : 1
            };
        });
    }
}
exports.default = LocalParserClient;
//# sourceMappingURL=localparserclient.js.map