<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

namespace local_deepler\local\services;
defined('MOODLE_INTERNAL') || die();

use context_user;
use DeepL\AppInfo;
use DeepL\AuthorizationException;
use DeepL\DeepLClient;
use DeepL\DeepLException;
use DeepL\Language;
use DeepL\LanguageCode;
use Deepl\Usage;
use local_deepler\local\data\glossary;
use local_deepler\local\data\user_glossary;
use stdClass;

require_once(__DIR__ . '/../../vendor/autoload.php');

/**
 * Helper class to connect to Deepl's API, fetch the available langs etc.
 * as well as prepare the data for the html selects and AMD.
 *
 * @package local_deepler
 * @copyright  2024 Bruno Baudry <bruno.baudry@bfh.ch>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class lang_helper {
    /**
     * Constant to display the lang as rephrasing.
     */
    const REPHRASESYMBOL = "® ";
    /**
     * The current moodle language.
     *
     * @var string
     */
    public string $currentlang;
    /**
     * The target language.
     *
     * @var string
     */
    public string $targetlang;
    /**
     * @var array|mixed Moodle instance's installed languages.
     */
    public mixed $moodlelangs;

    /**
     * Constructor.
     *
     * @param \DeepL\DeepLClient|null $translator
     * @param string|null $apikey
     * @param array|null $moodlelangs
     * @param string|null $currentlang
     * @param string|null $targetlang
     * @throws \DeepL\DeepLException
     * @throws \coding_exception
     * @throws \dml_exception
     */
    public function __construct(
        ?DeepLClient $translator = null,
        ?string $apikey = null,
        ?array $moodlelangs = null,
        ?string $currentlang = null,
        ?string $targetlang = null
    ) {
        $this->deeplsources = [];
        $this->deepltargets = [];
        $this->deeplrephraselangs = ['de', 'en-GB', 'en-US', 'es', 'fr', 'it', 'pt-BR', 'pt-PT'];
        $this->canimprove = false;
        $this->allowbeta = get_config('local_deepler', 'allowbeta') ?? false;
        // Remove this when DeepL improves its API language list.
        $this->setbetas();
        $this->currentlang = $currentlang ?? optional_param('lang', current_language(), PARAM_NOTAGS);
        $this->targetlang = $targetlang ?? optional_param('target_lang', '', PARAM_NOTAGS);
        if ($this->targetlang !== '') {
            $this->targetlang = LanguageCode::standardizeLanguageCode($this->targetlang);
        }
        $this->moodlelangs = $moodlelangs ?? get_string_manager()->get_list_of_translations();
        $this->deeplsourcelang = '';
        $this->apikey = $apikey ?? $this->initapikey();
        $this->dbtokenid = 0;
        $this->translator = $translator;
    }

    /**
     * Remove this when DeepL improves its API language list.
     *
     * @return void
     */
    private function setbetas() {
        $this->betalanguages = [
            new Language('Acehnese', 'ACE', null),
            new Language('Afrikaans', 'AF', null),
            new Language('Aragonese', 'AN', null),
            new Language('Assamese', 'AS', null),
            new Language('Aymara', 'AY', null),
            new Language('Azerbaijani', 'AZ', null),
            new Language('Bashkir', 'BA', null),
            new Language('Belarusian', 'BE', null),
            new Language('Bhojpuri', 'BHO', null),
            new Language('Bengali', 'BN', null),
            new Language('Breton', 'BR', null),
            new Language('Bosnian', 'BS', null),
            new Language('Catalan', 'CA', null),
            new Language('Cebuano', 'CEB', null),
            new Language('Kurdish (Sorani)', 'CKB', null),
            new Language('Welsh', 'CY', null),
            new Language('Esperanto', 'EO', null),
            new Language('Basque', 'EU', null),
            new Language('Persian', 'FA', null),
            new Language('Irish', 'GA', null),
            new Language('Galician', 'GL', null),
            new Language('Guarani', 'GN', null),
            new Language('Konkani', 'GOM', null),
            new Language('Gujarati', 'GU', null),
            new Language('Hausa', 'HA', null),
            new Language('Hindi', 'HI', null),
            new Language('Croatian', 'HR', null),
            new Language('Haitian Creole', 'HT', null),
            new Language('Armenian', 'HY', null),
            new Language('Igbo', 'IG', null),
            new Language('Icelandic', 'IS', null),
            new Language('Javanese', 'JV', null),
            new Language('Georgian', 'KA', null),
            new Language('Kazakh', 'KK', null),
            new Language('Kurdish (Kurmanji)', 'KMR', null),
            new Language('Kyrgyz', 'KY', null),
            new Language('Latin', 'LA', null),
            new Language('Luxembourgish', 'LB', null),
            new Language('Lombard', 'LMO', null),
            new Language('Lingala', 'LN', null),
            new Language('Maithili', 'MAI', null),
            new Language('Malagasy', 'MG', null),
            new Language('Maori', 'MI', null),
            new Language('Macedonian', 'MK', null),
            new Language('Malayalam', 'ML', null),
            new Language('Mongolian', 'MN', null),
            new Language('Marathi', 'MR', null),
            new Language('Malay', 'MS', null),
            new Language('Maltese', 'MT', null),
            new Language('Burmese', 'MY', null),
            new Language('Nepali', 'NE', null),
            new Language('Occitan', 'OC', null),
            new Language('Oromo', 'OM', null),
            new Language('Punjabi', 'PA', null),
            new Language('Pangasinan', 'PAG', null),
            new Language('Kapampangan', 'PAM', null),
            new Language('Dari', 'PRS', null),
            new Language('Pashto', 'PS', null),
            new Language('Quechua', 'QU', null),
            new Language('Sanskrit', 'SA', null),
            new Language('Sicilian', 'SCN', null),
            new Language('Albanian', 'SQ', null),
            new Language('Serbian', 'SR', null),
            new Language('Sesotho', 'ST', null),
            new Language('Sundanese', 'SU', null),
            new Language('Swahili', 'SW', null),
            new Language('Tamil', 'TA', null),
            new Language('Telugu', 'TE', null),
            new Language('Tajik', 'TG', null),
            new Language('Turkmen', 'TK', null),
            new Language('Tagalog', 'TL', null),
            new Language('Tswana', 'TN', null),
            new Language('Tsonga', 'TS', null),
            new Language('Tatar', 'TT', null),
            new Language('Urdu', 'UR', null),
            new Language('Uzbek', 'UZ', null),
            new Language('Wolof', 'WO', null),
            new Language('Xhosa', 'XH', null),
            new Language('Yiddish', 'YI', null),
            new Language('Cantonese', 'YUE', null),
            new Language('Zulu', 'ZU', null),
        ];
    }

    /**
     * Init global API key.
     *
     * @return string
     * @throws \dml_exception
     */
    private function initapikey(): string {
        $key = '';
        if (getenv('DEEPL_API_TOKEN')) {
            $key = getenv('DEEPL_API_TOKEN');
        } else if (get_config('local_deepler', 'apikey')) {
            $key = get_config('local_deepler', 'apikey');
        }
        return $key;
    }

    /**
     * Initialise the Deepl object.
     *
     * @param \stdClass $user
     * @param string $version
     * @return bool
     * @throws \DeepL\DeepLException
     * @throws \dml_exception
     */
    public function initdeepl(stdClass $user, string $version): bool {
        $this->user = $user;
        if (!$this->translator) {
            $this->setdeeplapi();
            $this->inittranslator($version);
        }

        try {
            $this->keyisfree = DeepLClient::isAuthKeyFreeAccount($this->apikey);
            $this->usage = $this->translator->getUsage();
            $this->canimprove = !$this->keyisfree;
            $this->deeplsources = $this->translator->getSourceLanguages();
            $this->deepltargets = $this->translator->getTargetLanguages();
            if ($this->allowbeta) {
                $this->deeplsources = array_merge($this->deeplsources, $this->betalanguages);
                $this->deepltargets = array_merge($this->deepltargets, $this->betalanguages);
            }
            $this->setcurrentlanguage();
            return true;
        } catch (DeepLException $e) {
            return false;
        }
    }

    /**
     * Set the key string.
     * If empty, it will try to get it from the .env useful for tests runs.
     *
     * @return void
     * @throws \dml_exception|\coding_exception
     */
    private function setdeeplapi(): void {
        global $DB;
        $tokens = $DB->get_records('local_deepler_tokens', null, 'id ASC');

        if (empty($tokens)) {
            // If no token mapping then allow nothing to do, we use the main key.
            return;
        }
        $tokenrecord = $this->find_first_matching_token($this->user, $tokens);
        if ($tokenrecord) {
            $this->apikey = $tokenrecord->token;
            $this->dbtokenid = $tokenrecord->id;
        } else if (!get_config('local_deepler', 'allowfallbackkey')) {
            // If no fallback key is allowed, then we can't use the api as no token mapping found.
            $this->apikey = '';
        }
    }

    /**
     * Finds the first available token for a user by looping through all tokens
     * and matching both standard and custom profile fields.
     *
     * @param \core_user|stdClass $user The Moodle user object.
     * @param array $tokens Array of token records to search through.
     * @return stdClass|false The first matching token record, or false if none found.
     * @throws \dml_exception|\coding_exception
     */
    private function find_first_matching_token(\core_user|stdClass $user, array $tokens): false|stdClass {
        global $DB;
        $foundtoken = false;
        $alluserfields = array_keys(utils::all_user_fields(context_user::instance($this->user->id, MUST_EXIST)));

        // Build a map of custom profile fields for DB fallback.
        $customfields = [];
        foreach ($DB->get_records('user_info_field') as $field) {
            $customfields['profile_field_' . $field->shortname] = $field;
        }

        foreach ($tokens as $token) {
            $attr = $token->attribute;
            $pattern = (string) $token->valuefilter;

            // Check if the attribute is a user field.
            if (in_array($attr, $alluserfields)) {
                // If the user object has the property, compare directly.
                if (property_exists($user, $attr)) {
                    $uservalue = (string) $user->$attr;
                    if (
                        ($pattern === $uservalue) ||
                        (strpos($pattern, '%') !== false) ||
                        (strpos($pattern, '*') !== false) ||
                        (strpos($pattern, '_') !== false)
                    ) {
                        if (utils::wildcard_match($pattern, $uservalue)) {
                            $foundtoken = $token;
                        }
                    } else if ($pattern === $uservalue) {
                        $foundtoken = $token;
                    }
                } else if (array_key_exists($attr, $customfields) && !empty($user->id)) {
                    // If not, and it's a custom profile field, fetch from DB.
                    $profiledata = $DB->get_record('user_info_data', [
                        'userid' => $user->id,
                        'fieldid' => $customfields[$attr]->id,
                    ]);
                    if ($profiledata) {
                        $uservalue = (string) $profiledata->data;
                        if (
                            ($pattern === $uservalue) ||
                            (strpos($pattern, '%') !== false) ||
                            (strpos($pattern, '*') !== false) ||
                            (strpos($pattern, '_') !== false)
                        ) {
                            if (utils::wildcard_match($pattern, $uservalue)) {
                                $foundtoken = $token;
                            }
                        } else if ($pattern === $uservalue) {
                            $foundtoken = $token;
                        }
                    }
                }
            }
        }
        return $foundtoken; // No matching token found.
    }

    /**
     * Initialise the Deepl object.
     * Return a Boolean of the cnx status.
     *
     * @param string $version
     * @return bool
     * @throws \DeepL\DeepLException
     */
    private function inittranslator(string $version): bool {
        if (!isset($this->translator)) {
            try {
                $this->translator = new DeepLClient($this->apikey, [
                    'send_platform_info' => true,
                    'app_info' => new AppInfo('Moodle-Deepler', $version),
                ]);
            } catch (AuthorizationException $e) {
                return false;
            }
        }
        return true;
    }

    /**
     * Getter for Usage.
     *
     * @return \Deepl\Usage
     */
    public function getusage(): Usage {
        return $this->usage;
    }

    /**
     * Getter for source langs.
     *
     * @return array|\DeepL\Language[]
     */
    public function getsourcelanguages(): array {
        return $this->deeplsources;
    }

    /**
     * Set the source language.
     *
     * @return void
     * @throws \DeepL\DeepLException
     */
    private function setcurrentlanguage(): void {
        // Moodle format is not the common culture format.
        // Deepl's sources are ISO 639-1 (Alpha 2) and uppercase.
        $this->deeplsourcelang = LanguageCode::removeRegionalVariant(str_replace('_', '-', $this->currentlang));
    }

    /**
     * Injects lang attributes to the config object.
     *
     * @param \stdClass $config
     * @return \stdClass
     */
    public function prepareconfig(stdClass &$config): stdClass {
        $config->usage = $this->usage;
        $config->limitReached = $config->usage->anyLimitReached();
        $config->targetlang = $this->targetlang;
        $config->currentlang = $this->currentlang;
        $config->deeplsourcelang = $this->deeplsourcelang;
        $config->isfree = $this->keyisfree;
        $config->rephrasesymbol = self::REPHRASESYMBOL;
        $config->canimprove = $this->canimprove;
        return $config;
    }

    /**
     * Prepare the strings for the UI as JSON.
     *
     * @return string
     * @throws \coding_exception
     */
    public function preparestrings(): string {
        // Status strings for UI icons.
        $config = new stdClass();
        $config->statusstrings = new stdClass();
        $config->statusstrings->failed = get_string('statusfailed', 'local_deepler');
        $config->statusstrings->success = get_string('statussuccess', 'local_deepler');
        $config->statusstrings->tosave = get_string('statustosave', 'local_deepler');
        $config->statusstrings->totranslate = get_string('statustotranslate', 'local_deepler');
        $config->statusstrings->wait = get_string('statuswait', 'local_deepler');
        // General UI strings.
        $config->uistrings = new stdClass();
        $config->uistrings->deeplapiexception = get_string('deeplapiexception', 'local_deepler');
        $config->uistrings->errordbpartial = get_string('errordbpartial', 'local_deepler');
        $config->uistrings->errordbtitle = get_string('errordbtitle', 'local_deepler');
        $config->uistrings->errortoolong = get_string('errortoolong', 'local_deepler');
        $config->uistrings->saveallmodaltitle = get_string('saveallmodaltitle', 'local_deepler');
        $config->uistrings->translatemodaltitle = get_string('translate:modal:title', 'local_deepler');
        $config->uistrings->translatemodalbody = get_string('translate:modal:body', 'local_deepler');
        $config->uistrings->saveallmodalbody = get_string('saveallmodalbody', 'local_deepler');
        $config->uistrings->canttranslatesame = get_string('canttranslatesame', 'local_deepler');
        return json_encode($config);
    }

    /**
     * Checks if source language is supported.
     *
     * @return bool
     */
    public function iscurrentsupported(): bool {
        return $this->islangsupported($this->deeplsourcelang);
    }

    /**
     * Checks if a given lang is supported by Deepl
     *
     * @param string $lang
     * @return bool
     * @throws \DeepL\DeepLException
     */
    public function islangsupported(string $lang): bool {
        $list = $this->deeplsources;
        $len = count($list);
        while ($len--) {
            $code = LanguageCode::standardizeLanguageCode($list[$len]->code);
            if ($code === $lang || $code === strtolower($lang)) {
                return true;
            }
        }
        return false;
    }

    /**
     * If key empty string.
     *
     * @return bool
     */
    public function isapikeynoset(): bool {
        return $this->apikey === '' || $this->apikey === null || $this->apikey === 'DEFAULT';
    }

    /**
     * Getter for deeplsourcelang.
     *
     * @return string
     */
    public function get_deeplsourcelang(): string {
        return $this->deeplsourcelang;
    }

    /**
     * Prepare source options.
     *
     * @return array
     */
    public function preparesourcesoptionlangs(): array {
        return $this->prepareoptionlangs($this->finddeeplsformoodle($this->deeplsources), true, false);
    }

    /**
     * Creates props for html selects.
     *
     * @param array $filtereddeepls
     * @param bool $issource
     * @param bool $verbose
     * @return array
     */
    private function prepareoptionlangs(array $filtereddeepls, bool $issource = true, bool $verbose = true): array {
        $tab = [];
        // Get the list of deepl langs that are supported by this moodle instance.
        foreach ($filtereddeepls as $l) {
            $tab[] = $this->getoption($issource, $l, $verbose);
        }
        return $tab;
    }

    /**
     * Sub function to list the options.
     *
     * @param bool $issource
     * @param mixed $l
     * @param bool $isverbose
     * @return array
     */
    private function getoption(bool $issource, mixed $l, bool $isverbose = true): array {
        // If the key is free, we can't improve the source lang.
        $code = LanguageCode::standardizeLanguageCode($l->code);
        $same = $issource ? $this->isrephrase($code, '') : $this->isrephrase('', $code);
        $text = $isverbose ? $l->name : $code;
        $langisrephrasable = in_array($code, $this->deeplrephraselangs, true);

        if ($issource) {
            $selected = $this->isrephrase($code, $this->deeplsourcelang);
            $disable = !$selected && ($same && !$this->canimprove || $same && !$langisrephrasable);
        } else {
            $selected = $this->targetlang !== '' && $this->isrephrase($code, $this->targetlang);
            $disable = ($same && !$langisrephrasable) || ($same && !$this->canimprove);
        }
        if ($same && $this->canimprove) {
            $text = self::REPHRASESYMBOL . $text;
            $code = self::REPHRASESYMBOL . $code;
        }
        return [
            'code' => $code,
            'lang' => $text,
            'verbose' => $l->name,
            'selected' => $selected,
            'disabled' => $disable,
        ];
    }

    /**
     * Check if source is same as target. Might call the rephrase instead.
     *
     * @param string $source
     * @param string $target
     * @return bool
     */
    public function isrephrase(string $source = '', string $target = ''): bool {
        $s = $source === '' ? $this->deeplsourcelang : $source;
        $t = $target === '' ? $this->targetlang : $target;
        return str_contains($t, $s);
    }

    /**
     * Find the Deepl langs that are supported by this moodle instance.
     *
     * @param array $deepls
     * @return array
     */
    private function finddeeplsformoodle(array $deepls): array {
        return array_filter($deepls, function ($item) {
            foreach (array_keys($this->moodlelangs) as $moodlecode) {
                $moodle = strtolower(str_replace('_', '-', $moodlecode));
                $deepl = strtolower($item->code);
                if (stripos($deepl, $moodle) !== false) {
                    return true;
                }
            }
            return false;
        });
    }

    /**
     *  Prepare target options.
     *
     * @return array
     */
    public function preparetargetsoptionlangs(): array {
        return $this->prepareoptionlangs($this->finddeeplsformoodle($this->deepltargets), false);
    }

    /**
     * Getter for canimprove.
     *
     * @return bool
     */
    public function get_canimprove(): bool {
        return $this->canimprove;
    }

    /**
     * Getter for ability  to use the improve API.
     *
     * @return bool
     */
    public function getcanimprove(): bool {
        return $this->canimprove;
    }

    /**
     * Getter for deeplrephraselangs.
     *
     * @return array
     */
    public function get_deeplrephraselangs(): array {
        return $this->deeplrephraselangs;
    }

    /**
     * Lists the compatible moodle langs for the current target lang.
     *
     * @return array
     * @throws \DeepL\DeepLException
     */
    public function findcompatiblelangs(): array {
        if ($this->targetlang === '') {
            return [];
        }
        $langroot = LanguageCode::removeRegionalVariant($this->targetlang);

        $compatibles = [];
        foreach (array_keys($this->moodlelangs) as $code) {
            if (str_contains($code, $langroot)) {
                $compatibles[] = $code;
            }
        }
        // Sort the array by the lang code, starting with the simplest one obviously.
        asort($compatibles);
        $tab = [];
        foreach ($compatibles as $item) {
            $tab[] = $item;
        }
        return $tab;
    }

    /**
     * Getter translator.
     *
     * @return \DeepL\DeepLClient|null
     */
    public function gettranslator(): ?DeepLClient {
        return $this->translator;
    }

    /**
     * Getter for main key.
     *
     * @return string
     */
    public function getapikey(): string {
        return $this->apikey;
    }

    /**
     * Getter for target langs.
     *
     * @return array|\DeepL\Language[]
     */
    public function gettargelanguages(): array {
        return $this->deepltargets;
    }

    /**
     * Getter for current lang.
     *
     * @param bool $mainlonly
     * @return string
     */
    public function getcurrentlang(bool $mainlonly = false): string {
        return $mainlonly ? substr($this->currentlang, 0, 2) : $this->currentlang;
    }

    /**
     * Getter for chosen target.
     *
     * @param bool $mainlonly
     * @return string
     */
    public function gettargetlang(bool $mainlonly = false): string {
        return $mainlonly ? substr($this->targetlang, 0, 2) : $this->targetlang;
    }

    /**
     * Adds a DeepL glossary if not yet stored in DB.
     *
     * @param array $deeplglossaries
     * @return void
     * @throws \dml_exception
     */
    public function adddeeplglossariesifunknown(array $deeplglossaries): void {
        /** @var \DeepL\GlossaryInfo $deeplglossary */
        foreach ($deeplglossaries as $deeplglossary) {
            if (!glossary::exists($deeplglossary->glossaryId)) {
                glossary::create(new glossary(
                    $deeplglossary->glossaryId,
                    $deeplglossary->name,
                    $deeplglossary->sourceLang,
                    $deeplglossary->targetLang,
                    $deeplglossary->entryCount
                ));
            }
        }
    }

    /**
     * Return all glossaries for current user.
     *
     * @return array
     * @throws \dml_exception
     */
    public function getusersglossaries(): array {
        $glos = [];
        $pivot = user_glossary::getallbyuser($this->user->id);

        foreach ($pivot as $item) {
            $glos[] = glossary::getbyid($item->glossaryid);
        }
        return $glos;
    }

    /**
     * Get all glossaries uploaded by translators of the same pool (sharing the same api token).
     *
     * @param array|null $except
     * @return array
     * @throws \dml_exception
     */
    public function getpoolglossaries(?array $except = []): array {
        // Build a set of IDs to avoid duplicates.
        $ids = array_map(fn($o) => $o->glossaryid, $except);
        $poolglossaries = glossary::getallbytokenid($this->dbtokenid);
        // Filter glossaries not build by user.
        return array_filter($poolglossaries, fn($glo) => !in_array($glo->glossaryid, $ids));
    }

    /**
     * Get all dictionaries except those bound to an api token.
     *
     * @return array
     * @throws \coding_exception
     * @throws \dml_exception
     */
    public function getpublicglossaries() {
        return glossary::getpublicexcepttokenid($this->dbtokenid);
    }

    /**
     * Get All from DeepL add missing, remove deleted return all.
     *
     *
     * @return glossary[]
     * @throws \DeepL\DeepLException
     * @throws \dml_exception
     */
    public function syncdeeplglossaries(): array {
        $deeplglossaries = $this->getalldeeplglossaries();
        // Array of db IDs and DeepL's Glossary IDs.
        $glossariesallids = glossary::getall_ids();
        // Flatten Glossary IDs in DB.
        $pluginsgloids = array_map(fn($o) => $o->glossaryid, $glossariesallids);
        // Add those added from DeepL UI if missing.
        foreach ($deeplglossaries as $deeplglossary) {
            if (!in_array($deeplglossary->glossaryId, $pluginsgloids)) {
                glossary::create(new glossary(
                    $deeplglossary->glossaryId,
                    $deeplglossary->name,
                    $deeplglossary->sourceLang,
                    $deeplglossary->targetLang,
                    $deeplglossary->entryCount
                ));
            }
        }
        // Flatten DeepL's glo IDs.
        $deeplsids = array_map(fn($o) => $o->glossaryId, $deeplglossaries);
        // Delete those deleted from DeepL's UI.
        $pluginidsotindeepl = array_filter($glossariesallids, function ($obj) use ($deeplsids) {
            if (!in_array($obj->glossaryid, $deeplsids)) {
                return $obj->id;
            }
        });
        $idstodelete = array_map(fn($o) => $o->id, $pluginidsotindeepl);
        foreach ($idstodelete as $deleteme) {
            $this->deleteglossary($deleteme, true);
        }
        return glossary::getall('', '');
    }

    /**
     * Fetches all glossaries.
     *
     * @return array
     * @throws \DeepL\DeepLException
     */
    private function getalldeeplglossaries(): array {
        return $this->translator->listGlossaries();
    }

    /**
     * Delete user's glossary.
     *
     * @param int $glossarydbid
     * @param bool $dbonly
     * @return bool|null
     * @throws \DeepL\DeepLException
     * @throws \dml_exception
     */
    public function deleteglossary(int $glossarydbid, bool $dbonly = false): ?bool {
        $guid = user_glossary::getbyuserandglossary($this->user->id, $glossarydbid);
        $success = $dbonly;
        if ($guid) {
            // Public glossaries downloaded from DeepL do not have users.
            $delete = user_glossary::delete($guid->id);
        }
        $glo = glossary::getbyid($glossarydbid);
        if (!$dbonly) {
            $success = $this->translator->deleteglossary($glo->glossaryid);
        }
        $deleted = glossary::delete($glossarydbid);
        return $success && $deleted;
    }

    /**
     * Getter for the current token id.
     *
     * @return int
     */
    public function getdbtokenid(): int {
        return $this->dbtokenid;
    }

    /**
     * Getter for allow beta.
     *
     * @return mixed
     */
    public function get_allowbeta(): mixed {
        return $this->allowbeta;
    }

    /**
     * Create HTML props for select.
     *
     * @param array $tab
     * @return string
     * TODO MDL-0000 allow regional languages setup (expl EN-GB)
     */
    private function preparehtmlotions(array $tab): string {
        $list = '';
        foreach ($tab as $item) {
            $list .= '<option value="' . $item['code'] . '"';
            if ($item['selected']) {
                $list .= ' selected ';
            }
            if ($item['disabled']) {
                $list .= ' disabled ';
            }
            $list .= ' data-initial-value="' . $item['code'] . '">' . $item['lang'] . '</option>';
        }
        return $list;
    }
    /**
     * Remove this when DeepL improves its API language list.
     *
     * @var array|string[] hardcoded list of beta languages for sources.
     */
    private array $betalanguages = [];
    /**
     * @var false|mixed|object|string|stdClass
     */
    private mixed $allowbeta;
    /**
     * Deepl usage bound to the api key.
     *
     * @var Usage
     */
    protected Usage $usage;
    /**
     * @var string The source language for deepl.
     */
    private string $deeplsourcelang;
    /**
     * @var string
     */
    private string $apikey;
    /** @var int the db id for the API key matching the user */
    private int $dbtokenid;
    /**
     * @var DeepLClient
     */
    private mixed $translator;
    /**
     * Languages available as source in Deepl's API.
     *
     * @var Language[]
     */
    private array $deeplsources;
    /**
     * Languages available as target in Deepl's API.
     *
     * @var Language[]
     */
    private array $deepltargets;
    /**
     * Type of DeepL subscrription.
     *
     * @var bool
     */
    private bool $keyisfree;
    /**
     * @var bool
     */
    private bool $canimprove;
    /**
     * @var array|string[]
     */
    private array $deeplrephraselangs;
    /** @var \stdClass */
    private stdClass $user;
}
