<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */
declare (strict_types=1);
namespace Packetery\Nette\Forms\Controls;

use Packetery\Nette;
use Packetery\Nette\Forms\Control;
use Packetery\Nette\Forms\Form;
use Packetery\Nette\Forms\Rules;
use Packetery\Nette\Utils\Html;
/**
 * Base class that implements the basic functionality common to form controls.
 *
 * @property-read Form $form
 * @property-read string $htmlName
 * @property   mixed $htmlId
 * @property   mixed $value
 * @property   string|object $caption
 * @property   bool $disabled
 * @property   bool $omitted
 * @property-read Html $control
 * @property-read Html $label
 * @property-read Html $controlPrototype
 * @property-read Html $labelPrototype
 * @property   bool $required
 * @property-read bool $filled
 * @property-read array $errors
 * @property-read array $options
 * @property-read string $error
 */
abstract class BaseControl extends \Packetery\Nette\ComponentModel\Component implements Control
{
    /** @var string */
    public static $idMask = 'frm-%s';
    /** @var mixed current control value */
    protected $value;
    /** @var Html  control element template */
    protected $control;
    /** @var Html  label element template */
    protected $label;
    /** @var bool|bool[] */
    protected $disabled = \false;
    /** @var callable[][]  extension methods */
    private static $extMethods = [];
    /** @var string|object textual caption or label */
    private $caption;
    /** @var array */
    private $errors = [];
    /** @var bool|null */
    private $omitted;
    /** @var Rules */
    private $rules;
    /** @var \Packetery\Nette\Localization\Translator|bool|null */
    private $translator = \true;
    // means autodetect
    /** @var array user options */
    private $options = [];
    /**
     * @param  string|object  $caption
     */
    public function __construct($caption = null)
    {
        $this->control = Html::el('input', ['type' => null, 'name' => null]);
        $this->label = Html::el('label');
        $this->caption = $caption;
        $this->rules = new Rules($this);
        $this->setValue(null);
        $this->monitor(Form::class, function (Form $form) : void {
            if (!$this->isDisabled() && $form->isAnchored() && $form->isSubmitted()) {
                $this->loadHttpData();
            }
        });
    }
    /**
     * Sets textual caption or label.
     * @param object|string  $caption
     * @return static
     */
    public function setCaption($caption)
    {
        $this->caption = $caption;
        return $this;
    }
    /** @return object|string */
    public function getCaption()
    {
        return $this->caption;
    }
    /**
     * Returns form.
     */
    public function getForm(bool $throw = \true) : ?Form
    {
        return $this->lookup(Form::class, $throw);
    }
    /**
     * Loads HTTP data.
     */
    public function loadHttpData() : void
    {
        $this->setValue($this->getHttpData(Form::DATA_TEXT));
    }
    /**
     * Loads HTTP data.
     * @return mixed
     */
    protected function getHttpData($type, string $htmlTail = null)
    {
        return $this->getForm()->getHttpData($type, $this->getHtmlName() . $htmlTail);
    }
    /**
     * Returns HTML name of control.
     */
    public function getHtmlName() : string
    {
        return $this->control->name ?? \Packetery\Nette\Forms\Helpers::generateHtmlName($this->lookupPath(Form::class));
    }
    /********************* interface Control ****************d*g**/
    /**
     * Sets control's value.
     * @return static
     * @internal
     */
    public function setValue($value)
    {
        $this->value = $value;
        return $this;
    }
    /**
     * Returns control's value.
     * @return mixed
     */
    public function getValue()
    {
        return $this->value;
    }
    /**
     * Is control filled?
     */
    public function isFilled() : bool
    {
        $value = $this->getValue();
        return $value !== null && $value !== [] && $value !== '';
    }
    /**
     * Sets control's default value.
     * @return static
     */
    public function setDefaultValue($value)
    {
        $form = $this->getForm(\false);
        if ($this->isDisabled() || !$form || !$form->isAnchored() || !$form->isSubmitted()) {
            $this->setValue($value);
        }
        return $this;
    }
    /**
     * Disables or enables control.
     * @param  bool  $value
     * @return static
     */
    public function setDisabled($value = \true)
    {
        if ($this->disabled = (bool) $value) {
            $this->setValue(null);
        } elseif (($form = $this->getForm(\false)) && $form->isAnchored() && $form->isSubmitted()) {
            $this->loadHttpData();
        }
        return $this;
    }
    /**
     * Is control disabled?
     */
    public function isDisabled() : bool
    {
        return $this->disabled === \true;
    }
    /**
     * Sets whether control value is excluded from $form->getValues() result.
     * @return static
     */
    public function setOmitted(bool $value = \true)
    {
        $this->omitted = $value;
        return $this;
    }
    /**
     * Is control value excluded from $form->getValues() result?
     */
    public function isOmitted() : bool
    {
        return $this->omitted || $this->isDisabled() && $this->omitted === null;
    }
    /********************* rendering ****************d*g**/
    /**
     * Generates control's HTML element.
     * @return Html|string
     */
    public function getControl()
    {
        $this->setOption('rendered', \true);
        $el = clone $this->control;
        return $el->addAttributes(['name' => $this->getHtmlName(), 'id' => $this->getHtmlId(), 'required' => $this->isRequired(), 'disabled' => $this->isDisabled(), 'data-nette-rules' => \Packetery\Nette\Forms\Helpers::exportRules($this->rules) ?: null]);
    }
    /**
     * Generates label's HTML element.
     * @param  string|object  $caption
     * @return Html|string|null
     */
    public function getLabel($caption = null)
    {
        $label = clone $this->label;
        $label->for = $this->getHtmlId();
        $caption = $caption ?? $this->caption;
        $translator = $this->getForm()->getTranslator();
        $label->setText($translator && !$caption instanceof \Packetery\Nette\HtmlStringable ? $translator->translate($caption) : $caption);
        return $label;
    }
    public function getControlPart() : ?Html
    {
        return $this->getControl();
    }
    public function getLabelPart() : ?Html
    {
        return $this->getLabel();
    }
    /**
     * Returns control's HTML element template.
     */
    public function getControlPrototype() : Html
    {
        return $this->control;
    }
    /**
     * Returns label's HTML element template.
     */
    public function getLabelPrototype() : Html
    {
        return $this->label;
    }
    /**
     * Changes control's HTML id.
     * @param  string|bool|null  $id
     * @return static
     */
    public function setHtmlId($id)
    {
        $this->control->id = $id;
        return $this;
    }
    /**
     * Returns control's HTML id.
     * @return mixed
     */
    public function getHtmlId()
    {
        if (!isset($this->control->id)) {
            $form = $this->getForm();
            $prefix = $form instanceof \Packetery\Nette\Application\UI\Form || $form->getName() === null ? '' : $form->getName() . '-';
            $this->control->id = \sprintf(self::$idMask, $prefix . $this->lookupPath());
        }
        return $this->control->id;
    }
    /**
     * Changes control's HTML attribute.
     * @return static
     */
    public function setHtmlAttribute(string $name, $value = \true)
    {
        $this->control->{$name} = $value;
        if ($name === 'name' && ($form = $this->getForm(\false)) && !$this->isDisabled() && $form->isAnchored() && $form->isSubmitted()) {
            $this->loadHttpData();
        }
        return $this;
    }
    /**
     * @deprecated  use setHtmlAttribute()
     * @return static
     */
    public function setAttribute(string $name, $value = \true)
    {
        return $this->setHtmlAttribute($name, $value);
    }
    /********************* translator ****************d*g**/
    /**
     * Sets translate adapter.
     * @return static
     */
    public function setTranslator(?Nette\Localization\Translator $translator)
    {
        $this->translator = $translator;
        return $this;
    }
    /**
     * Returns translate adapter.
     */
    public function getTranslator() : ?Nette\Localization\Translator
    {
        if ($this->translator === \true) {
            return $this->getForm(\false) ? $this->getForm()->getTranslator() : null;
        }
        return $this->translator;
    }
    /**
     * Returns translated string.
     * @return mixed
     */
    public function translate($value, ...$parameters)
    {
        if ($translator = $this->getTranslator()) {
            $tmp = \is_array($value) ? [&$value] : [[&$value]];
            foreach ($tmp[0] as &$v) {
                if ($v != null && !$v instanceof \Packetery\Nette\HtmlStringable) {
                    // intentionally ==
                    $v = $translator->translate($v, ...$parameters);
                }
            }
        }
        return $value;
    }
    /********************* rules ****************d*g**/
    /**
     * Adds a validation rule.
     * @param  callable|string  $validator
     * @param  string|object  $errorMessage
     * @return static
     */
    public function addRule($validator, $errorMessage = null, $arg = null)
    {
        $this->rules->addRule($validator, $errorMessage, $arg);
        return $this;
    }
    /**
     * Adds a validation condition a returns new branch.
     */
    public function addCondition($validator, $value = null) : Rules
    {
        return $this->rules->addCondition($validator, $value);
    }
    /**
     * Adds a validation condition based on another control a returns new branch.
     */
    public function addConditionOn(Control $control, $validator, $value = null) : Rules
    {
        return $this->rules->addConditionOn($control, $validator, $value);
    }
    /**
     * Adds a input filter callback.
     * @return static
     */
    public function addFilter(callable $filter)
    {
        $this->getRules()->addFilter($filter);
        return $this;
    }
    public function getRules() : Rules
    {
        return $this->rules;
    }
    /**
     * Makes control mandatory.
     * @param  bool|string|object  $value
     * @return static
     */
    public function setRequired($value = \true)
    {
        $this->rules->setRequired($value);
        return $this;
    }
    /**
     * Is control mandatory?
     */
    public function isRequired() : bool
    {
        return $this->rules->isRequired();
    }
    /**
     * Performs the server side validation.
     */
    public function validate() : void
    {
        if ($this->isDisabled()) {
            return;
        }
        $this->cleanErrors();
        $this->rules->validate();
    }
    /**
     * Adds error message to the list.
     * @param  string|object  $message
     */
    public function addError($message, bool $translate = \true) : void
    {
        $this->errors[] = $translate ? $this->translate($message) : $message;
    }
    /**
     * Returns errors corresponding to control.
     */
    public function getError() : ?string
    {
        return $this->errors ? \implode(' ', \array_unique($this->errors)) : null;
    }
    /**
     * Returns errors corresponding to control.
     */
    public function getErrors() : array
    {
        return \array_unique($this->errors);
    }
    public function hasErrors() : bool
    {
        return (bool) $this->errors;
    }
    public function cleanErrors() : void
    {
        $this->errors = [];
    }
    /********************* user data ****************d*g**/
    /**
     * Sets user-specific option.
     * @return static
     */
    public function setOption($key, $value)
    {
        if ($value === null) {
            unset($this->options[$key]);
        } else {
            $this->options[$key] = $value;
        }
        return $this;
    }
    /**
     * Returns user-specific option.
     * @return mixed
     */
    public function getOption($key, $default = null)
    {
        return $this->options[$key] ?? $default;
    }
    /**
     * Returns user-specific options.
     */
    public function getOptions() : array
    {
        return $this->options;
    }
    /********************* extension methods ****************d*g**/
    public function __call(string $name, array $args)
    {
        $class = static::class;
        do {
            if (isset(self::$extMethods[$name][$class])) {
                return self::$extMethods[$name][$class]($this, ...$args);
            }
            $class = \get_parent_class($class);
        } while ($class);
        return parent::__call($name, $args);
    }
    public static function extensionMethod(
        string $name,
        /*callable*/
        $callback
    ) : void
    {
        if (\strpos($name, '::') !== \false) {
            // back compatibility
            [, $name] = \explode('::', $name);
        }
        self::$extMethods[$name][static::class] = $callback;
    }
}
