<?php
/**
 * This class implements generic form, which you can actually use without
 * redeclaring it. Just add fields, buttons and use execute method.
 *
 * @author      Romans <romans@adevel.com>
 * @copyright   See file COPYING
 *
 * @version     $Id$
 */
class Form_Basic extends View implements ArrayAccess
{
    /**
     * Template of layout if form has one.
     *
     * @var View
     */
    public $layout = null;

    /**
     * Here we will have a list of errors occured in the form, when we tried to
     * submit it. field_name => error
     *
     * @var array
     */
    public $errors = array();

    /**
     * Those templates will be used when rendering form and fields.
     *
     * @var array
     */
    public $template_chunks = array();

    /**
     * This array holds list of values prepared for fields before their
     * initialization. When fields are initialized they will look into this
     * array to see if there are default value for them.
     * Afterwards fields will link to $this->data, so changing
     * $this->data['fld_name'] would actually affect field's value.
     * You should use $this->set() and $this->get() to read/write individual
     * field values. You should use $this->setStaticSource() to load values from
     * hash, BUT - AAAAAAAAAA: this array is no more!!!
     *
     * @var array
     */
    public $data = array();

    /**
     * If this is true, we won't load data or submit or validate anything.
     *
     * @var null|bool
     */
    public $bail_out = null;

    /**
     * true - update() will try updating existing row. false - it would insert new.
     *
     * @var bool
     */
    protected $loaded_from_db = false;

    /**
     * Contains AJAX instances assigned to buttons.
     *
     * @var array
     */
    protected $ajax_submits = array();

    /**
     * If condition was passed to a form through GET, contains a GET field name.
     *
     * @var null|string
     */
    protected $get_field = null;

    /** @var string JS widget name */
    public $js_widget = 'ui.atk4_form';

    /** @var array JS widget options */
    public $js_widget_arguments = array();

    /** @var string Class name of default exception */
    public $default_exception = 'Exception_ValidityCheck';

    /** @var string Class name of default controller */
    public $default_controller = 'Controller_MVCForm';

    /** @var Controller_Validator Validator object */
    public $validator = null;

    /**
     * Normally form fields are inserted using a form template. If you.
     * @todo check this - looks unused
     */
    public $search_for_field_spots;

    // {{{ Inherited properties

    /** @var App_Web */
    public $app;

    /** @var View */
    public $owner;

    // }}}

    public function init()
    {
        /*
         * During form initialization it will go through it's own template and
         * search for lots of small template chunks it will be using. If those
         * chunk won't be in template, it will fall back to default values.
         * This way you can re-define how form will look, but only what you need
         * in particular case. If you don't specify template at all, form will
         * work with default look.
         */
        parent::init();

        $this->getChunks();

        // After init method have been executed, it's safe for you to add
        // controls on the form. BTW, if you want to have default values such as
        // loaded from the table, then intialize $this->data array to default
        // values of those fields.
        $this->app->addHook('pre-exec', array($this, 'loadData'));
        $this->app->addHook('pre-render-output', array($this, 'lateSubmit'));
        $this->app->addHook('submitted', array($this, 'submitted'));

        $this->addHook('afterAdd', array($this, 'afterAdd'));
    }
    public function afterAdd($p, $c)
    {
        if ($c instanceof AbstractView) {
            $c->addHook('afterAdd', array($this, 'afterAdd'));
            $c->addMethod('addField', array($this, 'addField'));
            $c->addMethod('addSubmit', array($this, 'addSubmit'));
        }
    }

    protected function getChunks()
    {
        // commonly replaceable chunks
        $this->grabTemplateChunk('form_comment');
        $this->grabTemplateChunk('form_separator');
        $this->grabTemplateChunk('form_line'); // form line template,must contain field_caption,field_input,field_error
        if ($this->template->is_set('hidden_form_line')) {
            $this->grabTemplateChunk('hidden_form_line');
        }
        $this->grabTemplateChunk('field_error');    // template for error code, must contain field_error_str
        $this->grabTemplateChunk('field_mandatory');// template for marking mandatory fields

        // other grabbing will be done by field themselves as you will add them
        // to the form. They will try to look into this template, and if you
        // don't have apropriate templates for them, they will use default ones.
        $this->template_chunks['form'] = $this->template;
        $this->template_chunks['form']->del('Content');
        $this->template_chunks['form']->del('form_buttons');
        $this->template_chunks['form']->trySet('form_name', $this->name.'_form');
        $this->template_chunks['form']
            ->set('form_action', $this->app->url(null, array('submit' => $this->name)));

        return $this;
    }

    public function defaultTemplate($template = null, $tag = null)
    {
        return array('form');
    }
    public function grabTemplateChunk($name)
    {
        if ($this->template->is_set($name)) {
            $this->template_chunks[$name] = $this->template->cloneRegion($name);
        } else {
            unset($this->template_chunks[$name]);
            //return $this->fatal('missing form tag: '.$name);
            // hmm.. i wonder what ? :)
        }
    }
    /**
     * Should show error in field. Override this method to change form default alert.
     *
     * @param object $field Field instance that caused error
     * @param string $msg   message to show
     */
    public function showAjaxError($field, $msg)
    {
        // Avoid deprecated function use in reference field, line 246
        return $this->displayError($field, $msg);
    }

    public function displayError($field = null, $msg = null)
    {
        if (!$field) {
            // Field is not defined
            // TODO: add support for error in template
            $this->js()->univ()->alert($msg ?: 'Error in form')->execute();
        }
        if (!is_object($field)) {
            $field = $this->getElement($field);
        }

        $fn = $this->js_widget ? str_replace('ui.', '', $this->js_widget) : 'atk4_form';
        $this->js()->$fn('fieldError', $field->short_name, $msg)->execute();
    }

    /**
     * Adds error message to form field.
     *
     * @param string $field
     * @param string $text
     */
    public function error($field, $text = null)
    {
        /** @type Form_Field $form_field */
        $form_field = $this->getElement($field);
        $form_field->displayFieldError($text);
    }

    /**
     * Adds field in form.
     *
     * @param AbstractView|array|string $type
     * @param AbstractView|array|string $options
     * @param string $caption
     * @param string $attr Deprecated argument
     *
     * @return Form_Field
     */
    public function addField($type, $options = null, $caption = null, $attr = null)
    {
        $insert_into = $this->layout ?: $this;

        if (is_object($type) && $type instanceof AbstractView && !($type instanceof Form_Field)) {

            // using callback on a sub-view
            $insert_into = $type;
            list(, $type, $options, $caption, $attr) = func_get_args();
        }

        if ($options === null) {
            $options = $type;
            $type = 'Line';
        }

        if (is_array($options)) {
            $name = isset($options['name']) ? $options['name'] : null;
        } else {
            $name = $options; // backward compatibility
        }
        $name = preg_replace('|[^a-z0-9-_]|i', '_', $name);

        if ($caption === null) {
            $caption = ucwords(str_replace('_', ' ', $name));
        }

        /* normalzie name and put name back in options array */
        $name = $this->app->normalizeName($name);
        if (is_array($options)) {
            $options['name'] = $name;
        } else {
            $options = array('name' => $name);
        }

        $map = array(
            'dropdown' => 'DropDown',
            'checkboxlist' => 'CheckboxList',
            'hidden' => 'Hidden',
            'text' => 'Text',
            'line' => 'Line',
            'upload' => 'Upload',
            'radio' => 'Radio',
            'checkbox' => 'Checkbox',
            'password' => 'Password',
            'timepicker' => 'TimePicker',
            );
        $key = strtolower($type);
        $class = array_key_exists($key, $map) ? $map[$key] : $type;

        $class = $this->app->normalizeClassName($class, 'Form_Field');

        if ($insert_into === $this) {
            $template = $this->template->cloneRegion('form_line');
            $field = $this->add($class, $options, null, $template);
        } else {
            if ($insert_into->template->hasTag($name)) {
                $this->template->cloneRegion('field_input');
                $options['show_input_only'] = true;
                $field = $insert_into->add($class, $options, $name);
            } else {
                $template = $this->template->cloneRegion('form_line');
                $field = $insert_into->add($class, $options, null, $template);
            }

            // Keep Reference, for $form->getElement().
            $this->elements[$options['name']] = $field;
        }
        /** @type Form_Field $field */

        $field->setCaption($caption);
        $field->setForm($this);
        $field->template->trySet('field_type', strtolower($type));

        if ($attr) {
            if ($this->app->compat_42) {
                $field->setAttr($attr);
            } else {
                throw $this->exception('4th argument to addField is obsolete');
            }
        }

        return $field;
    }

    /**
     * Imports model fields in form by using form controller.
     *
     * @param Model $model
     * @param array|string|bool $fields
     */
    public function importFields($model, $fields = null)
    {
        /** @type Controller_MVCForm $c */
        $c = $this->add($this->default_controller);
        $c->importFields($model, $fields);
    }

    public function addSeparator($class = '', $attr = array())
    {
        if (!isset($this->template_chunks['form_separator'])) {
            /** @type View $v */
            $v = $this->add('View');
            return $v->addClass($class);
        }
        $c = clone $this->template_chunks['form_separator'];
        $c->trySet('fieldset_class', 'atk-cell '.$class);
        $this->template->trySet('fieldset_class', 'atk-cell');
        $this->template->trySet('form_class', 'atk-cells atk-cells-gutter-large');

        if (is_array($attr) && !empty($attr)) {
            foreach ($attr as $k => $v) {
                $c->appendHTML('fieldset_attributes', ' '.$k.'="'.$v.'"');
            }
        }

        /** @type Html $h */
        $h = $this->add('Html');
        return $h->set($c->render());
    }

    // Operating with field values
    //
    // {{{ ArrayAccess support
    public function offsetExists($name)
    {
        $f = $this->hasElement($name);
        return  $f && $f instanceof Form_Field;
    }
    public function offsetGet($name)
    {
        return $this->get($name);
    }
    public function offsetSet($name, $val)
    {
        $this->set($name, $val);
    }
    public function offsetUnset($name)
    {
        $this->set($name, null);
    }
    // }}}
    //
    public function get($field = null)
    {
        if (!$field) {
            return $this->data;
        }

        return $this->data[$field];
    }
    /*
     * temporarily disabled. TODO: will be implemented with abstract datatype
    function setSource($table,$db_fields=null){
        if(is_null($db_fields)){
            $db_fields=array();
            foreach($this->elements as $key=>$el){
                if(!($el instanceof Form_Field))continue;
                if($el->no_save)continue;
                $db_fields[]=$key;
            }
        }
        $this->dq = $this->app->db->dsql()
            ->table($table)
            ->field('*',$table)
            ->limit(1);
        return $this;
    }
     */
    public function set($field_or_array, $value = UNDEFINED)
    {
        // We use UNDEFINED, because 2nd argument of "null" is meaningfull
        if (is_array($field_or_array)) {
            foreach ($field_or_array as $key => $val) {
                if (isset($this->elements[$key]) && ($this->elements[$key] instanceof Form_Field)) {
                    $this->set($key, $val);
                }
            }

            return $this;
        }

        if (!isset($this->elements[$field_or_array])) {
            foreach ($this->elements as $key => $val) {
                echo "$key<br />";
            }
            throw new BaseException("Trying to set value for non-existant field $field_or_array");
        }
        if ($this->elements[$field_or_array] instanceof Form_Field) {
            $this->elements[$field_or_array]->set($value);
        } else {
            //throw new BaseException("Form fields must inherit from Form_Field ($field_or_array)");
            null;
        }

        return $this;
    }
    public function getAllFields()
    {
        return $this->get();
    }
    public function addSubmit($label = 'Save', $name = null, $tag = null)
    {
        $class = 'Form_Submit';
        if (is_object($label) && $label instanceof AbstractView && !($label instanceof Form_Field)) {
            // using callback on a sub-view
            $insert_into = $label;
            list(, $label, $name, $tag) = func_get_args();
            $submit = $insert_into->add($class, array('name' => $name, 'form' => $this), $tag);
        } else {
            if ($this->layout && $this->layout->template->hasTag($tag ?: 'FormButtons')) {
                $submit = $this->layout->add($class, array('name' => $name, 'form' => $this), $tag ?: 'FormButtons');
            } else {
                $submit = $this->add($class, array('name' => $name, 'form' => $this), $tag ?: 'form_buttons');
            }
        }

        /** @type Form_Submit $submit */
        $submit
            //->setIcon('ok') - removed as per Dmitry's request
            ->set($label)
            ->setNoSave();

        return $submit;
    }
    public function addButton($label = 'Button', $name = null, $tag = null)
    {
        $class = 'Button';
        if (is_object($label) && $label instanceof AbstractView && !($label instanceof Form_Field)) {
            // using callback on a sub-view
            $insert_into = $label;
            list(, $label, $name, $tag) = func_get_args();
            $button = $insert_into->add($class, $name, $tag);
        } else {
            if ($this->layout && $this->layout->template->hasTag($tag ?: 'FormButtons')) {
                $button = $this->layout->add($class, $name, $tag ?: 'FormButtons');
            } else {
                $button = $this->add($class, $name, $tag ?: 'form_buttons');
            }
        }

        /** @type Button $button */
        $button->setLabel($label);

        return $button;
    }

    public function loadData()
    {
        /**
         * This call will be sent to fields, and they will initialize their values from $this->data.
         */
        if (!is_null($this->bail_out)) {
            return;
        }
        $this->hook('post-loadData');
    }

    public function isLoadedFromDB()
    {
        return $this->loaded_from_db;
    }
    /* obsolete in 4.3 - use save() */
    public function update()
    {
        return $this->save();
    }
    public function save()
    {
        // TODO: start transaction here
        try {
            if ($this->hook('update')) {
                return $this;
            }

            if (!($m = $this->getModel())) {
                throw new BaseException("Can't save, model not specified");
            }
            if (!is_null($this->get_field)) {
                $this->app->stickyForget($this->get_field);
            }
            foreach ($this->elements as $short_name => $element) {
                if ($element instanceof Form_Field) {
                    if (!$element->no_save) {
                        $m->set($short_name, $element->get());
                    }
                }
            }
            $m->save();
        } catch (BaseException $e) {
            if ($e instanceof Exception_ValidityCheck) {
                $f = $e->getField();
                if ($f && is_string($f) && $fld = $this->hasElement($f)) {
                    /** @type Form_Field $fld */
                    $fld->displayFieldError($e->getMessage());
                } else {
                    $this->js()->univ()->alert($e->getMessage())->execute();
                }
            }
            if ($e instanceof Exception_ForUser) {
                $this->js()->univ()->alert($e->getMessage())->execute();
            }
            throw $e;
        }
    }
    public function submitted()
    {
        /*
         * Default down-call submitted will automatically call this method if form was submitted
         */
        // We want to give flexibility to our controls and grant them a chance
        // to hook to those spots here.
        // On Windows platform mod_rewrite is lowercasing all the urls.
        if ($_GET['submit'] != $this->name) {
            return;
        }
        if (!is_null($this->bail_out)) {
            return $this->bail_out;
        }

        $this->hook('loadPOST');
        try {
            $this->hook('validate');
            $this->hook('post-validate');

            if (!empty($this->errors)) {
                return false;
            }

            if (($output = $this->hook('submit', array($this)))) {
                $has_output = false; // @todo all this [if] block logic should be re-checked, looks suspicious
                /* checking if anything usefull in output */
                if (is_array($output)) {
                    $has_output = false;
                    foreach ($output as $row) {
                        if ($row) {
                            $has_output = true;
                            $output = $row;
                            break;
                        }
                    }
                    if (!$has_output) {
                        return true;
                    }
                }
                /* TODO: need logic re-check here + test scripts */
                //if(!is_array($output))$output=array($output);
                // already array
                if ($has_output) {
                    if ($output instanceof jQuery_Chain) {
                        $this->js(null, $output)->execute();
                    } elseif (is_string($output)) {
                        $this->js(null, $this->js()->reload())->univ()->successMessage($output)->execute();
                    }
                }
            }
        } catch (BaseException $e) {
            if ($e instanceof Exception_ValidityCheck) {
                $f = $e->getField();
                if ($f && is_string($f) && $fld = $this->hasElement($f)) {
                    /** @type Form_Field $fld */
                    $fld->displayFieldError($e->getMessage());
                } else {
                    $this->js()->univ()->alert($e->getMessage())->execute();
                }
            }
            if ($e instanceof Exception_ForUser) {
                $this->js()->univ()->alert($e->getMessage())->execute();
            }
            throw $e;
        }

        return true;
    }
    public function lateSubmit()
    {
        if (@$_GET['submit'] != $this->name) {
            return;
        }

        if ($this->bail_out === null || $this->isSubmitted()) {
            $this->js()->univ()
                ->consoleError('Form '.$this->name.' submission is not handled.'.
                    ' See: http://agiletoolkit.org/doc/form/submit')
                ->execute();
        }
    }
    public function isSubmitted()
    {
        // This is alternative way for form submission. After form is initialized
        // you can call this method. It will hurry up all the steps, but you will
        // have ready-to-use form right away and can make submission handlers
        // easier
        if ($this->bail_out !== null) {
            return $this->bail_out;
        }

        $this->loadData();
        $result = $_POST && $this->submitted();
        $this->bail_out = $result;

        return $result;
    }
    public function onSubmit($callback)
    {
        $this->addHook('submit', $callback);
        $this->isSubmitted();
    }
    public function setLayout($template)
    {
        if (!$template instanceof AbstractView) {
            if (is_string($template)) {
                $template = $this->add('View', null, null, array($template));
            } else {
                $template = $this->add('View', null, null, $template);
            }
        }

        $this->layout = $template;

        return $this;
    }
    public function render()
    {
        // Assuming, that child fields already inserted their HTML code into 'form'/Content using 'form_line'
        // Assuming, that child buttons already inserted their HTML code into 'form'/form_buttons

        if ($this->js_widget) {
            $fn = str_replace('ui.', '', $this->js_widget);
            $this->js(true)->_load($this->js_widget)->$fn($this->js_widget_arguments);
        }

        return parent::render();
    }
    /**
     * @deprecated 4.3.2 use getElement() instead
     */
    public function hasField($name)
    {
        if (!@$this->app->compat_42) {
            throw $this->exception('Use $form->hasElement instead', '_Obsolete');
        }

        return isset($this->elements[$name]) ? $this->elements[$name] : false;
    }
    public function isClicked($name)
    {
        if (is_object($name)) {
            $name = $name->short_name;
        }

        return $_POST['ajax_submit'] == $name || isset($_POST[$this->name.'_'.$name]);
    }
    /* external error management */
    public function setFieldError($field, $name)
    {
        if (!$this->app->compat_42) {
            throw $this->exception('4.3', '_Obsolete');
        }
        $this->errors[$field] = (isset($this->errors[$field]) ? $this->errors[$field] : '').$name;
    }

    public function validate($rule)
    {
        if (!$this->validator) {
            $this->validator = $this->add('Controller_Validator');
            /** @type Controller_Validator $this->validator */
            $this->validator->on('post-validate');
        }
        $this->validator->is($rule);
    }

    /**
     * @deprecated 4.3.2 Will be removed in 4.4
     */
    public function addClass($class)
    {
        if ($class == 'stacked' || $class == 'atk-form-stacked') {
            // there are no longer stacked forms, instead a separate template must be used
            $this->template->loadTemplate('form/stacked');
            $this->getChunks();
            $this->template->trySet('_name', $this->getJSID());

            return $this;
        } else {
            return parent::addClass($class);
        }
    }
}
