<?php

/**
 * Haste utilities for Contao Open Source CMS
 *
 * Copyright (C) 2012-2013 Codefog & terminal42 gmbh
 *
 * @package    Haste
 * @link       http://github.com/codefog/contao-haste/
 * @license    http://opensource.org/licenses/lgpl-3.0.html LGPL
 */

namespace Haste\Form;

use Contao\Controller;
use Contao\Date;
use Contao\Environment;
use Contao\FormFieldModel;
use Contao\FormHidden;
use Contao\FrontendTemplate;
use Contao\Input;
use Contao\Model;
use Contao\PageModel;
use Contao\System;
use Contao\TemplateLoader;
use Contao\Widget;
use Haste\Form\Validator\ValidatorInterface;
use Haste\Generator\RowClass;
use Haste\Util\ArrayPosition;

class Form extends Controller
{
    /**
     * State of the form
     * Can be either clean or dirty
     */
    const STATE_CLEAN = 0;
    const STATE_DIRTY = 1;

    /**
     * Form ID
     * @var string
     */
    protected $strFormId;

    /**
     * HTTP Method
     * @var string Can be either GET or POST
     */
    protected $strMethod;

    /**
     * Form action
     * @var string
     */
    protected $strFormAction;

    /**
     * True if the form has been submitted
     * @var boolean
     */
    protected $blnSubmitted;

    /**
     * Render forms tableless
     * @var boolean
     */
    protected $blnTableless = true;

    /**
     * True if the form has uploads
     * @var boolean
     */
    protected $blnHasUploads = false;

    /**
     * True if the HTML5 validation should be ignored
     * @var bool
     */
    protected $blnNoValidate = false;

    /**
     * Form fields in the representation AFTER the Widget::getAttributesFromDca() call
     * @var array
     */
    protected $arrFormFields = array();

    /**
     * Widget instances
     * @var Widget[]
     */
    protected $arrWidgets = array();

    /**
     * Bound model
     * @var Model
     */
    protected $objModel = null;

    /**
     * Validators
     * @var array
     */
    protected $arrValidators = array();

    /**
     * Enctype
     * @var string
     */
    protected $strEnctype = 'application/x-www-form-urlencoded';

    /**
     * Current form state
     * @var int
     */
    protected $intState = self::STATE_CLEAN;

    /**
     * Check if form is valid
     * @var boolean
     */
    protected $blnValid = true;

    /**
     * Input callback
     * @var callable
     */
    protected $inputCallback;

    /**
     * Initialize the form
     *
     * @param string   $strId          The ID of the form
     * @param string   $strMethod      The HTTP Method GET or POST
     * @param callable $varSubmitCheck A callable that checks if the form has been submitted
     * @param boolean  $blnTableless   Whether to render the form tableless or not
     *
     * @throws \InvalidArgumentException
     */
    public function __construct($strId, $strMethod, $varSubmitCheck, $blnTableless = true)
    {
        parent::__construct();

        if (is_numeric($strId)) {
            throw new \InvalidArgumentException('You cannot use a numeric form id.');
        }

        $this->strFormId = $strId;

        if (!in_array($strMethod, array('GET', 'POST'), true)) {
            throw new \InvalidArgumentException('The method has to be either GET or POST.');
        }

        if (!is_callable($varSubmitCheck)) {
            throw new \InvalidArgumentException('The submit check must be callable.');
        }

        $this->strMethod = $strMethod;
        $this->blnSubmitted = call_user_func($varSubmitCheck, $this);
        $this->blnTableless = $blnTableless;

        // The form action can be set using several helper methods but by default it's just
        // pointing to the current page
        $this->strFormAction = Environment::get('request');
    }

    /**
     * Set the form action directly
     *
     * @param string $strUri The URI
     *
     * @return $this
     */
    public function setFormActionFromUri($strUri)
    {
        $this->strFormAction = $strUri;

        return $this;
    }

    /**
     * Set the form action from a Contao page ID
     *
     * @param   int $intId The page ID
     *
     * @return   $this
     * @throws  \InvalidArgumentException
     */
    public function setFormActionFromPageId($intId)
    {
        if (($objPage = PageModel::findWithDetails($intId)) === null) {
            throw new \InvalidArgumentException(sprintf('The page id "%s" does apparently not exist!', $intId));
        }

        $this->strFormAction = Controller::generateFrontendUrl($objPage->row(), null, $objPage->language);

        return $this;
    }

    /**
     * Preserve the current GET parameters by adding them as hidden fields
     *
     * @param array $arrExclude
     */
    public function preserveGetParameters($arrExclude = array())
    {
        foreach ($_GET as $k => $v) {
            if (in_array($k, $arrExclude, false)) {
                continue;
            }

            if (array_key_exists($k, $this->arrFormFields)) {
                continue;
            }

            $this->addFormField($k, array(
                'inputType' => 'hidden',
                'value' => Input::get($k)
            ));
        }
    }

    /**
     * Get form method
     *
     * @return string
     */
    public function getMethod()
    {
        return $this->strMethod;
    }

    /**
     * Get the form action
     *
     * @return string
     */
    public function getFormAction()
    {
        return $this->strFormAction;
    }

    /**
     * Gets the form ID
     *
     * @return string The form ID
     */
    public function getFormId()
    {
        return $this->strFormId;
    }

    /**
     * Gets the encoding type
     *
     * @return  string
     */
    public function getEnctype()
    {
        return $this->strEnctype;
    }

    /**
     * Get novalidate flag
     *
     * @return bool
     */
    public function isNoValidate()
    {
        return $this->blnNoValidate;
    }

    /**
     * Generate the novalidate attribute
     *
     * @return string
     */
    public function generateNoValidate()
    {
        return $this->isNoValidate() ? ' novalidate' : '';
    }

    /**
     * Set novalidate flag
     *
     * @param bool $blnNoValidate
     */
    public function setNoValidate($blnNoValidate)
    {
        $this->blnNoValidate = (bool) $blnNoValidate;
    }

    /**
     * Get tableless flag
     *
     * @return bool
     */
    public function isTableless()
    {
        return $this->blnTableless;
    }

    /**
     * Set tabeless flag
     *
     * @param bool $blnTableless
     *
     * @return $this
     */
    public function setTableless($blnTableless)
    {
        $this->blnTableless = (bool) $blnTableless;
        $this->intState = self::STATE_DIRTY;

        return $this;
    }

    /**
     * @param bool $blnSubmitted
     *
     * @return Form
     */
    public function setSubmitted($blnSubmitted)
    {
        $this->blnSubmitted = (bool) $blnSubmitted;

        return $this;
    }

    /**
     * Check if the form has been submitted
     *
     * @return bool
     */
    public function isSubmitted()
    {
        return (bool) $this->blnSubmitted;
    }

    /**
     * Check if the form is valid (no widget has an error)
     *
     * @return  bool
     */
    public function isValid()
    {
        return (bool) $this->blnValid;
    }

    /**
     * Check if form is dirty (widgets need to be generated)
     *
     * @return bool
     */
    public function isDirty()
    {
        return (bool) ($this->intState === static::STATE_DIRTY);
    }

    /**
     * Check if there are uploads
     *
     * @return  bool
     */
    public function hasUploads()
    {
        // We need to create the widgets to know if we have uploads
        $this->createWidgets();

        return (bool) $this->blnHasUploads;
    }

    /**
     * Check if form has fields
     *
     * @return bool
     */
    public function hasFields()
    {
        return !empty($this->arrFormFields);
    }

    /**
     * Set a callback to fetch the widget input instead of using getPost()
     *
     * @param callable|null $callback The callback
     *
     * @return $this The widget object
     */
    public function setInputCallback(callable $callback = null)
    {
        $this->inputCallback = $callback;

        return $this;
    }

    /**
     * Adds a form field
     *
     * @param string        $strName the form field name
     * @param array         $arrDca The DCA representation of the field
     * @param ArrayPosition $position
     *
     * @return $this
     */
    public function addFormField($strName, array $arrDca, ArrayPosition $position = null)
    {
        $this->checkFormFieldNameIsValid($strName);

        if (null === $position) {
            $position = ArrayPosition::last();
        }

        // Make sure it has a "name" attribute because it is mandatory
        if (!isset($arrDca['name'])) {
            $arrDca['name'] = $strName;
        }

        // Support default values
        if (!$this->isSubmitted()) {
            if (isset($arrDca['default']) && !isset($arrDca['value'])) {
                $arrDca['value'] = $arrDca['default'];
            }

            // Try to load the default value from bound Model
            if (!($arrDca['ignoreModelValue'] ?? false) && $this->objModel !== null) {
                $arrDca['value'] = $this->objModel->$strName;
            }
        }

        if (!isset($arrDca['inputType'])) {
            throw new \RuntimeException(sprintf('You did not specify any inputType for the field "%s"!', $strName));
        }

        /** @type Widget $strClass */
        $strClass = $GLOBALS['TL_FFL'][$arrDca['inputType']];

        if (!class_exists($strClass)) {
            throw new \RuntimeException(sprintf('The class "%s" for type "%s" could not be found.', $strClass, $arrDca['inputType']));
        }

        // Convert date formats into timestamps
        $rgxp = $arrDca['eval']['rgxp'] ?? null;
        if (in_array($rgxp, array('date', 'time', 'datim'), true)) {
            $this->addValidator($strName, function($varValue) use ($rgxp) {
                if ($varValue != '') {
                    $key = $rgxp . 'Format';
                    $format = isset($GLOBALS['objPage']) ? $GLOBALS['objPage']->{$key} : $GLOBALS['TL_CONFIG'][$key];
                    $objDate = new Date($varValue, $format);
                    $varValue = $objDate->tstamp;
                }

                return $varValue;
            });
        }

        if (is_array($arrDca['save_callback'] ?? null)) {

            $this->addValidator(
                $strName,
                function($varValue, Widget $objWidget, Form $objForm) use ($arrDca, $strName) {
                    $objModel = $objForm->getBoundModel();
                    $dc = (object) array(
                        'id'            => $objModel ? $objModel->id : 0,
                        'table'         => $objModel ? $objModel::getTable() : '',
                        'value'         => $varValue,
                        'field'         => $strName,
                        'inputName'     => $objWidget->name,
                        'activeRecord'  => $objModel
                    );

                    foreach ($arrDca['save_callback'] as $callback) {
                        if (is_array($callback)) {
                            $objCallback = System::importStatic($callback[0]);
                            $varValue = $objCallback->{$callback[1]}($varValue, $dc);
                        } elseif (is_callable($callback)) {
                            $varValue = $callback($varValue, $dc);
                        }
                    }

                    return $varValue;
                }
            );
        }

        $objModel = $this->getBoundModel();
        $dc = (object) array(
            'id'            => $objModel ? $objModel->id : 0,
            'table'         => $objModel ? $objModel::getTable() : '',
            'value'         => $arrDca['value'] ?? null,
            'field'         => $strName,
            'inputName'     => $arrDca['name'],
            'activeRecord'  => $objModel
        );

        // Preserve the label
        $strLabel = $arrDca['label'] ?? null;

        $arrDca = $strClass::getAttributesFromDca($arrDca, $arrDca['name'], $arrDca['value'] ?? null, $strName, $dc->table, $dc);

        // Reset the ID to the field name
        $arrDca['id'] = $strName;

        // Remove the label if it was not set – Contao will set it to field name if it's not present
        if (!isset($strLabel) || !$strLabel) {
            $arrDca['label'] = '';
        }

        // Convert optgroups so they work with FormSelectMenu
        if (is_array($arrDca['options'] ?? null) && array_is_assoc($arrDca['options'])) {
            $arrOptions = $arrDca['options'];
            $arrDca['options'] = array();

            foreach ($arrOptions as $k => $v) {
                if (isset($v['label'])) {
                    $arrDca['options'][] = $v;
                } else {
                    $arrDca['options'][] = array(
                        'label'     => $k,
                        'value'     => $k,
                        'group'     => '1',
                    );

                    foreach ($v as $vv) {
                        $arrDca['options'][] = $vv;
                    }
                }
            }
        }

        $this->arrFormFields = $position->addToArray($this->arrFormFields, array($strName=>$arrDca));
        $this->intState = self::STATE_DIRTY;

        return $this;
    }

    /**
     * Add multiple form fields
     *
     * @param array         $arrFormFields
     * @param ArrayPosition $position
     *
     * @return $this
     */
    public function addFormFields(array $arrFormFields, ArrayPosition $position = null)
    {
        if (null !== $position && ($position->position() === ArrayPosition::FIRST || $position->position() === ArrayPosition::BEFORE)) {
            $arrFormFields = array_reverse($arrFormFields, true);
        }

        foreach ($arrFormFields as $strName => $arrDca) {
            $this->addFormField($strName, $arrDca, $position);
        }

        return $this;
    }

    /**
     * Binds a model instance to the form. If there is data, haste form will add
     * the present values as default values.
     *
     * @param Model
     *
     * @return $this
     */
    public function bindModel(Model $objModel = null)
    {
        $this->objModel = $objModel;

        return $this;
    }

    /**
     * Gets the bound model
     *
     * @return Model
     */
    public function getBoundModel()
    {
        return $this->objModel;
    }

    /**
     * Add the Contao hidden fields FORM_SUBMIT and REQUEST_TOKEN
     */
    public function addContaoHiddenFields()
    {
        $this->addFormField('FORM_SUBMIT', array(
            'name' => 'FORM_SUBMIT',
            'inputType' => 'hidden',
            'ignoreModelValue' => true,
            'value' => $this->getFormId()
        ));

        $tokenName = System::getContainer()->getParameter('contao.csrf_token_name');
        $tokenManager = System::getContainer()->has('contao.csrf.token_manager') ? System::getContainer()->get('contao.csrf.token_manager') : System::getContainer()->get('security.csrf.token_manager');

        $this->addFormField('REQUEST_TOKEN', array(
            'name' => 'REQUEST_TOKEN',
            'inputType' => 'hidden',
            'ignoreModelValue' => true,
            'value' => $tokenManager->getToken($tokenName)->getValue()
        ));
    }

    /**
     * Helper method to easily add a captcha field
     *
     * @param string        $strName The form field name
     * @param ArrayPosition $position
     */
    public function addCaptchaFormField($strName, ArrayPosition $position = null)
    {
        $this->addFormField($strName, array(
            'name'      => $strName . '_' . $this->strFormId, // make sure they're unique on a page
            'label'     => $GLOBALS['TL_LANG']['MSC']['securityQuestion'],
            'inputType' => 'captcha',
            'eval'      => array('mandatory'=>true)
        ), $position);
    }

    /**
     * Helper method to easily add a submit field
     *
     * @param string        $strName  The form field name
     * @param string        $strLabel The label for the submit button
     * @param ArrayPosition $position
     */
    public function addSubmitFormField($strName, $strLabel, ArrayPosition $position = null)
    {
        $this->addFormField($strName, array(
            'name'      => $strName,
            'label'     => $strLabel,
            'inputType' => 'submit'
        ), $position);
    }

    /**
     * Add form fields from a back end DCA
     *
     * @param string   $strTable    The DCA table name
     * @param callable $varCallback Called for each field, return true if you want to include the field in the form
     *
     * @return $this
     */
    public function addFieldsFromDca($strTable, $varCallback = null)
    {
        System::loadLanguageFile($strTable);
        $this->loadDataContainer($strTable);
        $arrFields = &$GLOBALS['TL_DCA'][$strTable]['fields'];

        foreach (($arrFields ?? []) as $k => $v) {
            if (is_callable($varCallback) && !call_user_func_array($varCallback, array(&$k, &$v))) {
                continue;
            }

            $this->addFormField($k, $v);
        }

        return $this;
    }

    /**
     * Return true if the field has "inputType" set, false otherwise
     *
     * This is a default callback that can be used with addFieldsFromDca() method. It prevents from adding fields
     * that do not have inputType specified which would result in an exception. The fields you typically would
     * like to skip are: id, tstamp, pid, sorting.
     *
     * @param string $field
     * @param array  $dca
     *
     * @return bool
     */
    public function skipFieldsWithoutInputType($field, array $dca)
    {
        return isset($dca['inputType']);
    }

    /**
     * Add form fields from a back end form generator form ID
     *
     * @param int      $intId       The form generator form ID
     * @param callable $varCallback Called for each field, return true if you want to include the field in the form
     *
     * @return $this
     * @throws \InvalidArgumentException
     */
    public function addFieldsFromFormGenerator($intId, $varCallback = null)
    {
        if (($objFields = FormFieldModel::findPublishedByPid($intId)) === null) {
            throw new \InvalidArgumentException('Form ID "' . $intId . '" does not exist or has no published fields.');
        }

        while ($objFields->next()) {
            // make sure "name" is set because not all form fields do need it and it would thus overwrite the array indexes
            $strName = $objFields->name ?: 'field_' . $objFields->id;

            $this->checkFormFieldNameIsValid($strName);

            $arrDca = $objFields->row();

            // Make sure it has a "name" attribute because it is mandatory
            if (!isset($arrDca['name'])) {
                $arrDca['name'] = $strName;
            }

            if (is_callable($varCallback) && !call_user_func_array($varCallback, array(&$strName, &$arrDca))) {
                continue;
            }

            $this->arrFormFields[$strName] = $arrDca;
        }

        $this->intState = self::STATE_DIRTY;

        return $this;
    }

    /**
     * Adds a form field from the form generator without trying to convert a DCA configuration.
     *
     * @param string             $strName
     * @param array              $arrDca
     * @param ArrayPosition|null $position
     */
    public function addFieldFromFormGenerator($strName, array $arrDca, ArrayPosition $position = null)
    {
        $this->checkFormFieldNameIsValid($strName);

        if (null === $position) {
            $position = ArrayPosition::last();
        }

        // make sure "name" is set because not all form fields do need it and it would thus overwrite the array indexes
        $strName = $arrDca['name'] ?: 'field_' . $arrDca['id'];

        // Make sure it has a "name" attribute because it is mandatory
        if (!isset($arrDca['name'])) {
            $arrDca['name'] = $strName;
        }

        $this->arrFormFields = $position->addToArray($this->arrFormFields, array($strName=>$arrDca));
        $this->intState = self::STATE_DIRTY;
    }

    /**
     * Get a form field by a given name
     *
     * @param string $strName The form field name
     *
     * @return array
     */
    public function getFormField($strName)
    {
        return $this->arrFormFields[$strName];
    }

    /**
     * Get all form fields
     *
     * @return array
     */
    public function getFormFields()
    {
        return $this->arrFormFields;
    }

    /**
     * Removes a form field
     *
     * @param string $strName The form field name
     *
     * @return $this
     */
    public function removeFormField($strName)
    {
        unset($this->arrFormFields[$strName]);
        $this->intState = self::STATE_DIRTY;

        return $this;
    }

    /**
     * Checks if there is a form field with a given name
     *
     * @param string $strName The form field name
     *
     * @return bool
     */
    public function hasFormField($strName)
    {
        return isset($this->arrFormFields[$strName]);
    }

    /**
     * Returns a widget instance if existing
     *
     * @param string $strName The form field name
     *
     * @return Widget
     */
    public function getWidget($strName)
    {
        $this->createWidgets();

        return $this->arrWidgets[$strName];
    }

    /**
     * Return all widgets
     *
     * @return array
     */
    public function getWidgets()
    {
        $this->createWidgets();

        return $this->arrWidgets;
    }

    /**
     * Add a validator to the form field
     *
     * @param string                      $strName      The form field name
     * @param ValidatorInterface|callable $varValidator An instance of ValidatorInterface or a callable that will be called on widget validation
     *
     * @return $this
     * @throws \InvalidArgumentException
     */
    public function addValidator($strName, $varValidator)
    {
        if ($varValidator instanceof ValidatorInterface || is_callable($varValidator)) {
            $this->arrValidators[$strName][] = $varValidator;
        } else {
            throw new \InvalidArgumentException('Your validator is invalid!');
        }

        return $this;
    }

    /**
     * Create the widget instances
     *
     * @return $this
     * @throws \RuntimeException
     */
    public function createWidgets()
    {
        // Do nothing if already generated
        if (!$this->isDirty()) {
            return $this;
        }

        $i = 0;

        // Reset to initial values
        $this->arrWidgets = array();
        $this->blnHasUploads = false;

        // Initialize widgets
        foreach ($this->arrFormFields as $strName => $arrField) {

            $strClass = $GLOBALS['TL_FFL'][$arrField['type']] ?? '';

            if (!class_exists($strClass)) {
                throw new \RuntimeException(sprintf('The class "%s" for type "%s" could not be found.', $strClass, $arrField['type']));
            }

            $arrField['tableless']  = $this->blnTableless;

            // Some widgets render the mandatory asterisk only based on "require" attribute
            if (!isset($arrField['required'])) {
                $arrField['required'] = (bool) ($arrField['mandatory'] ?? false);
            }

            $objWidget = new $strClass($arrField);

            if ($objWidget instanceof \uploadable) {
                $this->blnHasUploads = true;
            }

            $this->arrWidgets[$strName] = $objWidget;
            $i++;
        }

        RowClass::withKey('rowClass')->addCount('row_')->addFirstLast('row_')->addEvenOdd()->applyTo($this->arrWidgets);

        $this->intState = self::STATE_CLEAN;

        if ($this->hasUploads()) {
            if ('GET' === $this->getMethod()) {
                throw new \RuntimeException('How do you want me to upload your file using GET?');
            }

            $this->strEnctype = 'multipart/form-data';
        } else {
            $this->strEnctype = 'application/x-www-form-urlencoded';
        }

        return $this;
    }

    /**
     * Validate the form
     *
     * @return bool
     */
    public function validate()
    {
        if (!$this->isSubmitted()) {
            return false;
        }

        $this->createWidgets();
        $this->blnValid = true;

        foreach ($this->arrWidgets as $strName => $objWidget) {

            if (null !== $this->inputCallback && method_exists($objWidget, 'setInputCallback')) {
                $objWidget->setInputCallback($this->inputCallback);
            }

            $objWidget->validate();

            if ($objWidget->hasErrors()) {
                $this->blnValid = false;

            } elseif ($objWidget->submitInput()) {

                $varValue = $objWidget->value;

                // Run custom validators
                if (isset($this->arrValidators[$strName])) {

                    try {
                        foreach ($this->arrValidators[$strName] as $varValidator) {

                            if ($varValidator instanceof ValidatorInterface) {
                                $varValue = $varValidator->validate($varValue, $objWidget, $this);
                            } else {
                                $varValue = call_user_func($varValidator, $varValue, $objWidget, $this);
                            }
                        }
                    } catch (\Exception $e) {
                        $objWidget->class = 'error';
                        $objWidget->addError($e->getMessage());
                        $this->blnValid = false;
                    }
                }

                /** @noinspection NotOptimalIfConditionsInspection */
                if ($objWidget->hasErrors()) {
                    // Re-check the status in case a custom validator has added an error
                    $this->blnValid = false;

                } elseif ($this->objModel !== null) {
                    // Bind to Model instance
                    $this->objModel->$strName =  $varValue;
                }
            }
        }

        return $this->isValid();
    }

    /**
     * Add form to a template
     *
     * @param FrontendTemplate $objTemplate
     *
     * @return $this
     */
    public function addToTemplate(FrontendTemplate $objTemplate)
    {
        return $this->addToObject($objTemplate);
    }

    /**
     * Add form to a object
     *
     * @param object $objObject
     *
     * @return $this
     */
    public function addToObject($objObject)
    {
        $this->createWidgets();

        $tokenName = System::getContainer()->getParameter('contao.csrf_token_name');
        $tokenManager = System::getContainer()->has('contao.csrf.token_manager') ? System::getContainer()->get('contao.csrf.token_manager') : System::getContainer()->get('security.csrf.token_manager');

        $objObject->action = $this->getFormAction();
        $objObject->formId = $this->getFormId();
        $objObject->requestToken = $tokenManager->getToken($tokenName)->getValue();
        $objObject->method = strtolower($this->getMethod());
        $objObject->enctype = $this->getEnctype();
        $objObject->widgets = $this->arrWidgets;
        $objObject->valid = $this->isValid();
        $objObject->submitted = $this->isSubmitted();
        $objObject->hasUploads = $this->hasUploads();
        $objObject->novalidate = $this->generateNoValidate();
        $objObject->tableless = $this->blnTableless;

        /** @type Widget $objWidget */
        $arrWidgets = $this->splitHiddenAndVisibleWidgets();

        $objObject->hidden = '';

        // Generate hidden form fields
        foreach ((array) ($arrWidgets['hidden'] ?? []) as $objWidget) {
            $objObject->hidden .= $objWidget->parse();
        }

        $objObject->fields = '';

        // Generate visible form fields
        foreach ((array) $arrWidgets['visible'] as $objWidget) {
            $objObject->fields .= $objWidget->parse();
        }

        $objObject->hiddenWidgets  = $arrWidgets['hidden'] ?? [];
        $objObject->visibleWidgets = $arrWidgets['visible'] ?? [];

        $objObject->hasteFormInstance = $this;

        return $this;
    }

    /**
     * Get the helper object
     *
     * @return \stdClass
     */
    public function getHelperObject()
    {
        $helper = new \stdClass();
        $this->addToObject($helper);

        return $helper;
    }

    /**
     * Generate a form and return it as HTML string
     *
     * @param string|null $templateName The form wrapper template name or null to auto-select (based on Contao version).
     *
     * @return string
     */
    public function generate($templateName = null)
    {
        if (null === $templateName) {
            $templateName = 'form';

            try {
                TemplateLoader::getPath($templateName, 'html5');
            } catch (\Exception $e) {
                $templateName = 'form_wrapper';
            }
        }

        $objTemplate = new FrontendTemplate($templateName);
        $objTemplate->class = 'hasteform_' . $this->getFormId();
        $objTemplate->formSubmit = $this->getFormId();

        $this->addToTemplate($objTemplate);

        return $objTemplate->parse();
    }

    /**
     * Return the submitted data of a specific form field
     *
     * @param string $strName The form field name
     *
     * @return mixed    The value of the widget
     * @throws \BadMethodCallException
     * @throws \InvalidArgumentException
     */
    public function fetch($strName)
    {
        if (!$this->isSubmitted()) {
            throw new \BadMethodCallException('How do you want me to fetch data from an unsubmitted form?');
        }

        if ($this->getMethod() !== 'POST') {
            throw new \BadMethodCallException('Widgets only support fetching POST values. Use the Contao Input class for other purposes.');
        }

        if (!isset($this->arrWidgets[$strName])) {
            throw new \InvalidArgumentException('The widget with name "' . $strName . '" does not exist.');
        }

        $objWidget = $this->arrWidgets[$strName];

        if (!$objWidget->submitInput()) {
            // Do not throw exception here for BC
            return null;
        }

        return $objWidget->value;
    }

    /**
     * Return the submitted data as an associative array
     *
     * @param callable $varCallback A callable that should be used to fetch the data instead of the built in functionality
     *
     * @return array
     */
    public function fetchAll($varCallback = null)
    {
        $arrData = array();

        foreach ($this->arrWidgets as $strName => $objWidget) {
            // Do not check $objWidget->submitInput() here because the callback could handle it differently

            if (is_callable($varCallback)) {
                $varValue = call_user_func($varCallback, $strName, $objWidget);
            } else {
                $varValue = $this->fetch($strName);
            }

            if (null !== $varValue) {
                $arrData[$strName] = $varValue;
            }
        }

        return $arrData;
    }

    /**
     * Generate a form and return it as HTML string
     *
     * @return string
     */
    public function __toString()
    {
        return $this->generate();
    }

    /**
     * Check for a valid form field name
     *
     * @param string $strName The form field name
     *
     * @throws \InvalidArgumentException
     */
    protected function checkFormFieldNameIsValid($strName)
    {
        if (is_numeric($strName)) {
            throw new \InvalidArgumentException('You cannot use a numeric form field name.');
        }

        if (in_array($strName, array_keys($this->arrFormFields), true)) {
            throw new \InvalidArgumentException(sprintf('"%s" has already been added to the form.', $strName));
        }
    }

    /**
     * Splits hidden and visible widgets
     *
     * @return array
     */
    protected function splitHiddenAndVisibleWidgets()
    {
        $arrResult = array();
        foreach ($this->arrWidgets as $k => $objWidget) {
            $strKey = ($objWidget instanceof FormHidden) ? 'hidden' : 'visible';
            $arrResult[$strKey][$k] = $objWidget;
        }

        return $arrResult;
    }
}
