<?php
/**
 * Class implementing field of relational database. One object is created
 * for every field in a model. Essentially this object is responsible for
 * storing information about meta-information and assisting model in
 * query creation where particular field is included.
 */
class Field extends AbstractModel
{
    public $type = 'string';
    public $readonly = false;
    public $system = false;
    public $hidden = false;
    public $editable = true;
    public $visible = true;
    public $display = null;
    public $caption = null;
    public $placeholder = null;
    public $hint = null;
    public $group = null;
    public $allowHTML = false;
    public $sortable = false;
    public $searchable = false;
    public $mandatory = false;
    public $has_default_value = false;
    public $defaultValue = null;
    public $emptyText = null;
    public $auto_track_element = true;
    public $listData = null;
    public $theModel = null;

    public $relation = null;
    public $actual_field = null;

    public $onField = null;

    /**
     * Joins
     * @see from()
     * @var array
     */
    public $relations = array();

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

    /**
     * Implementation of generic setter-getter method which supports "UNDEFINED"
     * constant. This method is used by all other sette-getters.
     *
     * @param string $type  Corresponds to the name of property of a field
     * @param mixed  $value New value for a property.
     *
     * @return mixed|$this new or current property (if value is UNDEFINED)
     */
    public function setterGetter($type, $value = UNDEFINED)
    {
        if ($value === UNDEFINED) {
            return isset($this->$type) ? $this->$type : null;
        }
        $this->$type = $value;

        return $this;
    }

    /**
     * Sets the value of the field. Identical to $model[$fieldname]=$value.
     *
     * @param mixed $value new value
     *
     * @return $this
     */
    public function set($value = null)
    {
        $this->owner->set($this->short_name, $value);

        return $this;
    }

    /**
     * Get the value of the field of a loaded model. If model is not loaded
     * will return default value instead.
     *
     * @return mixed current value of a field
     */
    public function get()
    {
        if ($this->owner->loaded()
            || isset($this->owner->data[$this->short_name])
        ) {
            return $this->owner->get($this->short_name);
        }

        return $this->defaultValue();
    }

    /**
     * If field is accidentally converted to string, provide some
     * descriptive information.
     *
     * @return string descriptive
     */
    public function __toString()
    {
        return get_class($this)." ['".$this->short_name."']".' of '.$this->owner;
    }

    /**
     * Logical type of model field. This universal type is recognized by
     * view controllers (such as Controller_MVCForm, Controller_MVCGrid to
     * convert into supported field types.
     *
     * @param string $t new value
     *
     * @return string|$this current value if $t=UNDEFINED
     */
    public function type($t = UNDEFINED)
    {
        return $this->setterGetter('type', $t);
    }

    /**
     * Sets field caption which will be used by forms, grids and other view
     * elements as a label. The caption will be localized through $app->_().
     *
     * @param string $t new value
     *
     * @return string|$this current value if $t=UNDEFINED
     */
    public function caption($t = UNDEFINED)
    {
        if ($t === UNDEFINED && !$this->caption) {
            return ucwords(strtr(
                preg_replace('/_id$/', '', $this->short_name),
                '_',
                ' '
            ));
        }

        return $this->setterGetter('caption', $t);
    }

    /**
     * Sets field hint which will be used by forms.
     *
     * @param string $t new value
     *
     * @return string|$this current value if $t=UNDEFINED
     */
    public function hint($t = UNDEFINED)
    {
        return $this->setterGetter('hint', $t);
    }

    /**
     * Sets field placeholder (gray text inside input when it's empty).
     *
     * @param string $t new value
     *
     * @return string|$this current value if $t=UNDEFINED
     */
    public function placeholder($t = UNDEFINED)
    {
        return $this->setterGetter('placeholder', $t);
    }

    /**
     * While you may use visible(), editable() to include or exclude fields
     * from appearing in certain scenarios, you can also define a group which
     * you then can display instead of listing all fields manually inside
     * setModel(). Read more about Actual Fields.
     *
     * @param string $t new value
     *
     * @return string|$this current value if $t=UNDEFINED
     */
    public function group($t = UNDEFINED)
    {
        return $this->setterGetter('group', $t);
    }

    /**
     * Read only setting will affect the way how field is presented by views.
     * While model field is still writable directly, the Form will not try to
     * change the value of this field.
     *
     * @param bool $t new value
     *
     * @return bool|$this current value if $t=UNDEFINED
     */
    public function readonly($t = UNDEFINED)
    {
        return $this->setterGetter('readonly', $t);
    }

    /**
     * Mandatory setting will affect the way how field is presented by views.
     * For example, form can display asterisk near the field or add additional
     * validation.
     * This property will not affect the direct use of the field inside
     * model. If you would like that your model complains about empty fields,
     * you should edit beforeSave hook.
     *
     * @param bool $t new value
     *
     * @return bool|$this current value if $t=UNDEFINED
     */
    public function mandatory($t = UNDEFINED)
    {
        return $this->setterGetter('mandatory', $t);
    }

    /**
     * Set editable to false, if you want to exclude field from forms
     * or other means of editing data. This does not affect the actual model
     * values.
     *
     * @param bool $t new value
     *
     * @return bool|$this current value if $t=UNDEFINED
     */
    public function editable($t = UNDEFINED)
    {
        return $this->setterGetter('editable', $t);
    }

    /**
     * Configures the behavior of Form to disable tag stripping form user input.
     * By default all tags are stripped, setting this property to true will
     * no longer strip tags.
     *
     * @param bool $t new value
     *
     * @return bool|$this current value if $t=UNDEFINED
     */
    public function allowHTML($t = UNDEFINED)
    {
        return $this->setterGetter('allowHTML', $t);
    }

    /**
     * Setting searchable(true) will instruct Filter and similar views that
     * it should be possible to perform search by this field.
     *
     * @param bool $t new value
     *
     * @return bool|$this current value if $t=UNDEFINED
     */
    public function searchable($t = UNDEFINED)
    {
        return $this->setterGetter('searchable', $t);
    }

    /**
     * Will instruct Grid and similar views that the sorting controls must be
     * enabled for this field.
     *
     * @param bool $t new value
     *
     * @return bool|$this current value if $t=UNDEFINED
     */
    public function sortable($t = UNDEFINED)
    {
        return $this->setterGetter('sortable', $t);
    }

    /**
     * Normally views will attempt to pick most suitable way to present field.
     * For example, type='date' will be presented with DatePicker field in form.
     * You might be using add-ons or might have created your own field class.
     * If you would like to use it to present the field, use display(). If you
     * specify string it will be used by all views, otherwise specify it as
     * associtive array:.
     *
     *     $field->display(array('form'=>'line','grid'=>'button'));
     *
     * @param mixed $t new value
     *
     * @return mixed|$this current value if $t=UNDEFINED
     */
    public function display($t = UNDEFINED)
    {
        return $this->setterGetter('display', $t);
    }

    /**
     * In most cases $model['field'] would match "field" inside a database. In
     * some cases, however, you would want to use different database field. This
     * can happen when you join multiple tables and 'field' appears in multiple
     * tables.
     *
     * You can specify actual field when you declare a field within a model:
     *
     *     $model->addField('modelfield','dbfield');
     *
     * If you are unable to use addField (such as using custom field class),
     * you can use actual() modifier:
     *
     *     $model->add('filestore/File','modelfield')->actual('dbfield');
     *
     * Another potential use is if your database structure does not match
     * model convention:
     *
     *     $model->hasOne('Book')->actual('IDBOOK');
     *
     * @param string $t new value
     *
     * @return string|$this current value if $t=UNDEFINED
     */
    public function actual($t = UNDEFINED)
    {
        return $this->setterGetter('actual_field', $t);
    }

    /**
     * Marking field as system will cause it to always be loaded, even if
     * it's not requested through Actual Fields. It will also hide the field
     * making it dissapear from Grids and Forms. A good examples of system
     * fields are "id" or "created_dts".
     *
     * @param bool $t new value
     *
     * @return bool|$this current value if $t=UNDEFINED
     */
    public function system($t = UNDEFINED)
    {
        if ($t === true) {
            $this->editable(false)->visible(false);
        }

        return $this->setterGetter('system', $t);
    }

    /**
     * Hide field. Not sure!
     *
     * @param bool $t new value
     *
     * @return bool|$this current value if $t=UNDEFINED
     */
    public function hidden($t = UNDEFINED)
    {
        return $this->setterGetter('hidden', $t);
    }

    /**
     * This will provide a HTML settings on a field for maximum field size.
     * The length of a field will not be enforced by this setting.
     *
     * @param int $t new value
     *
     * @return int|$this current value if $t=UNDEFINED
     */
    public function length($t = UNDEFINED)
    {
        return $this->setterGetter('length', $t);
    }

    /**
     * Default Value is used inside forms when you present them without loaded
     * data. This does not change how model works, which will simply avoid
     * including unchanged field into insert/update queries.
     *
     * @param bool $t new value
     *
     * @return bool|$this current value if $t=UNDEFINED
     */
    public function defaultValue($t = UNDEFINED)
    {
        if ($t !== UNDEFINED) {
            $this->has_default_value = true;
        }

        return $this->setterGetter('defaultValue', $t);
    }

    /**
     * Controls field appearance in Grid or similar views.
     *
     * @param bool $t new value
     *
     * @return bool|$this current value if $t=UNDEFINED
     */
    public function visible($t = UNDEFINED)
    {
        return $this->setterGetter('visible', $t);
    }

    /**
     * Supplies a list data for multi-value fields (selects, radio buttons,
     * checkbox area, autocomplete). You may also use enum(). This setting
     * is typically used with a static falues (Male / Female), if your field
     * values could be described through a different model, use setModel()
     * or better yet - hasOne().
     *
     * @param array $t Array( id => val )
     *
     * @return array|$this current value if $t=UNDEFINED
     */
    public function listData($t = UNDEFINED)
    {
        if ($this->type() === 'boolean' && $t !== UNDEFINED) {
            $this->owner->addHook('afterLoad,afterUpdate,afterInsert', function ($m) use ($t) {
                // Normalize boolean data
                $val = !array_search($m->data[$this->short_name], $t);
                if ($val === false) {
                    return;
                } // do nothing
                $m->data[$this->short_name] = (boolean) $val;
            });

            $this->owner->addHook('beforeUpdate,beforeInsert', function ($m, &$data) use ($t) {
                // De-Normalize boolean data
                $val = (int) (!$m->get($this->short_name));
                if (!isset($t[$val])) {
                    return;
                }  // do nothing
                $data->set($this->short_name, $t[$val]);
            });
        }

        return $this->setterGetter('listData', $t);
    }

    /**
     * What to display when nothing is selected or entered? This will be
     * displayed on a drop-down when no value is selected: ("Choose ..")
     * if you are using this setting with a text field it will set a
     * placeholder HTML property.
     *
     * @param string $t new value
     *
     * @return string|$this current value if $t=UNDEFINED
     */
    public function emptyText($t = UNDEFINED)
    {
        return $this->setterGetter('emptyText', $t);
    }

    /**
     * Pass a callback to onField() which is executed with the argument
     * containing a field view, when model is associated with a form.
     *
     * @param callback $c new value
     *
     * @return string|$this current value if $t=UNDEFINED
     */
    public function onField($c = UNDEFINED)
    {
        return $this->setterGetter('onField', $c);
    }

    /**
     * Will execute setModel() on a field. Some fields will change their
     * behaviour with this. The value is a string (either Model_Book or Book)
     * but you might be able to use object also.
     *
     * I suggest to use $model->hasOne($model) instead of setModel($model)
     *
     * @param string $t new value
     *
     * @return string|$this current value if $t=UNDEFINED
     */
    public function setModel($t = UNDEFINED)
    {
        return $this->setterGetter('theModel', $t);
    }

    /**
     * Returns current model. This is different than other setters getters,
     * but it's done to keep consistency with the rest of Agile Toolkit.
     *
     * @return string current associated model Class
     */
    public function getModel()
    {
        return $this->theModel;
    }

    /**
     * Same as listData().
     *
     * @param array $t Array( id => val )
     *
     * @return array current value if $t=UNDEFINED
     */
    public function setValueList($t)
    {
        return $this->listData($t);
    }

    /**
     * Similar to listData() but accepts array of values instead of hash:.
     *
     *     listData(array(1=>'Male', 2=>'Female'));
     *     enum(array('male','female'));
     *
     * The value will be stored in database and also displayed to user.
     *
     * @param array $t Array( id => val )
     *
     * @return $this
     */
    public function enum($t)
    {
        return $this->listData(array_combine($t, $t));
    }

    /**
     * Binds the field to a relation (returned by join() function).
     *
     * @param mixed $m the result of join() function
     *
     * @return $this|object or the relation if $m is UNDEFINED
     */
    public function from($m = UNDEFINED)
    {
        if ($m === UNDEFINED) {
            return $this->relation;
        } elseif (is_object($m)) {
            $this->relation = $m;
        } else {
            $this->relations = $this->owner->relations[$m];
        }

        return $this;
    }

    /**
     * Modifies specified query to include this particular field.
     *
     * @param DB_dsql $select
     *
     * @return $this
     */
    public function updateSelectQuery($select)
    {
        $p = null;
        if (!empty($this->owner->relations)) {
            $p = $this->owner->table_alias ?: $this->owner->table;
        }

        if ($this->relation) {
            $select->field($this->actual_field ?: $this->short_name, $this->relation->short_name, $this->short_name);
        } elseif (!(is_null($this->actual_field)) && $this->actual_field != $this->short_name) {
            $select->field($this->actual_field, $p, $this->short_name);

            return $this;
        } else {
            $select->field($this->short_name, $p);
        }

        return $this;
    }

    /**
     * Modify insert query to set value of this field.
     *
     * @param DB_dsql $insert
     *
     * @return $this
     */
    public function updateInsertQuery($insert)
    {
        if ($this->relation) {
            $insert = $this->relation->dsql;
        }

        $insert->set(
            $this->actual_field ?: $this->short_name,
            $this->getSQL()
        );

        return $this;
    }

    /**
     * Modify update query to set value of this field.
     *
     * @param DB_dsql $modify
     *
     * @return $this
     */
    public function updateModifyQuery($modify)
    {
        if ($this->relation) {
            $modify = $this->relation->dsql;
        }

        $modify->set(
            $this->actual_field ?: $this->short_name,
            $this->getSQL()
        );

        return $this;
    }

    /**
     * Converts true/false into boolean representation according to the "enum".
     *
     * @param mixed $value
     *
     * @return int
     */
    public function getBooleanValue($value)
    {
        if ($value === null) {
            return;
        }
        if ($this->listData) {
            reset($this->listData);
            list(, $yes_value) = @each($this->listData);
            list(, $no_value) = @each($this->listData);
            if ($no_value === null) {
                $no_value = '';
            }
            /* not to convert N to Y */
            if ($yes_value == $value) {
                return $yes_value;
            }
            if ($no_value == $value) {
                return $no_value;
            }
        } else {
            $yes_value = 1;
            $no_value = 0;
        }

        return $value ? $yes_value : $no_value;
    }

    /**
     * Get value of this field formatted for SQL. Redefine if you need to convert.
     *
     * @return mixed
     */
    public function getSQL()
    {
        $val = $this->owner->get($this->short_name);
        if ($this->type == 'boolean') {
            $val = $this->getBooleanValue($val);
        }
        if ($val == ''
            && !$this->mandatory
            && ($this->listData || $this instanceof Field_Reference)
            && $this->type != 'boolean'
        ) {
            $val = null;
        }

        return $val;
    }

    /**
     * Returns field of this model.
     *
     * @return string
     */
    public function getExpr()
    {
        $q = $this->owner->_dsql();

        return $q->bt($this->relation ? $this->relation->short_name : $q->main_table).'.'.
            $q->bt($this->actual_field ?: $this->short_name);
    }



    // {{{ Deprecated methods

    /**
     * @deprecated 4.3.0 use mandatory() instead
     *
     * @param bool $t new value
     *
     * @return bool|$this current value if $t=UNDEFINED
     */
    public function required($t = UNDEFINED)
    {
        return $this->mandatory($t);
    }

    /** @deprecated 4.3.0 use hasOne instead */
    public function refModel($m)
    {
        if ($m == 'Model_Filestore_File') {
            return $this->add('filestore/Field_File');
        }
        $this->destroy();
        $fld = $this->add('Field_Reference');

        foreach ((Array) $this as $key => $val) {
            $fld->$key = $val;
        }

        return $this->owner->add($fld)->setModel(str_replace('Model_', '', $m));
    }

    /** @deprecated 4.3.0 use type($v) instead */
    public function datatype($v = UNDEFINED)
    {
        return $this->type($v);
    }

    /** @deprecated 4.3.0 use addExpression() instead */
    public function calculated($v = UNDEFINED)
    {
        throw $this->exception('calculated() field property is obsolete. Use addExpression() instead');
    }

    // }}}
}
