<?php

class PolylangTools
{

    /** @var modX $modx */
    public $modx = null;
    /** @var Polylang $polylang */
    public $polylang = null;
    /** @var PolylangDbHelper $dbHelper */
    public $dbHelper = null;
    /** @var pdoTools $pdoTools */
    public $pdoTools = null;
    /** @var miniShop2 $ms2 */
    public $ms2 = null;
    /** @var string $sessionKey */
    public $sessionKey = 'polylang';
    /** @var array $config */
    public $config = array();
    /** @var bool $debug */
    public $debug = false;
    /** @var array $optionTypes */
    protected $optionTypes = array();
    /** @var PolylangLanguage $language */
    protected $language = null;


    public function __construct(Polylang &$polylang, $config = array())
    {
        $this->polylang = &$polylang;
        $this->modx = &$polylang->modx;
        $this->config = array_merge($this->config, $config);
        $this->debug = $this->modx->getOption('polylang_debug', null, false, true);

    }

    /**
     * @param string $class
     * @param string $name
     * @param array $data
     * @param array $options
     * @return bool
     */
    public function addField($class, $name, array $data, array $options = array())
    {
        $result = false;
        if ($this->hasField($class, $name)) {
            $this->modx->log(modX::LOG_LEVEL_ERROR, "Error add new field '{$name}' in '{$class}'. Field already exists");
            return false;
        }
        $meta = $this->modx->getOption('meta', $data, array(), true);
        if ($meta) {
            $result = $this->getDbHelper()->addField($class, $name, $meta, $options);
            if ($result) {
                /** @var  PolylangField $field */
                $field = $this->modx->newObject('PolylangField');
                $field->fromArray($data);
                $field->set('name', $name);
                $field->set('class_name', $class);
                if (!$result = $field->save()) {
                    $this->getDbHelper()->removeField($class, $name, $options);
                }
            }
        }
        return $result;
    }

    /**
     * @param string $class
     * @param string $name
     * @param array $data
     * @param array $options
     * @return bool
     */
    public function alterField($class, $name, array $data, array $options = array())
    {
        $result = false;
        if ($field = $this->getField($name, $class)) {
            $meta = $this->modx->getOption('meta', $data, array(), true);
            $name = $this->modx->getOption('name', $data, $name);
            if (empty($meta)) {
                $meta = $field->get('meta');
            }
            if ($meta != $field->get('meta')) {
                if (!$this->getDbHelper()->alterField($class, $name, $meta, $options)) {
                    return false;
                }
            }
            if ($name !== $field->get('name')) {
                if (!$this->getDbHelper()->renameField($class, $field->get('name'), $name, $options)) {
                    return false;
                }
            }
            $field->fromArray($data);
            $result = $field->save();
        } else {
            $this->modx->log(modX::LOG_LEVEL_ERROR, "Error alter field. Field '{$name}' not found  in '{$class}'");
        }
        return $result;
    }

    /**
     * @param string $class
     * @param string $name
     * @return bool
     */
    public function removeField($class, $name)
    {
        $result = false;
        if ($field = $this->getField($class, $name)) {
            if ($result = $field->remove()) {
                $result = $this->getDbHelper()->removeField($class, $name);
            }
        } else {
            $this->modx->log(modX::LOG_LEVEL_ERROR, "Error remove field. Field '{$name}' not found  in '{$class}'");
        }
        return $result;
    }

    /**
     * @param string $class
     * @param string $name
     * @return object|null
     */
    public function getField($class, $name)
    {
        return $this->modx->getObject('PolylangField', array('name' => $name, 'class_name' => $class));
    }

    /**
     * @param string $class
     * @param string $name
     * @return bool
     */
    public function hasField($class, $name)
    {
        return $this->modx->getCount('PolylangField', array('name' => $name, 'class_name' => $class)) ? true : false;
    }

    /**
     * @param array $exclude
     * @return array
     */
    public function getContentClasses(array $exclude = array())
    {
        $result = array();
        $list = $this->modx->getOption('polylang_content_classes');
        $list = $this->fromJSON($list, array());
        if ($list) {
            foreach ($list as $key => $target) {
                if (!in_array($key, $exclude)) {
                    $result[strtolower($key)] = $key;
                }
            }
        }
        return $result;
    }

    /**
     * @return array
     */
    public function getContentClassesTarget()
    {
        $list = $this->modx->getOption('polylang_content_classes');
        return $this->fromJSON($list, array());
    }

    /**
     * @param modResource $resource
     * @param string $cultureKey
     * @return array
     */
    public function render(modResource $resource, $cultureKey):array
    {
        $result = [];
        if ($resource) {
            foreach ($this->getContentClassesTarget() as $class => $target) {
                if ($resource instanceof $target) {
                    if ($items = $this->renderFields($resource, $class, $cultureKey)) {
                        $class = strtolower($class);
                        $result[] = array(
                            'title' => $this->modx->lexicon('polylang_content_tab_' . $class),
                            'tabType' => 'fields',
                            'id' => 'polylang-window-polylangcontent-tab-' . $class,
                            'layout' => 'form',
                            'items' => $items,
                            'forceLayout' => true,
                            'deferredRender' => false,
                        );
                    }
                }
            }
            if ($resource instanceof msProduct) {
                $product = $this->modx->call('PolylangProduct', 'getInstance', array(
                    &$this->modx,
                    $resource->get('id'),
                    $cultureKey,
                ));

                if ($product->getOptionKeys()) {
                    $result[] = array(
                        'title' => $this->modx->lexicon('polylang_content_tab_options'),
                        'tabType' => 'options',
                        'items' => array(
                            'xtype' => 'modx-vtabs',
                            'autoTabs' => true,
                            'plain' => true,
                            'forceLayout' => true,
                            'deferredRender' => false,
                            'id' => 'polylang-window-polylangcontent-options-vtabs',
                            'items' => $this->renderOptions($product),
                        ),
                        // 'forceLayout' => true,
                        // 'deferredRender' => false,
                    );
                }
            }
        }
        return $result;
    }

    /**
     * @param PolylangProduct $product
     * @return array
     */
    public function renderOptions(PolylangProduct $product)
    {
        $result = array();
        $categories = array();
        $options = $product->getOptionFields();
        $index = -1;
        foreach ($options as $option) {
            $categoryId = $option['category'];
            $field = $this->prepareOptionField($option);
            if (empty($field)) continue;
            if (!isset($categories[$categoryId])) {
                $categories[$categoryId] = $index++;
                $result[$index] = array(
                    'title' => $option['category_name'] ? $option['category_name'] : $this->modx->lexicon('uncategorized'),
                    'id' => 'polylang-window-polylangcontent-options-tab-' . $categoryId,
                    'layout' => 'form',
                    'labelAlign' => 'top',
                    'category' => $categoryId,
                    'items' => array(),
                    'forceLayout' => true,
                    'deferredRender' => false,
                );
            }
            $result[$index]['items'][] = $field;
        }
        return $result;
    }

    /**
     * @param array $option
     * @return array
     */
    public function prepareOptionField(array $option)
    {
        $field = $option;
        $name = 'polylangproduct_' . $option['key'];
        if (!empty($option['ext_field'])) {
            if (is_string($option['ext_field'])) {
                $json = preg_replace("/'([^']+)'/", '"$1"', $option['ext_field']);
                $json = preg_replace('/(\{|,)\s*([\w_]+)\s*:/', '$1"$2":', $json);
                $json = preg_replace('/new\s+Ext\.data\.SimpleStore\s*\((\{.*\})\)/s', '$1', $json);
                $option['ext_field'] = $this->fromJSON($json);
            }
            $field = $option['ext_field'] ?: [];
        }


        return array_merge($field, array(
            'name' => $name,
            'fieldKey' => $option['key'],
            'key' => "{$option['key']}",
            'source' => "PolylangProductOption",
            'translate' => "{$option['polylang_translate']}",
            'value' => $option['value'] ?? '',
            'fieldLabel' => $option['caption'] ?? $this->modx->lexicon('ms2_product_' . $option['key']),
            'description' => "[[+{$option['key']}]]",
            'enableKeyEvents' => true,
            'category' => $option['category'],
            'category_name' => $option['category_name'],
            'allowBlank' => !$option['required'],
            'anchor' => '100%',
            'msgTarget' => 'under',
        ));
    }

    /**
     * @param modResource $resource
     * @param string $className
     * @param string $cultureKey
     * @return array
     */
    public function renderFields($resource, $className, $cultureKey)
    {
        $fields = array();
        $classKey = 'PolylangField';
        $q = $this->modx->newQuery($classKey);
        $q->select($this->modx->getSelectColumns($classKey, $classKey));
        $q->where(array(
            'active' => 1,
            'class_name' => $className
        ));
        $q->sortby('`rank`', 'ASC');
        if ($q->prepare() && $q->stmt->execute()) {
            while ($data = $q->stmt->fetch(PDO::FETCH_ASSOC)) {
                $fields[] = $this->renderField($resource, $className, $data, $cultureKey);
            }
        }
        return $fields;
    }

    /**
     * @param modResource $resource
     * @param string $className
     * @param array $data
     * @param string $cultureKey
     * @return array
     */
    public function renderField($resource, $className, array $data, $cultureKey)
    {
        $name = strtolower($className);
        $xtype = $data['xtype'];

        $editorHeight = $this->modx->getOption('polylang_editor_height', null, 350, true);
        $useCodeEditor = $this->modx->getOption('polylang_use_code_editor', null, 1, true);
        $whichElementEditor = $this->modx->getOption('which_element_editor', null, '', true);
        $useResourceEditorStatus = $this->modx->getOption('polylang_use_resource_editor_status', null, 1, true);

        $field = array(
            'xtype' => $xtype,
            'id' => "polylang-{$name}-{$data['name']}",
            'key' => "{$data['name']}",
            'source' => "{$className}",
            'translate' => "{$data['translate']}",
            'fieldLabel' => $data['caption'],
            'description' => $data['description'],
            'name' => "{$name}_{$data['name']}",
            'culture_key' => "{$cultureKey}",
            'allowBlank' => $data['required'] ? false : true,
            'anchor' => '100%',
        );
        if ($xtype == 'polylang-text-editor' &&
            $useCodeEditor &&
            $whichElementEditor &&
            $useResourceEditorStatus &&
            !$resource->get('richtext')
        ) {
            $field['xtype'] = 'polylang-code-editor';
        }

        if (!empty($data['code'])) {
            $code = $this->fromJSON($data['code'], array());
            $field = array_merge($field, $code);
        }
        return $field;
    }

    /**
     * @param msOption $option
     * @return null|PolylangOptionType
     */
    public function getOptionType($option)
    {
        $className = $this->loadOptionType($option->get('type'));

        if (class_exists($className)) {
            return new $className($option);
        } else {
            $this->modx->log(modX::LOG_LEVEL_ERROR,
                'Could not initialize Polylang option type class: "' . $className . '"');

            return null;
        }
    }

    /**
     * @param string $type
     * @return mixed
     */
    public function loadOptionType($type)
    {
        $this->modx->loadClass('PolylangProductOption', $this->config['modelPath'] . 'polylang/');
        $typePath = $this->config['corePath'] . 'processors/mgr/ms2/option/types/' . $type . '.class.php';
        if (array_key_exists($typePath, $this->optionTypes)) {
            $className = $this->optionTypes[$typePath];
        } else {
            /** @noinspection PhpIncludeInspection */
            $className = include_once $typePath;
            // handle already included classes
            if ($className == 1) {
                $o = array();
                $s = explode(' ', str_replace(array('_', '-'), ' ', $type));
                foreach ($s as $k) {
                    $o[] = ucfirst($k);
                }
                $className = 'Polylang' . implode('', $o) . 'Type';
            }
            $this->optionTypes[$typePath] = $className;
        }
        return $className;
    }

    public function setDefaultSettings()
    {
        $cultureKey = $this->modx->getOption('cultureKey');
        $defaultDomainLanguage = $this->modx->getOption('polylang_default_domain_settings', null, '{}', true);
        $defaultDomainLanguage = $this->fromJSON($defaultDomainLanguage, array());
        $defaultGeoLanguageCountry = $this->modx->getOption('polylang_geo_language_country', null, '{}', true);
        $defaultGeoLanguageCountry = $this->fromJSON($defaultGeoLanguageCountry, array());

        $settings = $this->modx->getOption(MODX_HTTP_HOST, $defaultDomainLanguage, array(), true);
        $host = $this->modx->getOption('site_host', $settings, MODX_HTTP_HOST, true);
        $siteUrl = $this->modx->getOption('site_url', $settings, MODX_SITE_URL, true);
        $geoDomains = $this->modx->getOption('geo_domains', $settings, array(), true);
        $geoLanguageCountry = $this->modx->getOption('geo_language_country', $settings, $defaultGeoLanguageCountry, true);

        $defaultBaseUrl = $this->modx->getOption('base_url', null, '');
        $originalLanguage = $this->modx->getOption('polylang_original_language', null, $cultureKey, true);
        $defaultLanguage = $this->modx->getOption('polylang_default_language', null, $originalLanguage, true);
        $defaultLanguage = $this->modx->getOption('default_language', $settings, $defaultLanguage, true);
        $forceLanguage = $this->modx->getOption('force_language', $settings, '', true);
        $visitorDefaultLanguage = $this->modx->getOption('visitor_default_language', $settings, '', true);
        $languageGroup = $this->modx->getOption('language_group', $settings, '', true);
        $defaultDetectVisitorLanguage = $this->modx->getOption('polylang_detect_visitor_language');
        $detectVisitorLanguage = $this->modx->getOption('detect_visitor_language', $settings, $defaultDetectVisitorLanguage, true);
        $defaultCacheResourceKey = $this->modx->getOption('cache_resource_key', null, 'resource');
        $this->modx->setOption('polylang_base_host', $host);
        $this->modx->setOption('polylang_original_language', $originalLanguage);
        $this->modx->setOption('polylang_original_base_url', $defaultBaseUrl);
        $this->modx->setOption('polylang_default_language', $defaultLanguage);
        $this->modx->setOption('polylang_default_site_url', $siteUrl);
        $this->modx->setOption('polylang_default_language_group', $languageGroup);
        $this->modx->setOption('polylang_detect_visitor_language', $detectVisitorLanguage);
        $this->modx->setOption('polylang_cache_resource_key', $defaultCacheResourceKey);
        $this->modx->setOption('polylang_geo_domains', $geoDomains);
        $this->modx->setOption('polylang_geo_language_country', $geoLanguageCountry);

        if ($forceLanguage) {
            $this->modx->setOption('polylang_force_language', $forceLanguage);
        }
        if ($visitorDefaultLanguage) {
            $this->modx->setOption('polylang_visitor_default_language', $visitorDefaultLanguage);
        }
        $this->invokeEvent('OnSetPolylangDefaultSettings', array(
            'tools' => $this,
        ));
    }


    /**
     * @param string $cultureKey
     *
     * @return array
     */
    public function getLanguageData($cultureKey)
    {
        $data = [];
        $language = $this->modx->getObject('PolylangLanguage', array(
            'culture_key' => $cultureKey
        ));
        if ($language) {
            $data = $language->toArray();
        }
        return $data;
    }

    /**
     * @param bool $force
     * @return PolylangLanguage|null
     */
    public function detectLanguage($force = false)
    {
        $detectedLanguage = null;
        $currentLanguage = $this->modx->getOption('cultureKey');
        if ($force || (defined(MODX_API_MODE) && MODX_API_MODE)) {
            $sessionKey = $this->makeSessionVarKey('language');
            $savedLanguage = false;
            if (isset($_SESSION[$sessionKey])) {
                $savedLanguage = $_SESSION[$sessionKey];
            } else if (isset($_COOKIE[$sessionKey])) {
                $savedLanguage = $_COOKIE[$sessionKey];
            }

            if ($this->debug) {
                $this->modx->log(modX::LOG_LEVEL_ERROR, "[detectLanguage] Current language={$currentLanguage}");
                $this->modx->log(modX::LOG_LEVEL_ERROR, "[detectLanguage] Saved language={$savedLanguage}");
            }

            if ($savedLanguage) {
                return $this->modx->getObject('PolylangLanguage', array(
                    'active' => 1,
                    'culture_key' => $savedLanguage
                ));
            }
        } else {
            $ctx = $this->modx->context->get('key');
            $url = $this->getRequestUrl(true) . '/';
            $cacheKey = $this->getCacheKey('detectLanguage' . $url . $ctx);
            $defaultLanguage = $this->modx->getOption('polylang_default_language');
            $cacheCultureKey = $this->modx->cacheManager->get($cacheKey);
            if ($this->debug) {
                $this->modx->log(modX::LOG_LEVEL_ERROR, "[detectLanguage] URL={$url}");
                $this->modx->log(modX::LOG_LEVEL_ERROR, "[detectLanguage] Default language={$defaultLanguage}");
                $this->modx->log(modX::LOG_LEVEL_ERROR, "[detectLanguage] Cache cultureKey={$cacheCultureKey}");
            }
            if (!$cacheCultureKey) {
                $this->modx->setOption('cultureKey', $defaultLanguage);
                $q = $this->modx->newQuery('PolylangLanguage');
                $q->where(array(
                    '`active`' => 1,
                    '`culture_key`:!=' => $defaultLanguage,
                ));
                $q->sortby('`rank`', 'ASC');
                /** @var PolylangLanguage[] $languages */
                $languages = $this->modx->getCollection('PolylangLanguage', $q);
                if ($languages) {
                    foreach ($languages as $language) {
                        $siteUrl = $language->getSiteUrl();
                        if (strpos($url, $siteUrl) === 0) {
                            if (!$language->get('active')) {
                                $this->modx->setOption('site_url', $this->modx->getOption("polylang_default_site_url"));
                                $this->modx->sendErrorPage();
                                return null;
                            }
                            $cacheCultureKey = $language->get('culture_key');
                            $this->modx->cacheManager->set($cacheKey, $cacheCultureKey, $this->config['cacheTime']);
                            $detectedLanguage = $language;
                            break;
                        }
                    }
                    if (!$detectedLanguage) {
                        $cacheCultureKey = $this->isAjaxRequest() ? $currentLanguage : $defaultLanguage;
                        $this->modx->cacheManager->set($cacheKey, $cacheCultureKey, $this->config['cacheTime']);
                    }
                }
            }
            if (!$detectedLanguage && $cacheCultureKey) {
                $detectedLanguage = $this->modx->getObject('PolylangLanguage', array('culture_key' => $cacheCultureKey));
            }
            if ($detectedLanguage) {
                if (!isset($_COOKIE['polylang_first_visit'])) {
                    setcookie('polylang_first_visit', 1, [
                        'expires' => time() + 31556926,
                        'path' => '/',
                        'domain' => MODX_HTTP_HOST,
                        'secure' => (boolean)$this->modx->getOption('session_cookie_secure', null, false),
                        'httponly' => (boolean)$this->modx->getOption('session_cookie_httponly', null, true),
                        'samesite' => $this->modx->getOption('session_cookie_samesite', null, ''),
                    ]);
                    $this->modx->setOption('cultureKey', $detectedLanguage->get('culture_key'));
                    $this->invokeEvent('OnTogglePolylangLanguage', array(
                        'tools' => $this,
                        'language' => $detectedLanguage,
                    ));
                }
            }
        }

        return $detectedLanguage;
    }

    /**
     * @param PolylangLanguage $language
     */
    public function setLanguage($language)
    {


        $this->language = $language;
        $siteUrl = $language->getSiteUrl();
        $cultureKey = $language->get('culture_key');
        $languageCode = $this->prepareLanguageCode($cultureKey);
        $scheme = $this->modx->getOption('link_tag_scheme');

        if ($scheme == '-1') {
            $this->modx->setOption('link_tag_scheme', 'abs');
        }
        if ($this->debug) {
            $this->modx->log(modX::LOG_LEVEL_ERROR, "[setLanguage] Culture key={$cultureKey}");
            $this->modx->log(modX::LOG_LEVEL_ERROR, "[setLanguage] Site URL={$siteUrl}");
        }

        if ($language->get('locale')) {
            $locale = $language->get('locale');
        } else {
            $locale = $this->country2locale($languageCode);
        }
        $this->modx->setOption('site_url', $siteUrl);
        $this->modx->setPlaceholder('+site_url', $siteUrl);

        $baseUrl = $language->getBaseUrl();
        $this->modx->setOption('base_url', $baseUrl);
        $this->modx->setPlaceholder('+base_url', $baseUrl);

        $this->modx->setPlaceholder('polylang_locale', $this->prapareLocale($locale));
        $this->modx->setOption('locale', $locale);
        setlocale(LC_ALL, $locale);
        setlocale(LC_NUMERIC, 'C');
        $this->modx->cultureKey = $cultureKey;
        $this->modx->setOption('cultureKey', $cultureKey);
        $this->modx->setPlaceholder('+cultureKey', $cultureKey);
        $this->modx->setPlaceholder('lang', $languageCode);
        $this->modx->setPlaceholder('polylang_site', !$language->isOriginal());
        $this->modx->setPlaceholder('polylang_language_id', $language->get('id'));
        $this->reloadLexicons($cultureKey);
        $sessionKey = $this->makeSessionVarKey('language');
        $this->setSessionVar('language', $cultureKey);
        setcookie($sessionKey, $cultureKey, [
            'expires' => time() + 31556926,
            'path' => '/',
            'domain' => MODX_HTTP_HOST,
            'secure' => (boolean)$this->modx->getOption('session_cookie_secure', null, false),
            'httponly' => (boolean)$this->modx->getOption('session_cookie_httponly', null, true),
            'samesite' => $this->modx->getOption('session_cookie_samesite', null, ''),
        ]);
        $cacheKey = $this->modx->getOption('polylang_cache_resource_key', null, 'resource');
        $this->modx->setOption('cache_resource_key', $cacheKey . '/' . $cultureKey);
    }

    /**
     * @return PolylangLanguage|null
     */
    public function getLanguage()
    {
        return $this->language;
    }

    public function resetLanguage()
    {
        $this->language = null;
    }


    /**
     * @param PolylangLanguage|null $language
     */
    public function setDefaultCurrencyForLanguage($language = null)
    {
        if ($this->modx->getOption('polylang_set_currency_for_language')) {
            if ($this->hasAddition('msmulticurrency')) {
                if (!$language) {
                    $language = $this->detectLanguage();
                }
                if ($language && $language->get('currency_id')) {
                    $this->modx->setOption('msmulticurrency.selected_currency_default', $language->get('currency_id'));
                }
            }
        }
    }

    /**
     * @param string $cultureKey
     */
    public function reloadLexicons($cultureKey)
    {
        $default = array('polylang:default', 'polylang:site');
        if ($this->hasAddition('minishop2')) {
            $default = array_merge($default, array('minishop2:default', 'minishop2:product', 'minishop2:cart'));
        }
        $lexicons = $this->modx->getOption('polylang_reload_lexicon', null, '', true);
        $lexicons = $this->explodeAndClean($lexicons);
        $lexicons = array_merge($default, $lexicons);
        //$this->modx->lexicon->clear();
        //$this->modx->lexicon->clearCache();
        $this->modx->setOption('cultureKey', $cultureKey);
        foreach ($lexicons as $lexicon) {
            $this->modx->lexicon->load("{$cultureKey}:{$lexicon}");
        }
    }

    /**
     * @return PolylangLanguage|null
     */
    public function detectVisitorLanguage()
    {
        $classKey = 'PolylangLanguage';
        $defaultLanguage = $this->modx->getOption('cultureKey');
        $detect = $this->modx->getOption('polylang_detect_visitor_language', null, false);
        $polylangDefaultLanguage = $this->modx->getOption('polylang_default_language', null, $defaultLanguage, true);
        $visitorDefaultLanguage = $this->modx->getOption('polylang_visitor_default_language', null, $polylangDefaultLanguage, true);
        if ($detect && !$this->getSessionVar('language')) {
            $languages = $this->getVisitorLanguages();
            if ($languages) {
                if (!isset($languages[$visitorDefaultLanguage])) {
                    $languages[] = $visitorDefaultLanguage;
                }
                $host = $this->getCurrentHost();
                foreach ($languages as $lang) {
                    if ($hostLanguage = $this->hasHostLanguage($lang, $host))
                        return $this->modx->getObject($classKey, array(
                            'culture_key' => $hostLanguage
                        ));
                }
            }
        }
        return null;
    }

    /**
     * @return string
     */
    protected function getCurrentHost()
    {
        $host = $this->modx->getOption('polylang_base_host');
        return str_replace('www.', '', $host);
    }

    /**
     * @return bool
     */
    public function isDetectGeoRedirectDomain()
    {
        $detect = $this->modx->getOption('polylang_geo_detect_domain', null, false);
        return $detect && !$this->getSessionVar('georedirectdomain') && !$this->isBot();
    }

    /**
     * @return string
     */
    public function detectGeoRedirectDomain()
    {
        $domain = '';
        $geoDomains = $this->modx->getOption('polylang_geo_domains', null, array());
        $geoParam = $this->modx->getOption('polylang_geo_detect_domain_param', null, '');
        if ($this->debug) {
            $this->modx->log(modX::LOG_LEVEL_ERROR, "[detectGeoRedirectDomain] Geo Domains:}" . print_r($geoDomains, 1));
            $this->modx->log(modX::LOG_LEVEL_ERROR, "[detectGeoRedirectDomain] Geo param={$geoParam}");
        }
        if ($geoDomains && $geoParam) {
            if ($geoLocator = $this->polylang->getGeoLocator()) {
                $ip = $this->getClientIp();
                $geoLocator->setIp($ip);
                $geoValue = $geoLocator->execute($geoParam, true);
                if ($geoValue && isset($geoDomains[$geoValue])) {
                    $domain = $geoDomains[$geoValue];
                }
                if ($this->debug) {
                    $this->modx->log(modX::LOG_LEVEL_ERROR, "[detectGeoRedirectDomain] Geo Value={$geoValue}");
                }
                $response = $this->invokeEvent('OnDetectPolylangGeoRedirectDomain', array(
                    'tools' => $this,
                    'geoLocator' => $geoLocator,
                    'geoValue' => $geoValue,
                    'geoParam' => $geoParam,
                    'geoDomains' => $geoDomains,
                    'domain' => $domain,
                ));
                if ($response['success']) {
                    $domain = $response['data']['domain'];
                } else {
                    $domain = '';
                }
            }
        }
        if ($this->debug) {
            $this->modx->log(modX::LOG_LEVEL_ERROR, "[detectGeoRedirectDomain] Detected domain={$domain}");
        }
        return $domain;
    }

    /**
     * @return bool
     */
    public function isDetectGeoRedirectLanguage()
    {
        $detect = $this->modx->getOption('polylang_geo_detect_language', null, false);
        return $detect && !$this->getSessionVar('georedirectlanguage') && !$this->isBot();
    }

    /**
     * @return string
     */
    public function detectGeoRedirectLanguage()
    {
        $language = '';
        $languageCountry = $this->modx->getOption('polylang_geo_language_country', null, array());
        if ($this->debug) {
            $this->modx->log(modX::LOG_LEVEL_ERROR, "[detectGeoRedirectLanguage] language for country:}" . print_r($languageCountry, 1));
        }
        if ($languageCountry) {
            if ($geoLocator = $this->polylang->getGeoLocator()) {
                $ip = $this->getClientIp();
                if ($this->debug) {
                    $this->modx->log(modX::LOG_LEVEL_ERROR, "[detectGeoRedirectDomain] for IP:{$ip}");
                }
                $geoLocator->setIp($ip);
                $countryCode = strtolower($geoLocator->getCountryCode());
                if ($countryCode && isset($languageCountry[$countryCode])) {
                    $language = $languageCountry[$countryCode];
                }
                if ($this->debug) {
                    $this->modx->log(modX::LOG_LEVEL_ERROR, "[detectGeoRedirectDomain] country code:{$countryCode}");
                }
                $response = $this->invokeEvent('OnDetectPolylangGeoRedirectLanguage', array(
                    'tools' => $this,
                    'geoLocator' => $geoLocator,
                    'country_code' => $countryCode,
                    'language_country' => $languageCountry,
                    'language' => $language,
                ));
                if ($response['success']) {
                    $language = $response['data']['language'];
                } else {
                    $language = '';
                }
            }
        }
        if ($this->debug) {
            $this->modx->log(modX::LOG_LEVEL_ERROR, "[detectGeoRedirectDomain] Detected language={$language}");
        }
        return $language;
    }


    /**
     * @param string $host
     * @param string|array $query
     */
    public function redirectToDomain(string $host, $query = '')
    {
        if (!$host || !$this->modx->resource) return;
        $id = $this->modx->resource->get('id');
        $ctx = $this->modx->resource->get('context_key');
        $baseHost = $this->modx->getOption('polylang_base_host', null, MODX_HTTP_HOST, true);
        $url = $this->modx->makeUrl($id, $ctx, $query, 'full');
        $url = str_replace($baseHost, $host, $url);
        header("Location: {$url}", true, 307);
        exit();
    }

    /**
     * @return PolylangLanguage|null
     */
    public function getForceLanguage()
    {
        $classKey = 'PolylangLanguage';
        $forceLanguage = $this->modx->getOption('polylang_force_language');
        if ($forceLanguage && !$this->getSessionVar('language')) {
            $q = $this->modx->newQuery($classKey);
            $q->where(array(
                '`active`' => 1,
                '`culture_key`:=' => $forceLanguage
            ));
            $q->sortby('`rank`');
            return $this->modx->getObject($classKey, $q);
        }
        return null;
    }

    /**
     * @return array
     */
    public function getVisitorLanguages()
    {
        $result = array();
        if (($list = strtolower($_SERVER['HTTP_ACCEPT_LANGUAGE']))) {
            if (preg_match_all('/([a-z]{1,8}(?:-[a-z]{1,8})?)(?:;q=([0-9.]+))?/', $list, $list)) {
                $tmp = array_combine($list[1], $list[2]);
                foreach ($tmp as $key => $v) {
                    $key = strtok($key, '-');
                    if (!isset($result[$key])) {
                        $result[$key] = $v ? $v : 1;
                    }
                }
                arsort($result, SORT_NUMERIC);
            }
        }
        return array_keys($result);
    }

    /**
     * @return string|array
     */
    public function getRequestQuery()
    {
        return $this->modx->request->getParameters();
    }

    /**
     * @param int|string $time
     */
    public function setLastModifiedHeader($time)
    {
        header('Last-Modified: ' . $this->gmtDate($time));
    }

    /**
     * @param int|string $time
     *
     * @return false|string
     */
    public function gmtDate($time)
    {
        if (is_string($time)) {
            $time = strtotime($time);
        }
        return gmdate('D, d M Y H:i:s \G\M\T', $time);
    }

    /**
     * @param mixed $value
     *
     * @return bool
     */
    public function isJSONStr($value): bool
    {
        return is_string($value) && ($value[0] == '[' || $value[0] == '{');
    }

    /**
     * @param mixed $value
     *
     * @return bool
     */
    public function isMIGXContent($value): bool
    {
        if (!$this->isJSONStr($value)) {
            return false;
        }
        return strpos($value, 'MIGX_id') !== false;

    }

    /**
     * @param string|array $val
     *
     * @return string
     */
    public function normalizeJSONStr($val)
    {
        if (is_string($val)) {
            $val = $this->fromJSON($val, array());
        }
        $val = json_encode($val, JSON_UNESCAPED_UNICODE);
        return $val;
    }

    /**
     * @return bool
     */
    public function isCurrentDefaultLanguage()
    {
        return $this->modx->getOption('cultureKey') == $this->getDefaultLanguage();
    }

    /**
     * @return string
     */
    public function getDefaultLanguage()
    {
        $defaultLanguage = $this->modx->getOption('cultureKey');
        return $this->modx->getOption('polylang_default_language', null, $defaultLanguage, true);
    }

    /**
     * @return bool
     */
    public function isCurrentOriginalLanguage()
    {
        return $this->modx->getOption('cultureKey') == $this->getOriginalLanguageKey();
    }

    /**
     * @return string
     */
    public function getOriginalLanguageKey()
    {
        $cultureKey = $this->modx->getOption('cultureKey');
        return $this->modx->getOption('polylang_original_language', null, $cultureKey, true);
    }

    /**
     * @param string $lang
     * @param string $host
     *
     * @return bool|string
     */
    public function hasHostLanguage(string $lang, string $host = '')
    {
        $host = $host ?: $_SERVER["HTTP_HOST"];
        $cacheKey = $this->getCacheKey('hasHostLanguage' . $host . $lang);
        $result = $this->modx->cacheManager->get($cacheKey);
        if (!is_string($result)) {
            $result = 'false';
            $classKey = 'PolylangLanguage';
            $host = trim(mb_strtolower($host));
            $lang = trim(mb_strtolower($lang));
            $lang = $this->prepareLanguageCode($lang);
            $q = $this->modx->newQuery($classKey);
            $q->where(array(
                "`{$classKey}`.`active`" => 1,
            ));
            $q->where("LOWER(`{$classKey}`.`culture_key`) LIKE '{$lang}%'");

            /** @var PolylangLanguage[] $languages */
            $languages = $this->modx->getCollection('PolylangLanguage', $q);
            if ($languages) {
                foreach ($languages as $language) {
                    if (mb_strtolower($language->getHost()) == $host) {
                        $result = $language->get('culture_key');
                        break;
                    }
                }

            }
            $this->modx->cacheManager->set($cacheKey, $result);
        }
        if (!is_string($result) || $result === 'false') {
            $result = false;
        }
        return $result;
    }

    /**
     * @return false
     */
    public function isBot()
    {
        /** @var PolylangCrawlerDetector $detector */
        $detector = $this->polylang->getCrawlerDetector();
        if ($detector) {
            return $detector->isCrawler();
        }
        return false;
    }

    /**
     * @param bool $removeLastSlash
     *
     * @return string
     */
    public function getRequestUrl($removeLastSlash = false)
    {
        $schema = MODX_URL_SCHEME;
        $url = $schema . $_SERVER["HTTP_HOST"] . $_SERVER['REQUEST_URI'];
        if ($removeLastSlash) {
            $url = preg_replace("#/$#", '', $url);
        }
        return $url;
    }

    /**
     * @param int $rid
     * @param string $cultureKey
     * @param bool $cache
     *
     * @return bool
     */
    public function hasError404($rid, $cultureKey = '', $cache = true)
    {
        if (!$this->modx->getOption('polylang_use_error_404')) return false;
        $cultureKey = $cultureKey ? $cultureKey : $this->modx->getOption('cultureKey');
        $cacheKey = $this->getCacheKey('hasError404' . $rid . $cultureKey);
        $result = $this->modx->cacheManager->get($cacheKey);
        if (!is_string($result) || !$cache) {
            $result = false;
            if ($cultureKey != $this->getOriginalLanguageKey()) {
                $ignoreResources = $this->modx->getOption('polylang_ignore_error_404_resources', null, '');
                $ignoreResources = $this->explodeAndClean($ignoreResources);
                if (empty($ignoreResources) || !in_array($rid, $ignoreResources)) {
                    $resources = $this->modx->getOption('polylang_use_error_404_resources', null, '');
                    $resources = $this->explodeAndClean($resources);

                    if (empty($resources) || in_array($rid, $resources)) {
                        $result = !$this->hasLocalization($rid, $cultureKey);
                    }
                    $response = $this->invokeEvent('OnHasPolylangError404', array(
                        'tools' => $this,
                        'resources' => $resources,
                        'has' => $result,
                    ));
                    $result = $response['data']['has'];
                }
            }
            $result = $result ? 'true' : 'false';
            $this->modx->cacheManager->set($cacheKey, $result);
        }
        return filter_var($result, FILTER_VALIDATE_BOOLEAN);
    }

    /**
     * @param int $rid
     * @param string $cultureKey
     * @param bool $force
     *
     * @return void
     */
    public function clearCacheHasError404(int $rid, string $cultureKey = '', bool $force = false)
    {
        if (
            $force ||
            $this->modx->getOption('polylang_use_error_404')
        ) {
            $cultureKey = $cultureKey ?: $this->modx->getOption('cultureKey');
            $cacheKey = $this->getCacheKey('hasError404' . $rid . $cultureKey);
            if ($this->modx->cacheManager->delete($cacheKey)) {
                $this->clearCacheMapLocalizations();
            }
        }
    }

    /**
     * @param bool $onlyActive
     * @param bool $cache
     *
     * @return array
     */
    public function getMapLocalizations($onlyActive = true, $cache = true)
    {
        $cacheKey = $this->getCacheKey('getMapLocalizations' . $onlyActive);
        $result = $this->modx->cacheManager->get($cacheKey);
        if (!$cache || !is_array($result)) {
            $result = array();
            $classKey = 'PolylangContent';
            $q = $this->modx->newQuery($classKey);
            $q->leftJoin('PolylangLanguage', 'Language', "`Language`.`culture_key` = `{$classKey}`.`culture_key`");
            $q->select($this->modx->getSelectColumns($classKey, $classKey, '', array('content_id', 'culture_key')));
            $q->where(array(
                "`Language`.`active`" => 1
            ));
            if ($onlyActive) {
                $q->where(array(
                    "`{$classKey}`.`active`" => 1,
                ));
            }
            $q->sortby("`{$classKey}`.`content_id`");

            if ($q->prepare() && $q->stmt->execute()) {
                while ($item = $q->stmt->fetch(PDO::FETCH_ASSOC)) {
                    if (empty($result[$item['content_id']])) {
                        $result[$item['content_id']] = array();
                    }
                    $result[$item['content_id']][] = $item['culture_key'];
                }
            }
            $this->modx->cacheManager->set($cacheKey, $result, $this->config['cacheTime']);
        }
        return $result;
    }

    /**
     * @param bool $onlyActive
     *
     * @return bool
     */
    public function clearCacheMapLocalizations(bool $onlyActive = true)
    {
        $cacheKey = $this->getCacheKey('getMapLocalizations' . $onlyActive);
        return $this->modx->cacheManager->delete($cacheKey);
    }


    /**
     * @param int $rid
     * @param string $cultureKey
     * @param bool $onlyActive
     * @param bool $cache
     *
     * @return bool
     */
    public function hasLocalization($rid, $cultureKey, $onlyActive = true, $cache = true)
    {

        $map = $this->getMapLocalizations($onlyActive, $cache);
        if (empty($map) || empty($map[$rid])) return false;
        return in_array($cultureKey, $map[$rid]);
    }

    /**
     * @return bool
     */
    public function isAjaxRequest()
    {
        if (
            isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
            strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'
        ) {
            return true;
        }
        return false;
    }

    /**
     * @return false
     */
    public function isAjaxRequestInAssets()
    {
        if (!$this->isAjaxRequest()) return false;
        $assetsUrl = $this->modx->getOption('assets_url', null, MODX_ASSETS_URL);
        $assetsUrl = preg_quote($assetsUrl, '/');
        return (bool)preg_match("/^{$assetsUrl}/", $_SERVER['REQUEST_URI']);

    }

    /**
     * @param string $code
     *
     * @return string
     */
    public function prepareLanguageCode($code)
    {
        return preg_replace('/([a-z]{2})[_-]([a-z]{2})/i', '$1', $code);
    }

    /**
     * @param bool $onlyActive
     * @param array $options
     *
     * @return array
     */
    public function getLanguageKeys($onlyActive = true, array $options = array())
    {
        $ctx = $this->modx->context->get('key');
        $languageGroup = $this->modx->getOption('languageGroup', $options);
        $onlyParents = $this->modx->getOption('onlyParents', $options, false);
        $cacheKey = $this->getCacheKey('getLanguageKeys' . $onlyActive . $onlyParents . $languageGroup . $ctx);
        $result = $this->modx->cacheManager->get($cacheKey);
        if (!is_array($result)) {
            $result = array();
            $classKey = 'PolylangLanguage';
            $q = $this->modx->newQuery($classKey);
            $q->leftJoin('PolylangLanguageGroupMember', 'LanguageGroupMember', "`LanguageGroupMember`.`language_id` = `{$classKey}`.`id`");
            $q->select($this->modx->getSelectColumns($classKey, $classKey, '', array('culture_key')));
            if ($onlyActive) {
                $q->where(array("`{$classKey}`.`active`" => 1));
            }
            if ($onlyParents) {
                $q->where(array("`{$classKey}`.`parent`" => 0));
            }
            if ($languageGroup) {
                if (!is_array($languageGroup)) {
                    $languageGroup = $this->explodeAndClean($languageGroup);
                }
                $q->where(array('`LanguageGroupMember`.`group_id`:IN' => $languageGroup));
            }
            $q->sortby("`{$classKey}`.`rank_translation`", 'ASC');
            $q->groupby("`{$classKey}`.`culture_key`");

            if ($q->prepare() && $q->stmt->execute()) {
                $result = $q->stmt->fetchAll(PDO::FETCH_COLUMN);
            }
            $this->modx->cacheManager->set($cacheKey, $result, $this->config['cacheTime']);
        }
        return $result;
    }

    /**
     * @param int $resourceId
     * @param bool $onlyActive
     * @param bool $cache
     *
     * @return array
     */
    public function getResourceLanguageKeys($resourceId, $onlyActive = true, $cache = true)
    {
        $ctx = $this->modx->context->get('key');
        $cacheKey = $this->getCacheKey('getResourceLanguageKeys' . $resourceId . $onlyActive . $ctx);
        $result = $this->modx->cacheManager->get($cacheKey);
        if (!is_array($result) || !$cache) {
            $result = array();
            $classKey = 'PolylangContent';
            $q = $this->modx->newQuery($classKey);
            $q->select($this->modx->getSelectColumns($classKey, $classKey, '', array('culture_key')));
            $q->where(array('content_id' => $resourceId));
            if ($onlyActive) {
                $q->leftJoin('PolylangLanguage', 'Language', '`Language`.`culture_key` = `PolylangContent`.`culture_key`');
                $q->where(array(
                    '`active`' => 1,
                    '`Language`.`active`' => 1,

                ));
            }
            if ($q->prepare() && $q->stmt->execute()) {
                $result = $q->stmt->fetchAll(PDO::FETCH_COLUMN);
            }
            $this->modx->cacheManager->set($cacheKey, $result, $this->config['cacheTime']);
        }
        return $result;
    }


    /**
     * @param string $code
     * @return PolylangLanguage|null
     */
    public function getLanguageByCode($code)
    {
        return $this->modx->getObject('PolylangLanguage', array(
            'culture_key' => $code
        ));
    }

    /**
     * @param bool $onlyActive
     * @param array $options
     *
     * @return array
     */
    public function getLanguages($onlyActive = true, array $options = array())
    {
        $ctx = $this->modx->context->get('key');
        $key = $this->modx->getOption('key', $options, 'culture_key');
        $languageGroup = $this->modx->getOption('languageGroup', $options);
        $sort = $this->modx->getOption('sort', $options, 'rank_translation');
        $dir = $this->modx->getOption('dir', $options, 'ASC');
        $cacheKey = $this->getCacheKey('getLanguages' . $onlyActive . $languageGroup . $ctx);
        $result = $this->modx->cacheManager->get($cacheKey);
        if (!is_array($result)) {
            $result = array();
            $classKey = 'PolylangLanguage';
            $q = $this->modx->newQuery($classKey);
            $q->leftJoin('PolylangLanguageGroupMember', 'LanguageGroupMember', "`LanguageGroupMember`.`language_id` = `{$classKey}`.`id`");
            $q->select($this->modx->getSelectColumns($classKey, $classKey));
            if ($onlyActive) {
                $q->where(array("`{$classKey}`.`active`" => 1));
            }
            if ($languageGroup) {
                if (!is_array($languageGroup)) {
                    $languageGroup = $this->explodeAndClean($languageGroup);
                }
                $q->where(array('`LanguageGroupMember`.`group_id`:IN' => $languageGroup));
            }
            $q->sortby("`{$classKey}`.`{$sort}`", $dir);
            $q->groupby("`{$classKey}`.`culture_key`");
            if ($q->prepare() && $q->stmt->execute()) {
                while ($item = $q->stmt->fetch(PDO::FETCH_ASSOC)) {
                    $result[$item[$key]] = $item;
                }
            }
            $this->modx->cacheManager->set($cacheKey, $result, $this->config['cacheTime']);
        }
        return $result;
    }

    /**
     * @param bool $onlyActive
     * @param array $options
     *
     * @return array
     */
    public function getLanguageUrls($onlyActive = true, array $options = array())
    {
        $ctx = $this->modx->context->get('key');
        $withSlash = $this->modx->getOption('withSlash', $options, true);
        $languageGroup = $this->modx->getOption('languageGroup', $options);
        $cacheKey = $this->getCacheKey('getLanguageUrls' . $ctx . $onlyActive . $languageGroup . $withSlash);
        $result = $this->modx->cacheManager->get($cacheKey);
        if (!is_array($result)) {
            $result = array();
            $classKey = 'PolylangLanguage';
            $q = $this->modx->newQuery($classKey);
            $q->leftJoin('PolylangLanguageGroupMember', 'LanguageGroupMember', "`LanguageGroupMember`.`language_id` = `{$classKey}`.`id`");
            if ($onlyActive) {
                $q->where(array('`active`' => 1));
            }
            if ($languageGroup) {
                if (!is_array($languageGroup)) {
                    $languageGroup = $this->explodeAndClean($languageGroup);
                }
                $q->where(array('`LanguageGroupMember`.`group_id`:IN' => $languageGroup));
            }
            $q->sortby('`rank`');
            /** @var  PolylangLanguage [] $languages */
            $languages = $this->modx->getCollection($classKey, $q);
            if ($languages) {
                foreach ($languages as $language) {
                    $result[$language->getSiteUrl($withSlash)] = $language->get('culture_key');
                }
            }
            $this->modx->cacheManager->set($cacheKey, $result, $this->config['cacheTime']);
        }
        return $result;
    }

    /**
     * @param int|string $id
     *
     * @return array
     */
    public function getDependentLanguages($id, array $options = array())
    {
        $result = array();
        $classKey = 'PolylangLanguage';
        $onlyActive = $this->modx->getOption('onlyActive', $options);
        if (!is_numeric($id)) {
            $language = $this->modx->getObject('PolylangLanguage', array('culture_key' => $id));
            if ($language) {
                $id = $language->get('id');
            } else {
                return $result;
            }
        }
        $q = $this->modx->newQuery($classKey);
        $q->select($this->modx->getSelectColumns($classKey, $classKey));
        $q->where(array(
            '`parent`' => $id
        ));
        if ($onlyActive) {
            $q->where(array('`active`' => 1));
        }
        if ($q->prepare() && $q->stmt->execute()) {
            $result = $q->stmt->fetchAll(PDO::FETCH_ASSOC);
        }
        return $result;
    }

    /**
     * @param array $fields
     *
     * @return array
     */
    public function putSearchFields(array $fields)
    {
        if (
            empty($fields) ||
            empty($this->modx->getOption('polylang_mse2_index'))
        ) {
            return $fields;
        }
        $defaultLanguage = $this->getOriginalLanguageKey();
        $languages = $this->getLanguageKeys();
        foreach ($languages as $lang) {
            if ($lang == $defaultLanguage) continue;
            foreach ($fields as $field => $weight) {
                $key = "{$lang}_{$field}";
                $fields[$key] = $weight;
            }
        }
        return $fields;
    }

    /**
     * @param mSearch2 $mSearch2
     * @param modResource $resource
     */
    public function putSearchIndex(mSearch2 &$mSearch2, modResource &$resource)
    {
        if (
            $mSearch2->fields &&
            $this->modx->getOption('polylang_mse2_index')
        ) {
            foreach ($this->getContentClassesTarget() as $className => $target) {
                if ($resource instanceof $target) {
                    if ($this->isSubclassContent($className)) {
                        $this->modx->call($className, 'putSearchIndex', array(&$this->modx, &$mSearch2, &$resource));
                    } else {
                        $this->modx->log(modX::LOG_LEVEL_ERROR, "[putSearchIndex] Class '{$className}' is not extend 'PolylangContentMain'!");
                    }
                }
            }
        }
    }

    /**
     * @param string $className
     *
     * @return bool
     */
    public function isSubclassContent($className)
    {
        try {
            $this->modx->loadClass($className);
            $class = new ReflectionClass($className);
            return $class->isSubclassOf('PolylangContentMain');
        } catch (Exception $e) {
            $this->modx->log(modX::LOG_LEVEL_ERROR, $e->getMessage());
            return false;
        }
    }

    /**
     * @param $var
     *
     * @return string
     */
    public function makeSessionVarKey($var)
    {
        $ctx = $this->modx->context->get('key');
        if ($ctx == 'mgr') $ctx = 'web';
        $key = md5(MODX_HTTP_HOST . $this->sessionKey);
        return "{$key}:{$ctx}:{$var}";
    }

    /**
     * @param string $var
     * @param mixed $default
     *
     * @return mixed
     */
    public function getSessionVar($var, $default = null)
    {
        $key = $this->makeSessionVarKey($var);
        return array_key_exists($key, $_SESSION) ? $_SESSION[$key] : $default;
    }

    /**
     * @param string $var
     * @param mixed $value
     */
    public function setSessionVar($var, $value)
    {
        $key = $this->makeSessionVarKey($var);
        $_SESSION[$key] = $value;
    }

    /**
     * @param modResource $resource
     * @param array $options
     */
    public function overrideResourceTvs(modResource &$resource, array $options = array())
    {
        $cultureKey = $this->modx->getOption('cultureKey', $options, '', true);
        $content = $this->modx->getObject('PolylangContent', array(
            '`culture_key`' => $cultureKey,
            '`content_id`' => $resource->get('id'),
        ));
        if (!$content) return;
        if ($tvs = $content->getTVKeys()) {
            $tvValues = PolylangContent::_loadTVs($content, '', array('isPrepare' => false));
            foreach ($tvs as $key) {
                $value = isset($tvValues[$key]) ? $tvValues[$key] : '';// $content->get($key);
                if (!empty($value)) {
                    $tv = $resource->get($key);
                    if (is_array($tv)) {
                        $resource->set($tv[0], array(
                            $tv[0],
                            $value,
                            $tv[2],
                            $tv[3],
                            $tv[4],
                        ));
                    } else if (is_string($tv)) {
                        $resource->set($key, $value);
                    }
                }
            }
        }
    }

    /**
     * @param callable $callback
     * @param array $options
     */
    public function prepareResourceData(callable $callback, array $options = array())
    {
        $contentId = $this->modx->getOption('content_id', $options);
        $class = $this->modx->getOption('class', $options);
        if (empty($class)) {
            $object = $this->modx->getObject('modResource', $contentId);
            if ($object) {
                $class = get_class($object);
            }
        }
        $class = str_replace('_mysql', '', $class);
        $tvPrefix = $this->modx->getOption('tvPrefix', $options, '', true);
        $skipTVs = $this->modx->getOption('skipTVs', $options, false, true);
        $decodeJSON = $this->modx->getOption('decodeJSON', $options, true, true);
        $cultureKey = $this->modx->getOption('cultureKey', $options, '', true);
        $includeTVs = $this->modx->getOption('includeTVs', $options, '', true);
        $processTVs = $this->modx->getOption('processTVs', $options, '', true);
        $skipEmptyValue = $this->modx->getOption('polylang_skip_empty_value', $options, true, true);
        if ($includeTVs && is_string($includeTVs)) {
            $includeTVs = $this->explodeAndClean($includeTVs, ',');
        }
        try {
            $class = new ReflectionClass($class);
            foreach ($this->getContentClassesTarget() as $className => $target) {
                if ($target == 'msProduct' && !$this->hasAddition('minishop2')) continue;
                if (!class_exists($target)) {
                    $this->modx->loadClass($target);
                }
                if ($class->getName() == $target || $class->isSubclassOf($target)) {
                    $tvs = array();
                    $process = array();
                    $exclude = array('id', 'content_id', 'culture_key');
                    $q = $this->modx->newQuery($className);
                    $q->where(array(
                        '`culture_key`' => $cultureKey,
                        '`content_id`' => $contentId,
                    ));
                    if ($className == 'PolylangContent') {
                        $exclude[] = 'active';
                        $q->where(array(
                            '`active`' => 1,
                        ));
                    }
                    /** @var xPDOObject $object */
                    $object = $this->modx->getObject($className, $q);
                    if ($object) {
                        $data = $object->toArray();
                        $status = $this->getStatusFields($className);
                        if ($className == 'PolylangContent') {

                            $tvs = $object->getTVKeys();
                            $process = $processTVs == 1 ? $tvs : $this->explodeAndClean($processTVs);

                            if ($skipTVs) {
                                $exclude = array_merge($exclude, $tvs);
                            }
                        }
                        foreach ($data as $key => $value) {
                            if (
                                in_array($key, $exclude) ||
                                ($status && isset($status[$key]) && !$status[$key])
                            ) {
                                continue;
                            }
                            if ($tvs && $includeTVs) {
                                if (in_array($key, $includeTVs)) {
                                    $key = $tvPrefix . $key;
                                    if ($process && isset($process[$key])) {
                                        $templateVar = $this->modx->getObject('modTemplateVar', array('name' => $key));
                                        $value = $templateVar->renderOutput($data['id']);
                                    }
                                }
                            }
                            if (!empty($value) || (empty($value) && !$skipEmptyValue)) {
                                if ($decodeJSON) {
                                    if ($this->isJSONStr($value)) {
                                        $tmp = json_decode($value, true);
                                        if (json_last_error() == JSON_ERROR_NONE) {
                                            $value = $tmp;
                                        }
                                    }
                                }
                                if ($value === null) $value = '';
                                $callback($key, $value, $this);
                            }
                        }
                    } else if ($className == 'PolylangContent') {
                        return;
                    }
                }
            }
        } catch (Exception $e) {
            $this->modx->log(modX::LOG_LEVEL_ERROR, $e->getMessage());
        }
    }

    /**
     * @param array $tvs
     *
     * @return array|false
     */
    public function getNameTranslatableTVs($tvs = array())
    {
        $cacheKey = $this->getCacheKey('getNameTranslatableTVs' . implode('', $tvs));
        $result = $this->modx->cacheManager->get($cacheKey);
        if (!is_array($result)) {
            $result = array();
            $classKey = 'modTemplateVar';
            $q = $this->modx->newQuery($classKey);
            $q->select($this->modx->getSelectColumns($classKey, $classKey, '', array('name')));
            $q->where(array(
                "`{$classKey}`.`polylang_translate`" => 1
            ));
            if ($tvs) {
                $q->where(array(
                    "`{$classKey}`.`name`:IN" => $tvs
                ));
            }
            if ($q->prepare() && $q->stmt->execute()) {
                $result = $q->stmt->fetchAll(PDO::FETCH_COLUMN);
            }
            $this->modx->cacheManager->set($cacheKey, $result, $this->config['cacheTime']);
        }
        return $result;
    }

    /**
     * @param string $cultureKey
     * @param array $tvs
     *
     * @return array
     */
    public function getDefaultTextTvs($cultureKey, $tvs = array())
    {
        $cacheKey = $this->getCacheKey('getDefaultTextTvs' . $cultureKey . implode('', $tvs));
        $result = $this->modx->cacheManager->get($cacheKey);
        if (!is_array($result)) {
            $result = array();
            $classKey = 'PolylangTvTmplvars';
            $q = $this->modx->newQuery($classKey);
            $q->select($this->modx->getSelectColumns($classKey, $classKey));
            $q->where(array(
                "`{$classKey}`.`culture_key`:IN" => $cultureKey
            ));
            if ($tvs) {
                $q->leftJoin('modTemplateVar', 'modTemplateVar', "`modTemplateVar`.`id` = `{$classKey}`.`tmplvarid`");
                $q->where(array(
                    "`modTemplateVar`.`name`:IN" => $tvs,
                ));
            }
            if ($q->prepare() && $q->stmt->execute()) {
                while ($item = $q->stmt->fetch(PDO::FETCH_ASSOC)) {
                    $result[$item['tmplvarid']] = $item['default_text'];
                }
            }
            $this->modx->cacheManager->set($cacheKey, $result, $this->config['cacheTime']);
        }
        return $result;
    }


    public function prepareModelContent()
    {
        $disabledFields = $this->getDisabledContentFields();
        $this->modx->getFieldMeta('PolylangContent');
        if ($disabledFields && !empty($this->modx->map['PolylangContent'])) {
            foreach ($disabledFields as $field) {
                unset($this->modx->map['PolylangContent']['fields'][$field]);
                unset($this->modx->map['PolylangContent']['fieldMeta'][$field]);
            }
        }
    }

    /**
     * @param array $ids
     */
    public function removeResourceLanguages(array $ids)
    {
        if ($ids) {
            $classes = $this->getContentClasses();
            if ($classes) {
                foreach ($classes as $class) {
                    $list = $this->modx->getCollection($class, array('content_id:IN' => $ids));
                    if ($list) {
                        foreach ($list as $item) {
                            $item->remove();
                        }
                    }
                }
            }
        }
    }

    /**
     * @return array
     */
    public function getDisabledContentFields()
    {
        $cacheKey = $this->getCacheKey('getDisabledFields_PolylangContent');
        $result = $this->modx->cacheManager->get($cacheKey);
        if (!is_array($result)) {
            $result = array();
            $classKey = 'PolylangField';
            $q = $this->modx->newQuery($classKey);
            $q->where(array(
                '`active`' => 0,
                '`class_name`' => 'PolylangContent',
            ));
            $q->select($this->modx->getSelectColumns($classKey, $classKey, '', array('name')));
            if ($q->prepare() && $q->stmt->execute()) {
                $result = $q->stmt->fetchAll(PDO::FETCH_COLUMN);
            }
            $this->modx->cacheManager->set($cacheKey, $result, $this->config['cacheTime']);
        }

        return $result;
    }


    /**
     * @param string $class
     * @return array
     */
    public function getStatusFields($class)
    {
        $cacheKey = $this->getCacheKey('field' . $class);
        $result = $this->modx->cacheManager->get($cacheKey);
        if (!is_array($result)) {
            $result = array();
            $classKey = 'PolylangField';
            $q = $this->modx->newQuery($classKey);
            $q->select($this->modx->getSelectColumns($classKey, $classKey, '', array('name', 'active')));
            $q->where(array(
                '`class_name`' => $class,
            ));
            if ($q->prepare() && $q->stmt->execute()) {
                while ($item = $q->stmt->fetch(PDO::FETCH_ASSOC)) {
                    $result[$item['name']] = $item['active'];
                }
                $this->modx->cacheManager->set($cacheKey, $result, $this->config['cacheTime']);
            }

        }
        return $result;
    }

    /**
     * @param int $languageId
     *
     * @return array
     */
    public function getLanguageGroupField($languageId, $field = 'id')
    {
        $result = array();
        $classKey = 'PolylangLanguageGroup';
        $q = $this->modx->newQuery($classKey);
        $q->leftJoin('PolylangLanguageGroupMember', 'GroupMember', "`GroupMember`.`group_id` = `{$classKey}`.`id`");
        $q->select($this->modx->getSelectColumns($classKey, $classKey, '', array($field)));
        $q->where(array('`GroupMember`.`language_id`' => $languageId));
        if ($q->prepare() && $q->stmt->execute()) {
            $result = $q->stmt->fetchAll(PDO::FETCH_COLUMN);
        }
        return $result;
    }

    /**
     * @param int $languageId
     *
     * @return bool
     */
    public function clearLanguageGroups($languageId)
    {
        $q = $this->modx->newQuery('PolylangLanguageGroupMember');
        $q->command('DELETE');
        $q->where(array('language_id' => $languageId));
        $q->prepare();
        return $q->stmt->execute();
    }

    /**
     * @param int $languageId
     * @param array $groups
     * @param bool $clear
     */
    public function addLanguageInGroups($languageId, array $groups = array(), $clear = true)
    {
        if ($clear) $this->clearLanguageGroups($languageId);
        if ($groups) {
            foreach ($groups as $group) {
                /** @var PolylangLanguageGroupMember $groupMember */
                $groupMember = $this->modx->newObject('PolylangLanguageGroupMember');
                $groupMember->fromArray(array(
                    'group_id' => $group,
                    'language_id' => $languageId,
                ), '', true);
                $groupMember->save();
            }
        }
    }


    /**
     * @param int $templateId
     * @param int $languageId
     *
     * @return array
     */
    public function getSeoPatternByTemplateId($templateId, $languageId)
    {
        $cacheKey = $this->getCacheKey('getSeoPatternByTemplateId' . $templateId . $languageId);
        $result = $this->modx->cacheManager->get($cacheKey);
        if (!is_array($result)) {
            $result = array();
            if ($templateId && $languageId) {
                $classKey = 'PolylangSeoPattern';
                $q = $this->modx->newQuery($classKey);
                $q->leftJoin('PolylangSeoFields', 'Fields', "`Fields`.`id` = `{$classKey}`.`field_id`");
                $q->leftJoin('PolylangSeoPatternTemplate', 'Template', "`Template`.`pattern_id` = `{$classKey}`.`id`");

                $q->select($this->modx->getSelectColumns($classKey, $classKey, '', array('value')));
                $q->select($this->modx->getSelectColumns('PolylangSeoFields', 'Fields', '', array('name')));

                $q->where(array(
                    "`{$classKey}`.`active`" => 1,
                    "`{$classKey}`.`language_id`" => $languageId,
                    "`Fields`.`active`" => 1,
                    "`Template`.`template_id`" => $templateId
                ));

                if ($q->prepare() && $q->stmt->execute()) {
                    while ($item = $q->stmt->fetch(PDO::FETCH_ASSOC)) {
                        $result[$item['name']] = $item;
                    }
                }
            }
            $this->modx->cacheManager->set($cacheKey, $result, $this->config['cacheTime']);
        }

        return $result;
    }

    /**
     * @param array $pattern
     * @param array $data
     *
     * @return array
     */
    public function prapareSeoPlaceholders(array $pattern, array $data = array())
    {
        $placeholders = array();
        if ($pattern) {
            foreach ($pattern as $key => $item) {
                $placeholders[$key] = $this->getPdoTools()->getChunk('@INLINE ' . $item['value'], $data, false);
            }
        }
        return $placeholders;
    }


    /**
     * @param int $patternId
     *
     * @return array
     */
    public function getSeoTemplateIds($patternId)
    {
        $result = array();
        $classKey = 'PolylangSeoPatternTemplate';
        $q = $this->modx->newQuery($classKey);
        $q->select($this->modx->getSelectColumns($classKey, $classKey, '', array('template_id')));

        $q->where(array(
            "`{$classKey}`.`pattern_id`" => $patternId
        ));


        if ($q->prepare() && $q->stmt->execute()) {
            $result = $q->stmt->fetchAll(PDO::FETCH_COLUMN);
        }
        return $result;
    }

    /**
     * @param int $patternId
     *
     * @return array
     */
    public function getSeoTemplates($patternId)
    {
        $result = array();
        $classKey = 'modTemplate';
        $ids = $this->getSeoTemplateIds($patternId);
        if ($ids) {
            $q = $this->modx->newQuery($classKey);
            $q->select($this->modx->getSelectColumns($classKey, $classKey));

            $q->where(array(
                "`{$classKey}`.`id`:IN" => $ids
            ));

            if ($q->prepare() && $q->stmt->execute()) {
                $result = $q->stmt->fetchAll(PDO::FETCH_ASSOC);
            }
        }
        return $result;
    }


    /**
     * @param string $language
     * @param string $topic
     *
     * @return array
     */
    public function getLexicons($language, $topic = 'site')
    {
        $data = array();
        /** @var modProcessorResponse $response */
        $response = $this->polylang->runProcessor('mgr/lexiconentries/getlist', array(
            'limit' => 10000,
            'topic' => $topic,
            'language' => $language,
        ));
        if ($response->isError()) {
            $this->modx->log(modX::LOG_LEVEL_ERROR, $response->getMessage());
        } else {
            $response = $this->fromJSON($response->getResponse(), array());
            if ($response && !empty($response['results'])) {
                $data = $response['results'];
            }
        }
        return $data;
    }

    /**
     * @param string $language
     * @param string $topic
     */
    public function createLexiconFile($language, $topic = 'site')
    {
        $path = $this->polylang->config['corePath'] . "lexicon/{$language}/";
        $file = $path . "{$topic}.inc.php";
        if (!file_exists($file)) {
            $content = "<?php\n";
            $this->modx->cacheManager->writeFile($file, $content);
        }
    }

    /**
     * @param string $language
     * @param string $topic
     */
    public function removeLexiconFile($language, $topic = 'site')
    {
        $path = $this->polylang->config['corePath'] . "lexicon/{$language}/";
        if (file_exists($path)) {
            $this->modx->cacheManager->deleteTree($path, array('deleteTop' => true, 'extensions' => ''));
            $q = $this->modx->newQuery('modLexiconEntry');
            $q->where(array(
                'namespace' => 'polylang',
                'topic' => $topic,
                'language' => $language,
            ));
            $entries = $this->modx->getCollection('modLexiconEntry', $q);
            /** @var modLexiconEntry $entry */
            foreach ($entries as $entry) {
                $entry->remove();
            }
            $this->modx->lexicon->clearCache();
        }

    }

    /**
     * @param int $id
     * @param string $context
     * @param string|array $args
     * @param int|string $scheme
     * @param array $options
     *
     * @return string
     */
    public function makeUrl(int $id, string $context = '', $args = '', $scheme = -1, array $options = array()): string
    {
        if (!$this->isCurrentDefaultLanguage()) {
            if ($language = $this->detectLanguage(true)) {
                return $language->makeUrl($id, $scheme, $args);
            }
        }
        return $this->modx->makeUrl($id, $context, $args, $scheme, $options);
    }


    /**
     * @param array $config
     *
     * @return pdoTools|null
     */
    public function getPdoTools($config = array())
    {
        if (!$this->hasAddition('pdotools')) return null;
        if (class_exists('pdoFetch') && (!isset($this->pdoTools) || !is_object($this->pdoTools))) {
            $this->pdoTools = $this->modx->getService('pdoFetch');
            $this->pdoTools->setConfig($config);
        }
        return empty($this->pdoTools) ? null : $this->pdoTools;


    }

    /**
     * @param array $config
     *
     * @return null|PolylangDbHelper
     */
    public function getDbHelper($config = array())
    {
        if (!$this->dbHelper || !is_object($this->dbHelper)) {
            if ($dbHelperClass = $this->modx->loadClass('tools.' . $this->config['dbHelperHandler'], $this->config['handlersPath'], true, true)) {
                $config = array_merge($this->config, $config);
                $this->dbHelper = new $dbHelperClass($this->modx, $config);
            } else {
                $this->modx->log(modX::LOG_LEVEL_ERROR, 'Could not load DbHelper class from');
            }
        }
        return $this->dbHelper;
    }


    /**
     * @param string $ctx
     * @param array $config
     *
     * @return  miniShop2|null
     */
    public function getMs2($ctx = '', $config = array())
    {
        if (!$this->hasAddition('minishop2')) return null;
        $ctx = $ctx ? $ctx : $this->modx->context->key;
        if (class_exists('miniShop2') && (!isset($this->ms2) || !is_object($this->ms2))) {
            $this->ms2 = $this->modx->getService('miniShop2');
            $this->ms2->initialize($ctx, $config);
        }

        return empty($this->ms2) ? null : $this->ms2;
    }

    /**
     * @param string $key
     * @param string $value
     * @param string $namespace
     * @param bool $clearCache
     *
     * @return bool
     */
    public function setOption($key, $value, $namespace = '', $clearCache = false)
    {
        if (empty(trim($key))) return false;

        $namespace = $namespace ? $namespace : $this->polylang->getNamespace();
        // $key = $namespace . '_' . $key;

        if (!$setting = $this->modx->getObject('modSystemSetting', $key)) {
            $setting = $this->modx->newObject('modSystemSetting');
            $setting->set('namespace', $namespace);
        }

        $val = is_array($value) ? $this->modx->toJSON($value) : $value;
        $setting->set('value', $val);

        if ($setting->save()) {
            $this->modx->setOption($key, $value);
            if ($clearCache) {
                $this->modx->cacheManager->refresh(array('system_settings' => array()));
            }
            return true;
        }
        return false;
    }

    /**
     * @param $str
     * @param string $default
     * @param bool $skipEmpty
     *
     * @return mixed|string
     */
    public function fromJSON($str, $default = '', $skipEmpty = true)
    {
        $val = $this->modx->fromJSON($str);
        if (($val === '' || $val === null) && $skipEmpty) {
            $val = $default;
        }
        return $val;
    }

    /**
     * Shorthand for original modX::invokeEvent() method with some useful additions.
     *
     * @param $eventName
     * @param array $params
     * @param $glue
     *
     * @return array
     */
    public function invokeEvent($eventName, array $params = array(), $glue = '<br/>')
    {
        if (isset($this->modx->event->returnedValues)) {
            $this->modx->event->returnedValues = null;
        }

        $response = $this->modx->invokeEvent($eventName, $params);
        if (is_array($response) && count($response) > 1) {
            foreach ($response as $k => $v) {
                if (empty($v)) {
                    unset($response[$k]);
                }
            }
        }

        $message = is_array($response) ? implode($glue, $response) : trim((string)$response);
        if (isset($this->modx->event->returnedValues) && is_array($this->modx->event->returnedValues)) {
            $params = array_merge($params, $this->modx->event->returnedValues);
        }

        return array(
            'success' => empty($message),
            'message' => $message,
            'data' => $params,
        );
    }

    /**
     * @param array|string $options
     *
     * @return string
     */
    public function getCacheKey($options)
    {
        return $this->polylang->getNamespace() . DIRECTORY_SEPARATOR . sha1(is_array($options) ? serialize($options) : $options);
    }

    /**
     * Sanitize the specified path
     *
     * @param string $path The path to clean
     *
     * @return string The sanitized path
     */
    public function normalizePath($path)
    {
        $path = str_replace('./', '/', $path);
        return preg_replace(array("/\.*[\/|\\\]/i", "/[\/|\\\]+/i"), array(DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR), $path);
    }

    /**
     * @param string $str
     *
     * @return string
     */
    public function unquote($str)
    {
        return str_replace(array("'", '"'), '', trim($str));
    }

    /**
     * @param string $str
     * @param string $delimiter
     *
     * @return array
     */
    public function explodeAndClean($str, $delimiter = ',')
    {
        if (empty($str)) return array();
        $array = explode($delimiter, $str);
        $array = array_map('trim', $array);
        $array = array_keys(array_flip($array));
        $array = array_filter($array);

        return $array;
    }


    /**
     * @param $array
     * @param string $delimiter
     *
     * @return string
     */
    public function cleanAndImplode($array, $delimiter = ',')
    {
        $array = array_map('trim', $array);
        $array = array_keys(array_flip($array));
        $array = array_filter($array);
        $array = implode($delimiter, $array);
        return $array;
    }

    /**
     * @param array $array
     *
     * @return array
     */
    public function cleanArray(array $array = array())
    {
        $array = array_map('trim', $array);
        $array = array_filter($array);
        $array = array_keys(array_flip($array));
        return $array;
    }

    /**
     * @param $needle
     * @param array $array
     * @param bool $all
     *
     * @return array
     */
    public function removeArrayByValue($needle, $array = array(), $all = true)
    {
        if (!$all) {
            if (FALSE !== $key = array_search($needle, $array)) unset($array[$key]);
            return $array;
        }
        foreach (array_keys($array, $needle) as $key) {
            unset($array[$key]);
        }
        return $array;
    }

    /**
     * @param array $arr
     *
     * @return string|null
     */
    public function getArrayFirstKey(array $arr)
    {
        $keys = array_keys($arr);
        return empty($keys) ? null : $keys[0];
    }

    /**
     * @param string $path
     * @param bool $normalize
     *
     * @return string
     */
    public function preparePath($path = '', $normalize = false)
    {
        $path = str_replace(array(
            '{base_path]',
            '{core_path}',
            '{assets_path}',
            '{assets_url}',
            '{mgr_path}',
            '{+core_path}',
            '{+assets_path}',
            '{+assets_url}',
        ), array(
            $this->modx->getOption('base_path', null, MODX_BASE_PATH),
            $this->modx->getOption('core_path', null, MODX_CORE_PATH),
            $this->modx->getOption('assets_path', null, MODX_ASSETS_PATH),
            $this->modx->getOption('assets_url', null, MODX_ASSETS_URL),
            $this->modx->getOption('mgr_path', null, MODX_MANAGER_PATH),
            $this->modx->getOption('core_path', null, MODX_CORE_PATH) . 'components/polylang/',
            $this->modx->getOption('assets_path', null, MODX_ASSETS_PATH) . 'components/polylang/',
            $this->modx->getOption('assets_url', null, MODX_ASSETS_PATH) . 'components/polylang/',
        ), $path);
        return $normalize ? $this->normalizePath($path) : $path;
    }

    /**
     * @param string $addition
     *
     * @return bool
     */
    public function hasAddition($addition = '')
    {
        $addition = strtolower($addition);
        return file_exists(MODX_CORE_PATH . 'components/' . $addition . '/model/' . $addition . '/');
    }

    /**
     * @param int $number
     *
     * @return float
     */
    public function formatNumber($number = 0, $ceil = false)
    {
        $number = str_replace(',', '.', $number);
        $number = (float)$number;

        if ($ceil) {
            $number = ceil($number / 10) * 10;
        }

        return round($number, 3);
    }


    /**
     * @param string|array $message
     * @param int $level
     */
    public function log($message, $level = modX::LOG_LEVEL_ERROR)
    {
        if (is_array($message)) {
            $message = print_r($message, 1);
        }
        $curLevel = $this->modx->getLogLevel();
        $this->modx->setLogLevel($level);
        $this->modx->log($level, $message);
        $this->modx->setLogLevel($curLevel);
    }

    /**
     * @param string|array $message
     */
    public function debug($message)
    {

        if (is_array($message)) {
            $message = print_r($message, 1);
        }
        $this->log($message, modX::LOG_LEVEL_DEBUG);
    }

    /**
     * @param string $code
     *
     * @return string
     */
    public function country2locale(string $code)
    {
        # http://wiki.openstreetmap.org/wiki/Nominatim/Country_Codes
        $arr = array(
            'ad' => 'ca',
            'ae' => 'ar',
            'af' => 'fa,ps',
            'ag' => 'en',
            'ai' => 'en',
            'al' => 'sq',
            'am' => 'hy',
            'an' => 'nl,en',
            'ao' => 'pt',
            'aq' => 'en',
            'ar' => 'es',
            'as' => 'en,sm',
            'at' => 'de',
            'au' => 'en',
            'aw' => 'nl,pap',
            'ax' => 'sv',
            'az' => 'az',
            'ba' => 'bs,hr,sr',
            'bb' => 'en',
            'bd' => 'bn',
            'be' => 'nl,fr,de',
            'bf' => 'fr',
            'bg' => 'bg',
            'bh' => 'ar',
            'bi' => 'fr',
            'bj' => 'fr',
            'bl' => 'fr',
            'bm' => 'en',
            'bn' => 'ms',
            'bo' => 'es,qu,ay',
            'br' => 'pt',
            'bq' => 'nl,en',
            'bs' => 'en',
            'bt' => 'dz',
            'bv' => 'no',
            'bw' => 'en,tn',
            'by' => 'be,ru',
            'bz' => 'en',
            'ca' => 'en,fr',
            'cc' => 'en',
            'cd' => 'fr',
            'cf' => 'fr',
            'cg' => 'fr',
            'ch' => 'de,fr,it,rm',
            'ci' => 'fr',
            'ck' => 'en,rar',
            'cl' => 'es',
            'cm' => 'fr,en',
            'cn' => 'zh',
            'co' => 'es',
            'cr' => 'es',
            'cu' => 'es',
            'cv' => 'pt',
            'cw' => 'nl',
            'cx' => 'en',
            'cy' => 'el,tr',
            'cz' => 'cs',
            'de' => 'de',
            'dj' => 'fr,ar,so',
            'dk' => 'da',
            'dm' => 'en',
            'do' => 'es',
            'dz' => 'ar',
            'ec' => 'es',
            'ee' => 'et',
            'eg' => 'ar',
            'eh' => 'ar,es,fr',
            'er' => 'ti,ar,en',
            'es' => 'es,ast,ca,eu,gl',
            'et' => 'am,om',
            'fi' => 'fi,sv,se',
            'fj' => 'en',
            'fk' => 'en',
            'fm' => 'en',
            'fo' => 'fo',
            'fr' => 'fr',
            'ga' => 'fr',
            'gb' => 'en,ga,cy,gd,kw',
            'gd' => 'en',
            'ge' => 'ka',
            'gf' => 'fr',
            'gg' => 'en',
            'gh' => 'en',
            'gi' => 'en',
            'gl' => 'kl,da',
            'gm' => 'en',
            'gn' => 'fr',
            'gp' => 'fr',
            'gq' => 'es,fr,pt',
            'gr' => 'el',
            'gs' => 'en',
            'gt' => 'es',
            'gu' => 'en,ch',
            'gw' => 'pt',
            'gy' => 'en',
            'hk' => 'zh,en',
            'hm' => 'en',
            'hn' => 'es',
            'hr' => 'hr',
            'ht' => 'fr,ht',
            'hu' => 'hu',
            'id' => 'id',
            'ie' => 'en,ga',
            'il' => 'he',
            'im' => 'en',
            'in' => 'hi,en',
            'io' => 'en',
            'iq' => 'ar,ku',
            'ir' => 'fa',
            'is' => 'is',
            'it' => 'it,de,fr',
            'je' => 'en',
            'jm' => 'en',
            'jo' => 'ar',
            'jp' => 'ja',
            'ke' => 'sw,en',
            'kg' => 'ky,ru',
            'kh' => 'km',
            'ki' => 'en',
            'km' => 'ar,fr',
            'kn' => 'en',
            'kp' => 'ko',
            'kr' => 'ko,en',
            'kw' => 'ar',
            'ky' => 'en',
            'kz' => 'kk,ru',
            'la' => 'lo',
            'lb' => 'ar,fr',
            'lc' => 'en',
            'li' => 'de',
            'lk' => 'si,ta',
            'lr' => 'en',
            'ls' => 'en,st',
            'lt' => 'lt',
            'lu' => 'lb,fr,de',
            'lv' => 'lv',
            'ly' => 'ar',
            'ma' => 'ar',
            'mc' => 'fr',
            'md' => 'ru,uk,ro',
            'me' => 'srp,sq,bs,hr,sr',
            'mf' => 'fr',
            'mg' => 'mg,fr',
            'mh' => 'en,mh',
            'mk' => 'mk',
            'ml' => 'fr',
            'mm' => 'my',
            'mn' => 'mn',
            'mo' => 'zh,en,pt',
            'mp' => 'ch',
            'mq' => 'fr',
            'mr' => 'ar,fr',
            'ms' => 'en',
            'mt' => 'mt,en',
            'mu' => 'mfe,fr,en',
            'mv' => 'dv',
            'mw' => 'en,ny',
            'mx' => 'es',
            'my' => 'ms,zh,en',
            'mz' => 'pt',
            'na' => 'en,sf,de',
            'nc' => 'fr',
            'ne' => 'fr',
            'nf' => 'en,pih',
            'ng' => 'en',
            'ni' => 'es',
            'nl' => 'nl',
            'no' => 'nb,nn,no,se',
            'np' => 'ne',
            'nr' => 'na,en',
            'nu' => 'niu,en',
            'nz' => 'en,mi',
            'om' => 'ar',
            'pa' => 'es',
            'pe' => 'es',
            'pf' => 'fr',
            'pg' => 'en,tpi,ho',
            'ph' => 'en,tl',
            'pk' => 'en,ur',
            'pl' => 'pl',
            'pm' => 'fr',
            'pn' => 'en,pih',
            'pr' => 'es,en',
            'ps' => 'ar,he',
            'pt' => 'pt',
            'pw' => 'en,pau,ja,sov,tox',
            'py' => 'es,gn',
            'qa' => 'ar',
            're' => 'fr',
            'ro' => 'ro',
            'rs' => 'sr',
            'ru' => 'ru',
            'rw' => 'rw,fr,en',
            'sa' => 'ar',
            'sb' => 'en',
            'sc' => 'fr,en,crs',
            'sd' => 'ar,en',
            'se' => 'sv',
            'sg' => 'en,ms,zh,ta',
            'sh' => 'en',
            'si' => 'sl',
            'sj' => 'no',
            'sk' => 'sk',
            'sl' => 'en',
            'sm' => 'it',
            'sn' => 'fr',
            'so' => 'so,ar',
            'sr' => 'nl',
            'st' => 'pt',
            'ss' => 'en',
            'sv' => 'es',
            'sx' => 'nl,en',
            'sy' => 'ar',
            'sz' => 'en,ss',
            'tc' => 'en',
            'td' => 'fr,ar',
            'tf' => 'fr',
            'tg' => 'fr',
            'th' => 'th',
            'tj' => 'tg,ru',
            'tk' => 'tkl,en,sm',
            'tl' => 'pt,tet',
            'tm' => 'tk',
            'tn' => 'ar',
            'to' => 'en',
            'tr' => 'tr',
            'tt' => 'en',
            'tv' => 'en',
            'tw' => 'zh',
            'tz' => 'sw,en',
            'ua' => 'uk',
            'ug' => 'en,sw',
            'um' => 'en',
            'us' => 'en,es',
            'uy' => 'es',
            'uz' => 'uz,kaa',
            'va' => 'it',
            'vc' => 'en',
            've' => 'es',
            'vg' => 'en',
            'vi' => 'en',
            'vn' => 'vi',
            'vu' => 'bi,en,fr',
            'wf' => 'fr',
            'ws' => 'sm,en',
            'ye' => 'ar',
            'yt' => 'fr',
            'za' => 'zu,xh,af,st,tn,en',
            'zm' => 'en',
            'zw' => 'en,sn,nd'
        );
        #----
        $code = strtolower($code);
        if ($code == 'eu') {
            return 'en_GB.utf-8';
        } elseif ($code == 'ap') { # Asia Pacific
            return 'en_US.utf-8';
        } elseif ($code == 'cs') {
            return 'sr_RS.utf-8';
        }
        #----
        if ($code == 'uk') {
            $code = 'gb';
        }
        if (array_key_exists($code, $arr)) {
            if (strpos($arr[$code], ',') !== false) {
                $new = explode(',', $arr[$code]);
                $loc = array();
                foreach ($new as $key => $val) {
                    $loc[] = $val . '_' . strtoupper($code) . '.utf-8';
                }
                return implode(',', $loc); # string; comma-separated values 'en_GB.utf-8,ga_GB.utf-8,cy_GB.utf-8,gd_GB.utf-8,kw_GB.utf-8'
            } else {
                return $arr[$code] . '_' . strtoupper($code) . '.utf-8'; # string 'en_US.utf-8'
            }
        }
        return 'en_US.utf-8';
    }

    /**
     * @param string $locale
     *
     * @return string
     */
    public function prapareLocale(string $locale)
    {
        return explode('.', $locale)[0];
    }

    /**
     * @param string $url
     *
     * @return bool|string
     */
    public function request($url)
    {
        $output = '';
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_TIMEOUT, 4);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_ENCODING, 'gzip, deflate');
        curl_setopt($ch, CURLOPT_HTTPHEADER,
            array(
                'User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome /69.0.3497.100 Safari /537.36',
                'Accept:text/html,application/xhtml + xml,application/xml;q = 0.9,image/webp,image/apng,*/*;q=0.8',
                'Accept-Language:ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7,uk;q=0.6',
            )
        );

        try {
            if (!$output = curl_exec($ch)) {
                $this->modx->log(modX::LOG_LEVEL_ERROR, "Error download. Url : {$url}. Message:\n" . curl_error($ch) . "\nError:\n" . curl_errno($ch) . "\noutput:\n {$output}");
                $this->modx->log(modX::LOG_LEVEL_ERROR, '$url=' . $url);
            }
        } catch (Exception $e) {
            $this->modx->log(modX::LOG_LEVEL_ERROR, $e->getMessage());
        }
        curl_close($ch);
        return $output;
    }

    /**
     * @param string $email
     * @param string $subject
     * @param string $body
     * @return bool
     */
    public function sendEmail($email, $subject, $body = '')
    {

        /** @var modPHPMailer $mail */
        $mail = $this->modx->getService('mail', 'mail.modPHPMailer');
        $mail->setHTML(true);

        $mail->address('to', trim($email));
        $mail->set(modMail::MAIL_SUBJECT, trim($subject));
        $mail->set(modMail::MAIL_BODY, $body);
        $mail->set(modMail::MAIL_FROM, $this->modx->getOption('emailsender'));
        $mail->set(modMail::MAIL_FROM_NAME, $this->modx->getOption('site_name'));
        if (!$send = $mail->send()) {
            $this->modx->log(modX::LOG_LEVEL_ERROR, 'An error occurred while trying to send the email: ' . $mail->mailer->ErrorInfo);
        }
        $mail->reset();
        return $send;
    }

    /**
     * @param string $flag
     * @return array|string
     */
    public function getClientIp($flag = 'ip')
    {
        $ip = '';
        $ipAll = array(); // networks IP
        $ipSus = array(); // suspected IP
        $serverVariables = array(
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_FORWARDED',
            'HTTP_X_CLUSTER_CLIENT_IP',
            'HTTP_X_COMING_FROM',
            'HTTP_FORWARDED_FOR',
            'HTTP_FORWARDED',
            'HTTP_COMING_FROM',
            'HTTP_CLIENT_IP',
            'HTTP_FROM',
            'HTTP_VIA',
            'REMOTE_ADDR',
        );

        foreach ($serverVariables as $serverVariable) {
            $value = '';
            if (isset($_SERVER[$serverVariable])) {
                $value = $_SERVER[$serverVariable];
            } elseif (getenv($serverVariable)) {
                $value = getenv($serverVariable);
            }

            if (!empty($value)) {
                $tmp = explode(',', $value);
                $ipSus[] = $tmp[0];
                $ipAll = array_merge($ipAll, $tmp);
            }
        }

        $ipSus = array_unique($ipSus);
        $ipAll = array_unique($ipAll);
        $ip = (sizeof($ipSus) > 0) ? $ipSus[0] : $ip;
        $result = array(
            'ip' => $ip,
            'suspected' => $ipSus,
            'network' => $ipAll,
        );
        if ($flag && isset($result[$flag])) {
            return $result[$flag];
        }
        return $result;
    }

    static public function prepareSearchTvName($name)
    {
        return str_ireplace('tv_', 'tv', $name);
    }

    /**
     * @param xPDO $xpdo
     * @param int $sessionWaitTimeout
     *
     * @return bool
     */
    public static function reconnect(xPDO &$xpdo, $sessionWaitTimeout = 0)
    {
        $xpdo->connection->pdo = null;
        if ($xpdo->connect(null, array(xPDO::OPT_CONN_MUTABLE => true))) {
            if ($sessionWaitTimeout) {
                self::setSessionWaitTimeout($xpdo, $sessionWaitTimeout);
            }
            return true;
        }
        return false;
    }

    /**
     * @param xPDO $xpdo
     * @param $time
     */
    public static function setSessionWaitTimeout(xPDO &$xpdo, $time)
    {
        if ($time <= 0) return;
        $xpdo->exec("set session wait_timeout={$time};");
        //$xpdo->exec("set session wait_timeout={$time}; set session interactive_timeout={$time};");
    }

    public function resetPdoFetchOptions()
    {
        if ($this->modx->getOption('pdoFetch.class') == 'PolylangFetch') {
            $this->setOption('pdoFetch.class', 'pdotools.pdofetch', 'pdotools');
            $this->setOption('pdoFetch.pdofetch_class_path', '{core_path}components/pdotools/model/', 'pdotools');
        }
    }

}