<?php

class PhonalisaRecord extends CModel
{

    public $id;
    public $primaryKey;
    public $isNewRecord = true;
    private $finderQueryCriteria; // query criteria (used by finder only)
    private $allValues = array();
    private $overWrittenValues = array();
    private static $modelCache = array();

    /**
     * An array of static model instances, class name => model
     * @var array
     */
    public static function myAttributes()
    {
        return array();
    }

    public function init()
    {
        
    }

    /**
     * Returns if the current record is new.
     * @return boolean whether the record is new and should be inserted when calling {@link save}.
     * This property is automatically set in constructor and {@link populateRecord}.
     * Defaults to false, but it will be set to true if the instance is created using
     * the new operator.
     */
    public function getIsNewRecord()
    {
        return $this->isNewRecord;
    }

    /**
     * Sets if the record is new.
     * @param boolean $value whether the record is new and should be inserted when calling {@link save}.
     * @see getIsNewRecord
     */
    public function setIsNewRecord($value)
    {
        $this->isNewRecord = $value;
    }

    /**
     * Returns the list of all attribute names of the model.
     * This would return all column names of the table associated with this AR class.
     * @return array list of attribute names.
     */
    public function attributeNames()
    {
        return array_keys($this->myAttributes());
    }

    /**
     * Constructor.
     * @param string $scenario scenario name. See {@link CModel::scenario} for more details about this parameter.
     */
    public function __construct($scenario = "insert")
    {
        if ($scenario === null) {
            return;
        }

        $this->setScenario($scenario);
        $this->setIsNewRecord(true);

        $this->init();
        $this->attachBehaviors($this->behaviors());
        $this->afterConstruct();
    }

    protected function instantiate($attributes)
    {
        $class = get_class($this);
        $model = new $class(null);
        return $model;
    }

    public function primaryKey()
    {
        return "primaryKey";
    }

    public function getPrimaryKey()
    {
        return $this->primaryKey;
    }

    public function populateRecord($attributes, $callAfterFind = true)
    {
        if ($attributes !== false) {
            $record = $this->instantiate($attributes);
            $record->setScenario('update');
            $record->init();
            foreach ($attributes as $name => $value) {
                if (property_exists($record, $name)) {
                    $record->$name = $value;
                }
            }
            $record->attachBehaviors($record->behaviors());
            if ($callAfterFind) {
                $record->afterFind();
            }
            return $record;
        } else {
            return null;
        }
    }

    protected function afterFind()
    {
        if ($this->hasEventHandler('onAfterFind')) {
            $this->onAfterFind(new CEvent($this));
        }
    }

    public function valueForKey($key, $default = "")
    {
        if (property_exists($this, $key)) {
            return $this->$key;
        }

        if (array_key_exists($key, $this->allValues)) {
            return @$this->allValues[$key];
        } else {
            return $default;
        }
    }

    public function setValueForKey($key, $value = "")
    {
        $this->allValues[$key] = $value;
        $this->overWrittenValues[$key] = $value;
        if (property_exists($this, $key)) {
            $this->$key = $value;
        }
    }

    public function findByPk($pk)
    {
        
        if (empty($pk)) {
            if (app()->params->devMode) { 
                Log::w(get_class($this).".findByPk(null)", "PhonalisaRecord");
            }
            return null;
        }
        
        $time = microtime(true);
        $this->beforeFind();
        $values = array();
        $obj = PhonalisaDBConnector::findByPK(get_class($this), $pk);
        if ($obj->status != "ok") {
            return null;
        }

        $sourceAttributes = (array) @$obj->result->attributes;
        foreach ($this->attributeNames() as $key) {
            if (array_key_exists($key, $sourceAttributes)) {
                $values[$key] = @$sourceAttributes[$key];
            } else {
                $values[$key] = null;
            }
        }

        $model = $this->populateRecord($values, true);
        $model->allValues = (array) @$obj->result->attributes;

        $model->primaryKey = $pk;
        $model->id = $pk;
        $model->setIsNewRecord(false);

        if (app()->params->devMode) { 
            Yii::log(get_class($this) . '.findByPk() duration: ' . (microtime(true) - $time), "error");
        }

        return $model;
    }

    /**
     * Finds all active records satisfying the specified condition.
     * See {@link find()} for detailed explanation about $condition and $params.
     * @param mixed $condition query condition or criteria.
     * @param array $params parameters to be bound to an SQL statement.
     * @return array list of active records satisfying the specified condition.
     * An empty array is returned if none is found.
     */
    public function findAll($criteria = array(), $params = array())
    {
        $time = microtime(true);
        $models = array();

        $limit = 1000;
        $offset = 0;

        if (isset($params['limit'])) {
            $limit = $params['limit'];
        }

        if (isset($params['offset'])) {
            $offset = $params['offset'];
        }

        $objs = PhonalisaDBConnector::findAll(get_class($this), $criteria, $limit, $offset);
        if ($objs == null) {
            return $models;
        }

        foreach ($objs->result as $pk => $model) {
            $newRecord = $this->populateRecord($model->attributes, true);
            $newRecord->allValues = (array) @$model->attributes;
            $newRecord->primaryKey = $pk;
            $newRecord->id = $pk;
            $newRecord->setIsNewRecord(false);
            $models[] = $newRecord;
        }
        Yii::log(get_class($this) . '.findAll() duration: ' . (microtime(true) - $time), "error");
        return $models;
    }

    public function find($attributes, $sort, $limit = 1000, $offset = 0 /* ,$condition='',$params=array() */)
    {
        Log::d("find: " . get_class($this));
        $time = microtime(true);
        $models = array();
        Yii::trace(get_class($this) . '.findAllByAttributes()', 'system.db.ar.CActiveRecord');
        $objs = PhonalisaDBConnector::find(get_class($this), $attributes, $sort, $limit, $offset);

        if ($objs == null) {
            return array();
        }

        foreach ($objs->result as $id => $model) {
            $newRecord = $this->populateRecord($model->attributes, true);
            $newRecord->allValues = (array) @$model->attributes;
            $newRecord->primaryKey = $model->id;
            $newRecord->id = $model->id;
            $newRecord->setIsNewRecord(false);
            $models[$id] = $newRecord;
        }
        ksort($models);
        Yii::log(get_class($this) . '.findAllByAttributes() duration: ' . (microtime(true) - $time), "error");
        return $models;
    }

    public function findAllByAttributes($attributes, $limit = 1000, $offset = 0 /* ,$condition='',$params=array() */)
    {
        $time = microtime(true);
        $models = array();
        Yii::trace(get_class($this) . '.findAllByAttributes()', 'system.db.ar.CActiveRecord');

        $objs = PhonalisaDBConnector::findAllByAttributes(get_class($this), $attributes, $limit, $offset);
        if ($objs == null) {
            return array();
        }
        foreach ($objs->result as $pk => $model) {
            $newRecord = $this->populateRecord($model->attributes, true);
            $newRecord->allValues = (array) @$model->attributes;
            $newRecord->primaryKey = $pk;
            $newRecord->id = $pk;
            $newRecord->setIsNewRecord(false);
            $models[] = $newRecord;
        }
        Yii::log(get_class($this) . '.findAllByAttributes() duration: ' . (microtime(true) - $time), "error");
        return $models;
    }

    public function countAllByAttributes($attributes)
    {
        return $this->countByAttributes($attributes);
    }

    public function countByAttributes($attributes)
    {
        $result = PhonalisaDBConnector::count(get_class($this), $attributes);
        return $result->count;
    }

    public function findByAttributes($attributes)
    {
        $time = microtime(true);
        Yii::trace(get_class($this) . '.findAllByAttributes()', 'system.db.ar.CActiveRecord');

        $objs = PhonalisaDBConnector::findAllByAttributes(get_class($this), $attributes, 1);

        foreach ($objs->result as $pk => $model) {
            $newRecord = $this->populateRecord($model->attributes, true);
            $newRecord->allValues = (array) @$model->attributes;
            $newRecord->primaryKey = $pk;
            $newRecord->id = $pk;
            $newRecord->setIsNewRecord(false);
            return $newRecord;
        }
        
        $duration = (microtime(true) - $time);
        
        Yii::log(get_class($this) . '.findByAttributes() duration: ' . $duration, "error");
        return null;
    }

    /**
     * Returns the query criteria associated with this model.
     * @param boolean $createIfNull whether to create a criteria instance if it does not exist. Defaults to true.
     * @return CDbCriteria the query criteria that is associated with this model.
     * This criteria is mainly used by {@link scopes named scope} feature to accumulate
     * different criteria specifications.
     */
    public function getDbCriteria($createIfNull = true)
    {
        if ($this->finderQueryCriteria === null) {
            if (($c = $this->defaultScope()) !== array() || $createIfNull) {
                $this->finderQueryCriteria = new CDbCriteria($c);
            }
        }
        return $this->finderQueryCriteria;
    }

    /**
     * Returns the default named scope that should be implicitly applied to all queries for this model.
     * Note, default scope only applies to SELECT queries. It is ignored for INSERT, UPDATE and DELETE queries.
     * The default implementation simply returns an empty array. You may override this method
     * if the model needs to be queried with some default criteria (e.g. only active records should be returned).
     * @return array the query criteria. This will be used as the parameter to the constructor
     * of {@link CDbCriteria}.
     */
    public function defaultScope()
    {
        return array();
    }

    /**
     * Sets the query criteria for the current model.
     * @param CDbCriteria $criteria the query criteria
     * @since 1.1.3
     */
    public function setDbCriteria($criteria)
    {
        $this->finderQueryCriteria = $criteria;
    }

    public static function model($className = null)
    {
        $className = $className == null ? get_called_class() : $className;

        if (isset(self::$modelCache[$className])) {
            return self::$modelCache[$className];
        } else {
            $model = self::$modelCache[$className] = new $className(null);
            $model->attachBehaviors($model->behaviors());
            return $model;
        }
    }

    /**
     * This event is raised before the record is saved.
     * By setting {@link CModelEvent::isValid} to be false, the normal {@link save()} process will be stopped.
     * @param CModelEvent $event the event parameter
     */
    public function onBeforeSave($event)
    {
        $this->raiseEvent('onBeforeSave', $event);
    }

    /**
     * This method is invoked before saving a record (after validation, if any).
     * The default implementation raises the {@link onBeforeSave} event.
     * You may override this method to do any preparation work for record saving.
     * Use {@link isNewRecord} to determine whether the saving is
     * for inserting or updating record.
     * Make sure you call the parent implementation so that the event is raised properly.
     * @return boolean whether the saving should be executed. Defaults to true.
     */
    protected function beforeSave()
    {
        if ($this->hasEventHandler('onBeforeSave')) {
            $event = new CModelEvent($this);
            $this->onBeforeSave($event);
            return $event->isValid;
        } else {
            return true;
        }
    }

    public function save($runValidation = true, $attributes = null)
    {
        $time = microtime(true);
        if (!$this->beforeSave()) {
            return false;
        }


        if ($runValidation && !$this->validate($attributes)) {
            return false;
        }

//        if ($this->validate($attributes)) {
        $saveValues = $this->attributes;

        foreach ($this->overWrittenValues as $key => $val) {
            if (!array_key_exists($key, $this->attributes)) {
                $saveValues[$key] = $val; // set these Attributes
            }
        }

        // overWritting NULL with '' for search function in DB
        foreach ($saveValues as $key => $value) {
            if ($saveValues[$key] === null) {
                $saveValues[$key] = '';
            }
        }

        $result = PhonalisaDBConnector::save(get_class($this), $saveValues, $this->primaryKey);
        if ($result->status == "ok") {
            $this->primaryKey = $result->id;
            $this->id = $result->id;
        } else {
            return false;
        }
//        } else {
//            return false;
//        }

        $this->afterSave();

        Yii::log(get_class($this) . '.save() duration: ' . (microtime(true) - $time), "error");
        return true;
    }

    public function saveFileToFieldByPath($field, $filePath)
    {
        $fileHandle = fopen($filePath, "rb");
        if ($fileHandle === false) {
            return false;
        }

        return $this->saveFileToField($field, $fileHandle, filesize($filePath));
    }

    //needs a valid file pointer!
    public function saveFileToField($field, $fp, $size)
    {

        $data = fread($fp, $size);
        return $this->saveBinaryField($field, $data, $size);
    }

    public function saveBinaryField($field, $data, $length)
    {

        $time = microtime(true);
        $result = PhonalisaDBConnector::saveBinaryField(get_class($this), $this->primaryKey, $field, $data, $length);
        if ($result->status != "ok") {
            return false;
        }

        Yii::log(get_class($this) . '.saveBinaryField() duration: ' . (microtime(true) - $time), "error");
        return true;
    }

    public function getBinaryField($field, &$data, &$length)
    {

        $time = microtime(true);
        $result = PhonalisaDBConnector::getBinaryField(get_class($this), $this->primaryKey, $field, $data, $length);
        if ($result->status != "ok") {
            return false;
        }

        Yii::log(get_class($this) . '.saveBinaryField() duration: ' . (microtime(true) - $time), "error");
        return true;
    }

    public function deleteBinaryField($field, $data = '', $length = 0)
    {
        $time = microtime(true);
        $result = PhonalisaDBConnector::deleteBinaryField(get_class($this), $this->primaryKey, $field, $data, $length);
        if ($result->status != "ok") {
            return false;
        }

        Yii::log(get_class($this) . '.deleteBinaryField() duration: ' . (microtime(true) - $time), "error");
        return true;
    }

    /**
     * This event is raised after the record is saved.
     * @param CEvent $event the event parameter
     */
    public function onAfterSave($event)
    {
        $this->raiseEvent('onAfterSave', $event);
    }

    /**
     * This method is invoked after saving a record successfully.
     * The default implementation raises the {@link onAfterSave} event.
     * You may override this method to do postprocessing after record saving.
     * Make sure you call the parent implementation so that the event is raised properly.
     */
    protected function afterSave()
    {
        if ($this->hasEventHandler('onAfterSave')) {
            $this->onAfterSave(new CEvent($this));
        }
    }

    public function onBeforeDelete($event)
    {
        $this->raiseEvent('onBeforeDelete', $event);
    }

    protected function beforeDelete()
    {
        if ($this->hasEventHandler('onBeforeDelete')) {
            $event = new CModelEvent($this);
            $this->onBeforeDelete($event);
            return $event->isValid;
        } else {
            return true;
        }
    }

    public function delete()
    {

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

        $result = PhonalisaDBConnector::delete(get_class($this), $this->primaryKey);
        if ($result->status == "ok") {
            $this->afterDelete();
            return true;
        } else {
            return false;
        }
    }

    public function onAfterDelete($event)
    {
        $this->raiseEvent('onAfterDelete', $event);
    }

    protected function afterDelete()
    {
        if ($this->hasEventHandler('onAfterDelete')) {
            $this->onAfterDelete(new CEvent($this));
        }
    }

    public function onBeforeFind($event)
    {
        $this->raiseEvent('onBeforeFind', $event);
    }

    public function onAfterFind($event)
    {
        $this->raiseEvent('onAfterFind', $event);
    }

    protected function beforeFind()
    {
        if ($this->hasEventHandler('onBeforeFind')) {
            $event = new CModelEvent($this);
            $this->onBeforeFind($event);
        }
    }

    public function count($criteria = '', $params = array())
    {
        $attributes = array();
        if (@$criteria->condition != "") {
            $conditions = explode(" AND ", $criteria->condition);
            foreach ($conditions as $condition) {
                if (!empty($condition)) {
                    $keyAndValue = explode("=", $condition);
                    $attributes[trim($keyAndValue[0])] = trim($keyAndValue[1]);
                }
            }
        }

        $res = PhonalisaDBConnector::count(get_class($this), $attributes);
        return $res->count;
    }

    // for relations between "models" see adminGroups for example
    public function with()
    {
        if (func_num_args() > 0) {
            $with = func_get_args();
            if (is_array($with[0])) { // the parameter is given as an array
                $with = $with[0];
            }

            if (!empty($with)) {
                $this->getDbCriteria()->mergeWith(array('with' => $with));
            }
        }
        return $this;
    }

    public function isNewRecord()
    {
        if ($this->id != null) {
            return false;
        } else {
            return true;
        }
    }

    public function getModelName()
    {
        return get_class($this);
    }

    public function getFilter($criteria, $order)
    {
        if (isset($_REQUEST[get_class($this)])) {
            $condition = '';
            foreach ($_REQUEST[get_class($this)] as $attributeName => $value) {
                $value = str_replace('*', '', $value);
                if ($value != '') {
                    $value = $this->getConvertedSearchValue($value, $attributeName);
                    $condition = $this->getSearchConditions($condition, $attributeName, $value);
                }
            }
            $criteria->condition = $this->getCriteriaConditions($condition, $criteria->condition);
            $criteria->order = $order;
        } else {
            if (!@$_REQUEST['sort'] && !isset($_REQUEST[get_class($this)])) {
                $criteria->order = $order;
            }
        }
        return array('criteria' => $criteria);
    }

    protected function getConvertedSearchValue($value, $attributeName)
    {
        if ($this->isDateTimeAttribute($attributeName)) {
            $value = $this->convertValueToDateTime($value);
        }
        return $value;
    }

    protected function isDateTimeAttribute($attributeName)
    {
        $myAttributes = $this->myAttributes();

        if (!isset($myAttributes[$attributeName]['type'])) {
            return false;
        }

        if ($myAttributes[$attributeName]['type'] == 'dateTime') {
            return true;
        }
        return false;
    }

    protected function convertValueToDateTime($searchValue)
    {
        $converter = new PlDateInputConverter($searchValue);
        return $converter->toDate();
    }

    protected function getFormatedDateTimeIfSearchForTime($searchValue, $dateTimeFormat)
    {
        $dateTimeFormat.= ' H:i';
        if (substr_count($searchValue, ':') > 1) {
            $dateTimeFormat.= ':s';
        }
        $dateTime = PhonalisaDate::changeSearchRequestDateTimeToServerFormat($searchValue, $dateTimeFormat);

        if (get_class($this) == 'CDR') {
            $dateTime = str_replace(' ', 'T', $dateTime);
        }
        return $dateTime;
    }

    protected function getSearchConditions($condition, $attributeName, $value)
    {
        if ($condition === '') {
            $condition .= $attributeName . '=' . implode('*|', permute($value)) . '*';
        } else {
            $condition .= ' AND ' . $attributeName . '=' . implode('*|', permute($value)) . '*';
        }
        return $condition;
    }

    protected function getCriteriaConditions($condition, $criteriaConditions)
    {
        if ($condition != '') {
            if ($criteriaConditions) {
                $criteriaConditions.= ' AND ' . $condition;
            } else {
                $criteriaConditions = $condition;
            }
        }
        return $criteriaConditions;
    }

    /**
     * Saves a selected list of attributes.
     * Unlike {@link save}, this method only saves the specified attributes
     * of an existing row dataset and does NOT call either {@link beforeSave} or {@link afterSave}.
     * Also note that this method does neither attribute filtering nor validation.
     * So do not use this method with untrusted data (such as user posted data).
     * You may consider the following alternative if you want to do so:
     * <pre>
     * $postRecord=Post::model()->findByPk($postID);
     * $postRecord->attributes=$_POST['post'];
     * $postRecord->save();
     * </pre>
     * @param array $attributes attributes to be updated. Each element represents an attribute name
     * or an attribute value indexed by its name. If the latter, the record's
     * attribute will be changed accordingly before saving.
     * @return boolean whether the update is successful
     * @throws CException if the record is new or any database error
     */
    public function saveAttributes($attributes)
    {
        if (!$this->getIsNewRecord()) {
            Yii::trace(get_class($this) . '.saveAttributes()', 'system.db.ar.CActiveRecord');
            $values = array();
            foreach ($attributes as $name => $value) {
                if (is_integer($name)) {
                    $values[$value] = $this->$value;
                } else {
                    $values[$name] = $this->$name = $value;
                }
            }

            $result = PhonalisaDBConnector::save(get_class($this), $this->attributes, $this->primaryKey);
            if ($result->status == "ok") {
                $this->primaryKey = $result->id;
                $this->id = $result->id;
            } else {
                return false;
            }

            return true;
        } else {
            throw new CDbException(Yii::t('yii', 'The active record cannot be updated because it is new.'));
        }
    }

    /**
     * @return string the associated database table name
     */
    public function tableName()
    {
        return lcfirst(get_class($this));
    }

    public function getIsSavedRecord()
    {
        return $this->isNewRecord();
    }

    public function getFormatedErrors()
    {
        $errrors = $this->errors;
        $formatedErrors = array();

        foreach ($errrors as $errKey => $errMsg) {
            $key = $this->getAttributeLabel($errKey);
            $formatedErrors[$key] = $errMsg;
        }
        return $formatedErrors;
    }
}
