<?php
/**
 * Базовый класс для работы всех полей форм CMS
 */
#[\AllowDynamicProperties]
class cmsFormField {

    /**
     * Префикс названия ячейки БД поля для кэширования
     */
    const FIELD_CACHE_POSTFIX = '_cache';

    /**
     * Имя поля, как было задано в форме
     * @var string
     */
    public $name = '';
    /**
     * Имя поля, как должно быть в HTML теге
     * @var string
     */
    public $element_name = '';
    /**
     * Класс CSS для родительского элемента поля
     * @var string
     */
    public $css_class = '';
    /**
     * Тип фильтрации для поля
     * true, false, int, str или date
     * @var string | boolean
     */
    public $filter_type = false;
    /**
     * Краткое описания поля фильтрации для простых фильтров в админке
     * @var string
     */
    public $filter_hint  = false;
    /**
     * Название поля
     * @var string
     */
    public $title = '';
    public $element_title = '';
    /**
     * Флаг, указывающий, что поле может быть использовано для создания в полях типов контента, конструкторе форм и т.п.
     * @var boolean
     */
    public $is_public = true;
    /**
     * Последняя часть строки SQL запроса для создания поля в базе данных
     * @var string
     */
    public $sql = '';
    /**
     * Последняя часть строки SQL запроса для создания поля в базе данных, в котором будет храниться кэшированное значение
     * @var string
     */
    public $cache_sql = '';
    /**
     * Флаг, указывающий, что при создании поля в базе данных (например, при добавлении поля в типах контента)
     * необходимо также добавить SQL индекс к этому полю
     * @var boolean
     */
    public $allow_index = true;
    /**
     * Флаг, указывающий, что нам нужна денормализация данных, полученных из поля формы
     * @var boolean
     */
    public $is_denormalization = false;
    /**
     * Массив записи, в которой это поле используется
     * @var array
     */
    public $item = [];
    /**
     * Массив записи списка (например категория), в котором это поле используется
     * @var array
     */
    protected $item_list = [];
    /**
     * Контекст запроса
     * Устанавливается в методе parse объекта класса cmsForm
     * @var object
     */
    public $request = null;
    /**
     * ID поля, если запись о нём есть в таблице
     * @var integer
     */
    public $field_id = 0;
    /**
     * Флаг, что поле виртуальное
     * @var boolean
     */
    public $is_virtual = false;
    /**
     * Флаг скрытого поля type="hidden"
     * @var boolean
     */
    public $is_hidden = false;
    /**
     * Флаг видимости поля при помощи display:none
     * @var boolean
     */
    public $is_visible = true;
    /**
     * Массив правил валидации
     * @var array
     */
    public $rules = [];
    /**
     * Значение поля из массива конвертировать в json
     * @var boolean
     */
    public $store_array_as_json = false;
    /**
     * Флаг, что выводится тизер поля
     * @var boolean
     */
    public $is_parse_teaser = false;
    /**
     * Массив опций поля
     * @var array
     */
    public $options = [];
    protected $default_options_loaded = false;

    /**
     * Тип переменной поля
     * boolean | integer | double | string | array | object | resource
     * если получаемые значения от поля типизированы (всегда одного типа)
     * указывайте это свойство в своем классе поля
     * @var string
     */
    public $var_type = null;
    /**
     * Массив для данных в шаблоне
     * @var array
     */
    public $data = [];

    /**
     * Зависимости показа (видимости) поля
     * @var arra
     */
    public $visible_depend = [];

    /**
     * Исключение поля в контроллерах
     * @var array
     */
    public $excluded_controllers = [];

    public $context = null;
    public $context_params = [];

    /**
     * Метод для вывода поля в форме
     * @var string
     */
    public $display_input = 'getInput';

    /**
     * Показывать заголовок поля
     * при выводе поля для фильтра
     *
     * @var boolean
     */
    public $show_filter_input_title = false;

    /**
     * Формировать поле формы на нескольких языках
     *
     * @var boolean
     */
    public $multilanguage = false;

    /**
     * Поле может быть доступно на нескольких языках
     * как минимум контроллер languages ориентируется на
     * этот флаг
     *
     * @var boolean
     */
    public $can_multilanguage = false;

    /**
     * Язык поля (для мультиязычности)
     *
     * @var string
     */
    public $lang = '';

    /**
     * Параметры мультиязычности поля
     *
     * @var array
     */
    public $multilanguage_params = [
        // Флаг, что поле в таблице
        // При этом автоматически создадутся языковые колонки
        'is_table_field' => null,
        // Если флаг выше включен, в этой таблице создадутся языковые колонки
        'table' => null,
        // Убирать правило required с поля
        'unset_required' => null
    ];

    /**
     * Тип поля
     * по факту имя файла
     *
     * @var string
     */
    public $field_type = '';

    /**
     * Название субъекта использования поля
     * Задаётся полю при рендере опций поля в админке
     *
     * @var string
     */
    public $subject_name = '';

    /**
     * Подключать языковой файл для поля?
     *
     * @var boolean
     */
    protected $use_language = false;

    /**
     * Последняя ошибка поля
     * @var mixed
     */
    protected $last_error;

    /**
     * @param string $name Имя поля
     * @param array $options Массив опций
     */
    public function __construct($name, $options = false) {

        $this->lang = cmsConfig::get('language');

        if($name){
            $this->setName($name);
        }

        $this->field_type = substr(mb_strtolower(get_called_class()), 5);
        $this->class = $this->field_type;

        if($this->use_language){
            cmsCore::loadFieldLanguage($this->field_type);
        }

        if ($options) {
            $this->setOptions($options);
        }
    }

    /**
     * Возвращает последнюю ошибку поля
     *
     * @return mixed
     */
    public function getError() {
        return $this->last_error;
    }

    /**
     * Устанавливает ошибку поля
     *
     * @param mixed $last_error
     * @return $last_error
     */
    public function setError($last_error) {
        $this->last_error = $last_error;
        return $last_error;
    }

    /**
     * Возвращает свойство поля по названию
     * @param string $key Имя свойства
     * @return mixed
     */
    public function getProperty($key){
        return isset($this->{$key}) ? $this->{$key} : false;
    }

    /**
     * Устанавливает свойство поля
     * @param string $key Имя свойства
     * @param mixed $value Присваиваемое значение
     * @return $this
     */
    public function setProperty($key, $value) { $this->{$key} = $value; return $this; }

    /**
     * Возвращает опции поля
     * Опции - это объекты полей опций
     *
     * @return array
     */
    public function getOptions() { return []; }

    public function getOptionsExtended() {

        $ext_options = [];

        $options = $this->getOptions();

        if($options){
            foreach ($options as $option_field) {
                if(!empty($option_field->extended_option)){
                    $ext_options[] = $option_field;
                }
            }
        }

        return $ext_options;
    }

    /**
     * Возвращает значение опции поля
     * @param string $key Имя опции
     * @param mixed $default Значение по умолчанию
     * @return mixed
     */
    public function getOption($key, $default = null) {

        if (array_key_exists($key, $this->options)) {

            if(!$this->options[$key] && $default !== null){
                return $default;
            }

            return $this->options[$key];
        }

        if ($this->default_options_loaded !== true) {

            $options = $this->getOptions();

            $field_options = [];

            foreach ($options as $field) {
                $field_options[$field->getName()] = $field->getDefaultValue();
            }

            $this->options = array_merge($field_options, $this->options);

            $this->default_options_loaded = true;

            return $this->getOption($key, $default);

        } else {
            return $default;
        }
    }

    /**
     * Устанавливает все параметры для поля
     * опции и свойства
     *
     * @param array $options
     */
    public function setOptions($options) {
        if (is_array($options)) {
            foreach ($options as $option => $value) {
                if ($option === 'id') {
                    $this->field_id = $value;
                    continue;
                }
                $this->{$option} = $value;
            }
            $show_title = true;
            if (array_key_exists('show_title', ($options['options'] ?? []))) {
                $show_title = $options['options']['show_title'];
            }
            if (isset($options['title']) && $show_title) {
                $this->element_title = $options['title'];
            }
        }
    }

    /**
     * Устанавливает значение опции
     * @param string $key Название опции
     * @param mixed $value Значение опции
     * @return $this
     */
    public function setOption($key, $value) { $this->options[$key] = $value; return $this; }

    public function setContext($value){$this->context = $value; return $this;}
    /**
     * Возвращает название поля
     * @return string
     */
    public function getTitle(){ return $this->title; }

    /**
     * Возвращает имя поля
     * @return string
     */
    public function getName() { return $this->name; }

    /**
     * Возвращает имя поля в случае денормализации при сохранении значения от поля
     * @return string
     */
    public function getDenormalName() { return $this->name.self::FIELD_CACHE_POSTFIX; }

    /**
     * Устанавливает имя поля и имя HTML элемента поля
     * @param string $name Имя поля
     * @return $this
     */
    public function setName($name) {

        $this->name = $name;

        $keys = explode(':', $name);
        $this->element_name = count($keys) > 1 ? array_shift($keys) . '[' . implode('][', $keys) . ']' : $name;

        $this->id = str_replace([':', '|'], '_', $name);

        return $this;
    }

    /**
     * Возвращает имя HTML элемента поля
     * @return string
     */
    public function getElementName() { return $this->element_name; }

    /**
     * Устанавливает данные текущей записи поля
     * @param array $item
     * @return $this
     */
    public function setItem($item) {

        $this->item = $item;

        return $this;
    }

    /**
     * Устанавливает данные текущего списка,
     * В записях которого это поле
     *
     * @param array $item
     * @return $this
     */
    public function setItemList($item) {

        $this->item_list = $item;

        return $this;
    }

    /**
     * Возвращает последнюю часть строки SQL запроса
     * для создания поля в базе данных,
     * в котором будет храниться кэшированное значение
     *
     * @return string
     */
    public function getCacheSQL() { return $this->cache_sql; }

    /**
     * Возвращает последнюю часть строки SQL запроса
     * для создания поля в базе данных
     *
     * @return string
     */
    public function getSQL() {

        $max_length = $this->getOption('max_length');

        if ($max_length) {
            return str_replace('{max_length}', (string)$max_length, $this->sql);
        }

        return $this->sql;
    }

    /**
     * Возвращает массив правил валидации поля
     * @return array
     */
    public function getRules(){ return $this->rules; }

    /**
     * Возвращает булево значение наличия умолчания у поля
     * @return boolean
     */
    public function hasDefaultValue() {

        $default_lang_key = 'default_' . $this->lang;

        return isset($this->default) || isset($this->{$default_lang_key});
    }

    /**
     * Возвращает значение по умолчанию у поля
     * @return mixed
     */
    public function getDefaultValue() {

        if (!$this->multilanguage) {
            return $this->default ?? null;
        }

        $default_lang_key = 'default_' . $this->lang;

        return $this->{$default_lang_key} ?? ($this->default ?? null);
    }

    /**
     * Возвращает тип переменной для поля
     * @param bool $is_filter Указывает, что нам нужен тип при использовании в фильтре
     * @return string|null
     */
    public function getDefaultVarType($is_filter = false) {

        if (is_string($this->var_type)) {
            return $this->var_type;
        }

        $default_value = $this->getDefaultValue();

        if ($default_value === null) {
            return null;
        }

        return gettype($default_value);
    }

    /**
     * Возвращает HTML код поля в форме
     * @param mixed $value Текущее значение поля
     * @return string
     */
    public function getInput($value) {
        $this->title = $this->element_title;
        return cmsTemplate::getInstance()->renderFormField($this->class, [
            'field' => $this,
            'value' => $value
        ]);
    }

    /**
     * Возвращает HTML код поля в форме фильтра
     * @param mixed $value Текущее значение поля
     * @return string
     */
    public function getFilterInput($value){

        if(!$this->show_filter_input_title){
            $this->element_title = false;
        }

        // при фильтрации все поля необязательны
        $required_key = array_search(['required'], $this->getRules());
        if($required_key !== false){
            unset($this->rules[$required_key]);
        }
        return $this->getInput($value);
    }

    /**
     * Возвращает отформатированное значение поля для показа в записи
     * @param mixed $value Значение поля
     * @return string
     */
    public function parse($value){ return ''; }

    /**
     * Возвращает отформатированное значение поля для показа в списке записей
     * @param mixed $value Значение поля
     * @return string
     */
    public function parseTeaser($value) {

        $this->is_parse_teaser = true;

        return $this->parse($value);
    }

    /**
     * Выполняет некие действия после отработки метода parse
     * для всех полей одной записи
     * @param mixed $value Значение уже отформатированного поля
     * @param array $item Массив полей записи, с уже обработанными данными
     * @return mixed
     */
    public function afterParse($value, $item){ return $value; }

    /**
     * Выполняет некие действия над массивом записи
     * после всех обработок
     *
     * @param array $item
     * @param array $fields
     * @return array
     */
    public function hookItem($item, $fields){ return $item; }

    /**
     * Если значение вашего поля предполагает тип, отличный от типа «string» или же
     * значение поля формируется как некий HTML код, то этот метод должен вернуть
     * как минимум строковое представление значения.
     * Метод используется при автоматическом формировании
     * SEO параметров по шаблонам: slug (URL страницы), title, keywords, description.
     *
     * @param mixed $value Значение поля
     * @return string
     */
    public function getStringValue($value){ return $this->parse($value); }

    /**
     * Метод, который вызывается при применении фильтра списка записей,
     * например, при работе фильтра списка записей типов контента,
     * списка пользователей, групп.
     * В методе необходимо реализовать нужную фильтрацию для этого поля,
     * пользуясь объектом модели и значением из формы.
     * Вернуть метод должен объект $model.
     *
     * @param object $model Объект модели из контекста вызова
     * @param mixed $value Значение поля
     * @return boolean | $model
     */
    public function applyFilter($model, $value) { return false; }

    /**
     * Если поле работает с файлами
     * Возвращает массив путей файлов от корня CMS
     *
     * @param mixed $value
     * @return boolean | array
     */
    public function getFiles($value){
        return false;
    }

    /**
     * Метод, который подготавливает входную переменную
     * из поля для записи в базу данных
     *
     * @param mixed $value Значение поля из формы
     * @param boolean $is_submitted Форма отправлена?
     * @param mixed $old_value Предыдущее значение поля
     * @return string | array
     */
    public function store($value, $is_submitted, $old_value = null) {
        if ($this->store_array_as_json && is_array($value)) {
            return cmsModel::arrayToString($value);
        }
        return $value;
    }

    /**
     * Подготавливает входную переменную
     * из поля фильтра
     *
     * @param mixed $value Значение поля из формы фильтра
     * @return mixed
     */
    public function storeFilter($value){
        return $value;
    }

    /**
     * Метод, аналогичный store, но должен вернуть строку,
     * которая будет использоваться для денормализации значения
     *
     * @param mixed $value
     * @return string
     */
    public function storeCachedValue($value){
        return null;
    }

    /**
     * Метод вызывается после сохранения записей типов контента, профилей и групп
     *
     * @param array $item Полный массив записи, в которой есть текущее поле
     * @param object $model Объект модели из контекста вызова
     * @param string $action Действие - add или edit
     * @return void
     */
    public function afterStore($item, $model, $action){
        return;
    }

    /**
     * Метод, который вызывается при удалении записи
     *
     * @param mixed $value Значение поля
     * @return boolean
     */
    public function delete($value){
        return true;
    }

    /**
     * Метод вызывается после создания поля в админке,
     * например в типах контента, профилях или группах
     *
     * @param string $content_table_name Название таблицы, для которой поле создаётся
     * @param array $field Массив данных поля
     * @param object $model Объект модели контроллера контекста работы
     * @return $this
     */
    public function hookAfterAdd($content_table_name, $field, $model){
        return $this;
    }

    /**
     * Метод вызывается после редактирования поля в админке,
     * например в типах контента, профилях или группах
     *
     * @param string $content_table_name Название таблицы, для которой поле редактируется
     * @param array $field Новый массив данных поля
     * @param array $field_old Старый (до редактирования) массив данных поля
     * @param object $model объект модели контроллера контекста работы
     * @return $this
     */
    public function hookAfterUpdate($content_table_name, $field, $field_old, $model){
        return $this;
    }

    /**
     * Метод вызывается после удаления поля в админке,
     * например в типах контента, профилях или группах
     *
     * @param string $content_table_name Название таблицы, для которой поле было создано
     * @param array $field Массив данных поля
     * @param object $model Объект модели контроллера контекста работы
     * @return $this
     */
    public function hookAfterRemove($content_table_name, $field, $model){
        return $this;
    }

    /*
     * Метод для получения списка в полях
     */
    public function getListItems(){

        $items = [];

        if (isset($this->items)){

            $items = $this->items;

        } else if (isset($this->generator)) {

            $generator = $this->generator;
            $items = $this->items = $generator($this->item, $this->request, $this);

        } else if ($this->hasDefaultValue()) {

            $this->items = (!empty($this->show_empty_value) ? ['' => ''] : [])  + string_explode_list($this->getDefaultValue());

            $list_sorting = $this->getOption('list_sorting', '');

            switch ($list_sorting) {
                case 'keys':
                    ksort($this->items, SORT_NATURAL);
                    break;
                case 'values':
                    asort($this->items, SORT_NATURAL);
                    break;
                default:
                    break;
            }

            $items = $this->items;

        } else if($this->getOption('list_where') === 'table'){

            $list_table = $this->getOption('list_table');

            if($list_table){

                $model = new cmsModel();

                $list_order = $this->getOption('list_order');
                if($list_order){

                    $ordering = explode(':', $list_order);

                    $model->orderBy($ordering[0], (!empty($ordering[1]) ? $ordering[1] : 'asc'));
                }

                $list_where_cond = $this->getOption('list_where_cond');
                if($list_where_cond){
                    $list_where_cond = json_decode($list_where_cond, true);
                    if(is_array($list_where_cond)){
                        $model->applyDatasetFilters(['filters' => $list_where_cond]);
                    }
                }

                $model->limit(false);

                $this->items = $model->selectTranslatedField($this->getOption('list_where_title'), $list_table)->
                        get($list_table, function ($item, $model){
                    return $item[$this->getOption('list_where_title')];
                }, $this->getOption('list_where_id')) ?: [];

                if(!$list_order){
                    ksort($this->items, SORT_NATURAL);
                }

                if (!empty($this->show_empty_value)) {
                    $this->items = ['' => ''] + $this->items;
                }

                $items = $this->items;
            }
        }

        return $items;
    }

}
