<?php

namespace dokuwiki\Form;

use dokuwiki\Extension\Event;

/**
 * Class Form
 *
 * Represents the whole Form. This is what you work on, and add Elements to
 *
 * @package dokuwiki\Form
 */
class Form extends Element
{
    /**
     * @var array name value pairs for hidden values
     */
    protected $hidden = [];

    /**
     * @var Element[] the elements of the form
     */
    protected $elements = [];

    /**
     * Creates a new, empty form with some default attributes
     *
     * @param array $attributes
     * @param bool  $unsafe     if true, then the security token is ommited
     */
    public function __construct($attributes = [], $unsafe = false)
    {
        global $ID;

        parent::__construct('form', $attributes);

        // use the current URL as default action
        if (!$this->attr('action')) {
            $get = $_GET;
            if (isset($get['id'])) unset($get['id']);
            $self = wl($ID, $get, false, '&'); //attributes are escaped later
            $this->attr('action', $self);
        }

        // post is default
        if (!$this->attr('method')) {
            $this->attr('method', 'post');
        }

        // we like UTF-8
        if (!$this->attr('accept-charset')) {
            $this->attr('accept-charset', 'utf-8');
        }

        // add the security token by default
        if (!$unsafe) {
            $this->setHiddenField('sectok', getSecurityToken());
        }

        // identify this as a new form based form in HTML
        $this->addClass('doku_form');
    }

    /**
     * Sets a hidden field
     *
     * @param string $name
     * @param string $value
     * @return $this
     */
    public function setHiddenField($name, $value)
    {
        $this->hidden[$name] = $value;
        return $this;
    }

    #region element query function

    /**
     * Returns the numbers of elements in the form
     *
     * @return int
     */
    public function elementCount()
    {
        return count($this->elements);
    }

    /**
     * Get the position of the element in the form or false if it is not in the form
     *
     * Warning: This function may return Boolean FALSE, but may also return a non-Boolean value which evaluates
     * to FALSE. Please read the section on Booleans for more information. Use the === operator for testing the
     * return value of this function.
     *
     * @param Element $element
     *
     * @return false|int
     */
    public function getElementPosition(Element $element)
    {
        return array_search($element, $this->elements, true);
    }

    /**
     * Returns a reference to the element at a position.
     * A position out-of-bounds will return either the
     * first (underflow) or last (overflow) element.
     *
     * @param int $pos
     * @return Element
     */
    public function getElementAt($pos)
    {
        if ($pos < 0) $pos = count($this->elements) + $pos;
        if ($pos < 0) $pos = 0;
        if ($pos >= count($this->elements)) $pos = count($this->elements) - 1;
        return $this->elements[$pos];
    }

    /**
     * Gets the position of the first of a type of element
     *
     * @param string $type Element type to look for.
     * @param int $offset search from this position onward
     * @return false|int position of element if found, otherwise false
     */
    public function findPositionByType($type, $offset = 0)
    {
        $len = $this->elementCount();
        for ($pos = $offset; $pos < $len; $pos++) {
            if ($this->elements[$pos]->getType() == $type) {
                return $pos;
            }
        }
        return false;
    }

    /**
     * Gets the position of the first element matching the attribute
     *
     * @param string $name Name of the attribute
     * @param string $value Value the attribute should have
     * @param int $offset search from this position onward
     * @return false|int position of element if found, otherwise false
     */
    public function findPositionByAttribute($name, $value, $offset = 0)
    {
        $len = $this->elementCount();
        for ($pos = $offset; $pos < $len; $pos++) {
            if ($this->elements[$pos]->attr($name) == $value) {
                return $pos;
            }
        }
        return false;
    }

    #endregion

    #region Element positioning functions

    /**
     * Adds or inserts an element to the form
     *
     * @param Element $element
     * @param int $pos 0-based position in the form, -1 for at the end
     * @return Element
     */
    public function addElement(Element $element, $pos = -1)
    {
        if (is_a($element, '\dokuwiki\Form\Form')) throw new \InvalidArgumentException(
            'You can\'t add a form to a form'
        );
        if ($pos < 0) {
            $this->elements[] = $element;
        } else {
            array_splice($this->elements, $pos, 0, [$element]);
        }
        return $element;
    }

    /**
     * Replaces an existing element with a new one
     *
     * @param Element $element the new element
     * @param int $pos 0-based position of the element to replace
     */
    public function replaceElement(Element $element, $pos)
    {
        if (is_a($element, '\dokuwiki\Form\Form')) throw new \InvalidArgumentException(
            'You can\'t add a form to a form'
        );
        array_splice($this->elements, $pos, 1, [$element]);
    }

    /**
     * Remove an element from the form completely
     *
     * @param int $pos 0-based position of the element to remove
     */
    public function removeElement($pos)
    {
        array_splice($this->elements, $pos, 1);
    }

    #endregion

    #region Element adding functions

    /**
     * Adds a text input field
     *
     * @param string $name
     * @param string $label
     * @param int $pos
     * @return InputElement
     */
    public function addTextInput($name, $label = '', $pos = -1)
    {
        return $this->addElement(new InputElement('text', $name, $label), $pos);
    }

    /**
     * Adds a password input field
     *
     * @param string $name
     * @param string $label
     * @param int $pos
     * @return InputElement
     */
    public function addPasswordInput($name, $label = '', $pos = -1)
    {
        return $this->addElement(new InputElement('password', $name, $label), $pos);
    }

    /**
     * Adds a radio button field
     *
     * @param string $name
     * @param string $label
     * @param int $pos
     * @return CheckableElement
     */
    public function addRadioButton($name, $label = '', $pos = -1)
    {
        return $this->addElement(new CheckableElement('radio', $name, $label), $pos);
    }

    /**
     * Adds a checkbox field
     *
     * @param string $name
     * @param string $label
     * @param int $pos
     * @return CheckableElement
     */
    public function addCheckbox($name, $label = '', $pos = -1)
    {
        return $this->addElement(new CheckableElement('checkbox', $name, $label), $pos);
    }

    /**
     * Adds a dropdown field
     *
     * @param string $name
     * @param array $options
     * @param string $label
     * @param int $pos
     * @return DropdownElement
     */
    public function addDropdown($name, $options, $label = '', $pos = -1)
    {
        return $this->addElement(new DropdownElement($name, $options, $label), $pos);
    }

    /**
     * Adds a textarea field
     *
     * @param string $name
     * @param string $label
     * @param int $pos
     * @return TextareaElement
     */
    public function addTextarea($name, $label = '', $pos = -1)
    {
        return $this->addElement(new TextareaElement($name, $label), $pos);
    }

    /**
     * Adds a simple button, escapes the content for you
     *
     * @param string $name
     * @param string $content
     * @param int $pos
     * @return Element
     */
    public function addButton($name, $content, $pos = -1)
    {
        return $this->addElement(new ButtonElement($name, hsc($content)), $pos);
    }

    /**
     * Adds a simple button, allows HTML for content
     *
     * @param string $name
     * @param string $html
     * @param int $pos
     * @return Element
     */
    public function addButtonHTML($name, $html, $pos = -1)
    {
        return $this->addElement(new ButtonElement($name, $html), $pos);
    }

    /**
     * Adds a label referencing another input element, escapes the label for you
     *
     * @param string $label
     * @param string $for
     * @param int $pos
     * @return Element
     */
    public function addLabel($label, $for = '', $pos = -1)
    {
        return $this->addLabelHTML(hsc($label), $for, $pos);
    }

    /**
     * Adds a label referencing another input element, allows HTML for content
     *
     * @param string $content
     * @param string|Element $for
     * @param int $pos
     * @return Element
     */
    public function addLabelHTML($content, $for = '', $pos = -1)
    {
        $element = new LabelElement($content);

        if (is_a($for, '\dokuwiki\Form\Element')) {
            /** @var Element $for */
            $for = $for->id();
        }
        $for = (string) $for;
        if ($for !== '') {
            $element->attr('for', $for);
        }

        return $this->addElement($element, $pos);
    }

    /**
     * Add fixed HTML to the form
     *
     * @param string $html
     * @param int $pos
     * @return HTMLElement
     */
    public function addHTML($html, $pos = -1)
    {
        return $this->addElement(new HTMLElement($html), $pos);
    }

    /**
     * Add a closed HTML tag to the form
     *
     * @param string $tag
     * @param int $pos
     * @return TagElement
     */
    public function addTag($tag, $pos = -1)
    {
        return $this->addElement(new TagElement($tag), $pos);
    }

    /**
     * Add an open HTML tag to the form
     *
     * Be sure to close it again!
     *
     * @param string $tag
     * @param int $pos
     * @return TagOpenElement
     */
    public function addTagOpen($tag, $pos = -1)
    {
        return $this->addElement(new TagOpenElement($tag), $pos);
    }

    /**
     * Add a closing HTML tag to the form
     *
     * Be sure it had been opened before
     *
     * @param string $tag
     * @param int $pos
     * @return TagCloseElement
     */
    public function addTagClose($tag, $pos = -1)
    {
        return $this->addElement(new TagCloseElement($tag), $pos);
    }

    /**
     * Open a Fieldset
     *
     * @param string $legend
     * @param int $pos
     * @return FieldsetOpenElement
     */
    public function addFieldsetOpen($legend = '', $pos = -1)
    {
        return $this->addElement(new FieldsetOpenElement($legend), $pos);
    }

    /**
     * Close a fieldset
     *
     * @param int $pos
     * @return TagCloseElement
     */
    public function addFieldsetClose($pos = -1)
    {
        return $this->addElement(new FieldsetCloseElement(), $pos);
    }

    #endregion

    /**
     * Adjust the elements so that fieldset open and closes are matching
     */
    protected function balanceFieldsets()
    {
        $lastclose = 0;
        $isopen = false;
        $len = count($this->elements);

        for ($pos = 0; $pos < $len; $pos++) {
            $type = $this->elements[$pos]->getType();
            if ($type == 'fieldsetopen') {
                if ($isopen) {
                    //close previous fieldset
                    $this->addFieldsetClose($pos);
                    $lastclose = $pos + 1;
                    $pos++;
                    $len++;
                }
                $isopen = true;
            } elseif ($type == 'fieldsetclose') {
                if (!$isopen) {
                    // make sure there was a fieldsetopen
                    // either right after the last close or at the begining
                    $this->addFieldsetOpen('', $lastclose);
                    $len++;
                    $pos++;
                }
                $lastclose = $pos;
                $isopen = false;
            }
        }

        // close open fieldset at the end
        if ($isopen) {
            $this->addFieldsetClose();
        }
    }

    /**
     * The HTML representation of the whole form
     *
     * @param string $eventName  (optional) name of the event: FORM_{$name}_OUTPUT
     * @return string
     */
    public function toHTML($eventName = null)
    {
        $this->balanceFieldsets();

        // trigger event to provide an opportunity to modify this form
        if (isset($eventName)) {
            $eventName = 'FORM_' . strtoupper($eventName) . '_OUTPUT';
            Event::createAndTrigger($eventName, $this, null, false);
        }

        $html = '<form ' . buildAttributes($this->attrs()) . '>';

        foreach ($this->hidden as $name => $value) {
            $html .= '<input type="hidden" name="' . $name . '" value="' . formText($value) . '" />';
        }

        foreach ($this->elements as $element) {
            $html .= $element->toHTML();
        }

        $html .= '</form>';

        return $html;
    }
}
