<?php
    
class cobject {
    var $obj_name;
    var $attrs = array();
    var $attrs_vals  = array();
    var $id = 0;
    var $errors = array();
    var $global_type = 0; // 0 - локальный, 1 - глобальный
    var $recursion_depth = 0;
    var $force_hidden = array();
    var $sql_where; // Глобальное ограничение на объект
    var $save_sql = '';
    var $diffed = array();
    var $exceptPrimaryKeys = ['_sys_cat' => 'cat_id', // объекты где основной ключ не ID при сохранении
                              '_sys_cat_admin' => 'cat_id'];

    
    /**
     * Конструктор
     * @param string  $obj_name имя таблицы в DB
     * @param integer $depth    текущия глубина рекурсии (для предотвращения бесконечной рекурсии)
     */
    function __construct( $obj_name, $depth = 0 ) {
        global $statistica;
        
        $statistica['objects_add']++;
        $this->recursion_depth = ($depth+1);
        $this->obj_name = $obj_name;
        // Определяем тип объекта по имени
    //if ( strpos($obj_name, '_sys_') !== false ) {
        // All objects is local in basic mode
            $this->global_type = 1;
        //}

         if ($depth <= 2 ) { // Не более 3х вложений!!
                $this->getAttrs();
        }
        
        $this->attrs_vals = array();
    }
    
    /**
     * Получить экземпляр базы данных, связанный с объектом
     * @return [type] [description]
     */
    function getDB() {
        global $db, $core_db;
        
        if ( $this->global_type == 1 ) {
            return $core_db;
        } else {
            return $db;
        }
    }

    /**
     * Pагружаем атрибуты в объект
     * @return [type] [description]
     */
    private function getAttrs() {
        global $objects_cache;
        $curdb = $this->getDB();
        if ( $objects_cache[$this->obj_name] ) {
            foreach( $objects_cache[$this->obj_name] as $name => $arr ) {
                        $this->attrs[$name]['name']  = $name;
                        $this->attrs[$name]['type']  = $arr['attr_type'];
                        $this->attrs[$name]['templ'] = str_replace('[', '{', str_replace(']', '}', $arr['attr_templ']));
                        $this->attrs[$name]['desc']  = $arr['attr_desc'];
                        $this->attrs[$name]['hide']  = $arr['attr_hide'];
                        $this->attrs[$name]['mand']  = $arr['attr_mand'];
                        $this->attrs[$name]['attr_obj'] = $this->getAttrObj( $name,  $arr['attr_type'] );
            }
                        
        } else {
            $db2 = $curdb->q("SELECT * FROM `_sys_datatypes` WHERE `obj_name` = '".$this->obj_name."' AND !`_sys_deleted` ORDER BY `attr_order` ASC");   
                while( $db2->r() ) {
                    $name=$db2->f('attr_name');
                    $objects_cache[$this->obj_name][$name] = $db2->lastRow;
                    $this->attrs[$name]['name']  = $name;
                    $this->attrs[$name]['type']  = $db2->f('attr_type');
                    $this->attrs[$name]['templ'] = str_replace('[', '{', str_replace(']', '}', $db2->f('attr_templ')));
                    $this->attrs[$name]['desc']  = $db2->f('attr_desc');
                    $this->attrs[$name]['hide']  = $db2->f('attr_hide');
                    $this->attrs[$name]['mand']  = $db2->f('attr_mand');
                    $this->attrs[$name]['attr_obj'] = $this->getAttrObj( $name, $db2->f('attr_type') );
            }
        }
    }
    
    /**
     * Получить объект атрибута
     * @param  string $attr_name имя атрибута
     * @param  string $attr_type тип атрибута
     * @return object           
     */
    function getAttrObj( $attr_name, $attr_type ) {
        @list($type, $obj_link) = explode( '|', $attr_type );
        $classname = 'attr_' . $type;
        if ( class_exists( $classname ) ) { 
            $attr_obj = new $classname( $attr_name, $this);
            return $attr_obj;
        } else {
            die( 'Заявленный класс "' . $classname . '" - не определен'  );    
        }
    }
    
    
    /**
     * Расширенный конструктор SQL запросов
     * @param  string  $sql_from     что берём
     * @param  string  $sql_where    с каким where ограничением
     * @param  integer $show_deleted забираем ли удалённые объекты (по умолчанию нет)
     * @return string                SQL запрос
     */
    function getSelectEx( $sql_from ="*", $sql_where = "", $show_deleted = 0) {
        $sql = "SELECT " . $sql_from;
        $sql .= " FROM `" . $this->obj_name . "` WHERE 1 ";
        if (!$show_deleted && isset($this->attrs['_sys_deleted']) ) {
          $sql .= " AND !`".$this->obj_name."`.`_sys_deleted` ";
        }
        $sql .= $sql_where;
        return $sql;
    }
    
    /**
     * Конструктор SQL запросов
     * @param  string  $sql_where_in where ограничение
     * @param  integer $show_deleted забираем ли удалённые объекты (по умолчанию нет)
     * @return string                SQL запрос
     */
    function getSelect($sql_where_in = "", $show_deleted = 0) {
        global $auth, $cat;
        $sql_joins = '';
        
        /* Access deleted */
        $sql_where = '';
        if (!$show_deleted && isset($this->attrs['_sys_deleted']) ) {
          $sql_where .= " AND !`".$this->obj_name."`.`_sys_deleted` ";
        }

        /* Access Group */
        if ( isset($this->attrs['access_group']) && ( $auth->user_id != _ROOT_USER_ID ) ) {
            if ( !$auth->is_login() ) {
                  $sql_where .= " AND !`".$this->obj_name."`.`access_group` ";
            } else {
                $tmp_sql[] = ' !`'.$this->obj_name.'`.`access_group` ';
                foreach( $auth->groups as $v ) {
                  $tmp_sql[] = ' FIND_IN_SET('.$v.',`'.$this->obj_name.'`.`access_group`) ';
                }
                if ($tmp_sql ) {
                  $sql_where .= " AND (" . implode(" OR ", $tmp_sql ) . " ) ";
                } else {
                  $sql_where .= " AND !`".$this->obj_name."`.`access_group` ";
                }
            }
                
        }
        
        /* access geo */
        if ( isset($this->attrs['access_city']) && !defined('_ADMINKA_') ) {
             global $geoip;
             $sql_where .= " AND ( FIND_IN_SET(".$geoip->city_id.", `".$this->obj_name."`.`access_city` ) OR `".$this->obj_name."`.`access_city` = '' ) ";
        }
        /* Global where */
        if ( $this->sql_where ) {
            $sql_where = $this->sql_where .' '. $sql_where;
        }
        
        
        /* incoming arg */
        $sql_where .= $sql_where_in;
        
        /* SELECT */
        $sql = "SELECT ";
        $sqls[] = " `".$this->obj_name."`.`id` ";
        foreach( $this->attrs as $k => $arr ) {
               $sqls[] = " `".$this->obj_name."`.`".$k."` ";
        }
        $sql .= implode(",", $sqls);
        $sql .= " FROM `" . $this->obj_name . "` " . $sql_joins . " WHERE 1 ";

        
        $sql .= $sql_where;
        return $sql;
    }
    

    /**
     * Загружаем данные в объект по ID
     * @param  integer  $id      ID из таблицы
     * @param  boolean $usecache Использовать ли кэш
     */
    function byId( $id, $usecache = true ) {
        global $cached_objects;

        $this->diffed = array();
        $this->save_sql = '';


        if ($cached_objects[$this->obj_name][$id] && $usecache) {
            $this->attrs_vals = $cached_objects[$this->obj_name][$id];
            $this->id = $id;
        } else {
            $curdb = $this->getDB();
            $db2 = $curdb->q( $this->getSelect( ' AND `'. $this->obj_name.'`.`id` = "'.$id.'" ', 1 ) );       
            $this->attrs_vals = array();
            $this->id = 0;
            if ( $db2->r() ) {
                foreach( $this->attrs as $k => $arr ) {
                    $this->attrs_vals[$k] = $db2->f( $k );
                        
                }
                $this->id = $id;
                    $cached_objects[$this->obj_name][$id] = $this->attrs_vals;
            } else {
                return false;   
            }
        }
    }


    /**
     * Сохраняет объект
     * @param  boolean $do_log  логировать ли сохранение
     * @param  boolean $delayed отложенный UPDATE/INSERT
     */
    function save($do_log = true, $delayed = false ) {

        $curdb = $this->getDB();


        /* только измененные атрибуты */   
        if ( empty( $this->diffed ) ) return false;
        $this->diffed = array_unique($this->diffed);

        foreach( $this->diffed as $key ) {
             if ( $this->attrs[$key] ) {
                $this->attrs[$key]['attr_obj']->presave();
             }
        }
        
        foreach(  $this->diffed as $key ) {
             $val = $this->attrs_vals[$key];
             if (is_array($val)) { 
                $val = trim_array($val);
                $val = implode(',', $val);
             }
             $val = db_escape_string( $val );
             $update[] = ' `'.$this->obj_name.'`.`'.$key.'` = "' . $val . '" ' ;
             $insert[$key] = $val;
        }   


        // Расширенный лог для товаров        
        $this->itemObjLog();
        
        if ( $this->id ) {
            
            if ($do_log) {
                if ($this->global_type == 1 ) {
                    $db2 = $curdb->q('SELECT * FROM `'.$this->obj_name.'` WHERE `id` = ' . intval( $this->id ) );
                    if ( $db2->nr() ) {
                            $old_d = $db2->lastRow;
                            $new_d =  $this->attrs_vals;
                            foreach( $old_d as $k=>$v ) {
                                if ( $k == 'id' ) continue;
                                if ( $k == 'full' ) continue;
                                if ( $new_d[$k] != $v ) {
                                    $s_old[$k] = $v;    
                                    $s_new[$k] = $new_d[$k];    
                                }
                            }
                            if ( $s_old || $s_new ) {
                                global $auth;
                                $db2->q("INSERT INTO `_sys_obj_log` (`datetime`,`user_id`,`obj_name`,`obj_id`,`old_data`,`user_ip`,`new_data`)
                                         VALUES ('".time()."','".$auth->user_id."','".$this->obj_name."','".$this->id."','".db_escape_string(serialize($s_old))."','".$_SERVER['REMOTE_ADDR']."','".mysql_real_escape_string(serialize($s_new))."') ");
                            }
                    }
                }
            }
            
           if ( $delayed ) {
             $sql = 'UPDATE LOW_PRIORITY `'.$this->obj_name.'` SET ';
           } else {
             $sql = 'UPDATE `'.$this->obj_name.'` SET ';
           }
           
           $sql .= implode( ',', $update );

           if (isset($this->exceptPrimaryKeys[$this->obj_name]) ) {
                   $saveKey = $this->exceptPrimaryKeys[$this->obj_name];
                   $sql .= ' WHERE `'.$this->obj_name.'`.`'.$saveKey.'` = "' . $this->get($saveKey) . '"'; 
            } else {
                   $sql .= ' WHERE `'.$this->obj_name.'`.`id` = "' . $this->id . '"'; 
        }

           $curdb->q( $sql );
           $this->save_sql = $sql;
           global $cached_objects;
           unset($cached_objects[$this->obj_name][$this->id]);
           
        } else {
           $sql =  'INSERT INTO `' . $this->obj_name . '` (`' . implode( '`, `', array_keys( $insert ) ) .  '`) VALUES ( "' . implode( '", "', array_values( $insert ) ) . '")' ;
           $db2 = $curdb->q( $sql );   
           $this->id = $db2->insert_id();
           $this->save_sql = $sql;
            if ($do_log) {
                if ($this->global_type == 1 ) {
                    global $auth;
                    $db2->q("INSERT INTO `_sys_obj_log` (`datetime`,`user_id`,`obj_name`,`obj_id`,`old_data`,`user_ip`,`new_data`)
                             VALUES ('".time()."','".$auth->user_id."','".$this->obj_name."','".$this->id."','','".$_SERVER['REMOTE_ADDR']."','') ");
                }
        }

        }

        foreach( $this->diffed as $key ) {
             if ( $this->attrs[$key] ) {
                $this->attrs[$key]['attr_obj']->postsave();
             }
        }
        
    }

   /**
     * Сбрасывает (обнуляет) объект
     */
    public function reset() {
        $this->byId(0);
        $this->save_sql = '';
        $this->diffed = [];
    }

    /**
     * Создает новые объект с данными текущего, но без ID
     */
    function copy() {
        $this->id = 0;
        $this->save_sql = '';
        foreach($this->attrs_vals as $k => $v) {
            $this->diffed[] = $k;
        }
    }

    /**
     * Сохраняет объект как копию
     * @param  boolean $do_log логировать ли запрос
     */
    function saveAsCopy($do_log = true) {
        $this->id = 0;
        $this->save_sql = '';
        foreach( $this->attrs as $key => $val ) {
           $this->diffed[] = $key;
        }   
        return $this->save($do_log);
    }
        
    /**
     * Удаляет объект
     */
    function del() {

        $curdb = $this->getDB();

        if ( $this->id ) {
           if ( isset($this->attrs['_sys_deleted']) ) {
               $curdb->q('UPDATE `'.$this->obj_name.'` SET `'.$this->obj_name.'`.`_sys_deleted` = "1" WHERE `'.$this->obj_name.'`.`id` = "'.$this->id.'"');
           } else {
               $curdb->q('DELETE FROM `'.$this->obj_name.'` WHERE `'.$this->obj_name.'`.`id` = "'.$this->id.'"');
           }
        }
       global $cached_objects;
       unset($cached_objects[$this->obj_name][$this->id]);
    }
    
    /**
     * Заплоняет объект строчкой из базы данных
     * @param  db $db ссылка на подключение к базе данных
     */
    function lRow( $db ) {
        $this->diffed = array();

        foreach( $this->attrs as $key => $val ) {
           $this->attrs_vals[$key] = $db->f( $key );
        }   
        $this->id = $db->f('id');
        global $cached_objects;
        $cached_objects[$this->obj_name][$db->f('id')] = $this->attrs_vals;
        return $this;
    }
    
    /**
     * Заполняет объект из POST данных формы
     */
    function lPost() {
        if ( intval( $_POST['id']) ) {
            $this->byId( intval( $_POST['id']) );   
        }
        foreach( $this->attrs as $k => $val ) {
           if (in_array($k, $this->force_hidden ) ) continue;
           if ( !isset($_FILES[$k]) && !isset($_POST[$k]) && (!$this->attrs[$k]['mand']) ) {
                if ( substr( $val['name'], -8 ) != '_preview' ) {
                    $this->set($k,'');
                }
           } else {
                $val['attr_obj']->lPost();
           }
//           $this->diffed[] = $k;
        }   
        $this->id = intval($_POST['id']);
    }
    
    
    /**
     * Меняет данных атрибута (сеттер)
     * @param string $attr_name имя атрибута
     * @param string $value     данные
     */
    function set( $attr_name, $value ) {
        

        // Проверка на совпадение, с учётом приведения типов
        // в this->attrs_vals всегда лежат string
       
        if  ( (string)$this->attrs_vals[$attr_name] === (string)$value ) return false;
        if  ( $this->attrs_vals[$attr_name] === '0' && $value === "") return false;
        if  ( $this->attrs_vals[$attr_name] === '0' && $value === false ) return false;
        if  ( $this->attrs_vals[$attr_name] === '1' && $value === true ) return false;
        if  ( is_numeric($this->attrs_vals[$attr_name]) && is_numeric($value) && $this->attrs_vals[$attr_name] == $value ) return false;
        
        $this->attrs_vals[$attr_name] = $value;
        if (!in_array($attr_name, $this->diffed)) {
            $this->diffed[] = $attr_name;
        }


        return true;

    }
    
    /**
     * Получает данные атрибута из объекта
     * @param  string $attr_name имя атрибута
     * @return string
     */
    function get( $attr_name ) {
        return $this->attrs_vals[$attr_name];
    }
    
    
    /**
     * Отрисовка атрибутов через шаблонизатор Render
     * @param  string $tpl    переменная-шаблон
     * @param  boolean $crop   укороченный вывод
     * @param  array   $params устарело - не используется
     * @return [type]          [description]
     */
    function assign( $tpl, $crop = false, $params = array() ) {
        global $R;
        $vars = $R->getVars($tpl);
        foreach( $this->attrs as $key => $val ) {
            if ( in_array( $key, $vars ) ) {
                if ($this->recursion_depth < 3 ) {
                    $tpl = $R->set( $key, $this->attrs[$key]['attr_obj']->render_text($crop), $tpl );
                } else {
                    $tpl = $R->set( $key, $this->get($key), $tpl );
                }
            }
            $tpl = $R->set( $key.'_real', $this->get($key), $tpl );
            $tpl = $R->set( $key.'_money', number_format((float)$this->get($key), 2), $tpl );
        }
        
        $tpl = $R->set( "id", $this->id, $tpl );
        $tpl = $R->set( $this->obj_name."_id", $this->id, $tpl );
        return $tpl;
    }
    
   
    function validate() {

        $valid = true;      

        foreach( $this->attrs as $attr_name=>$attr ) {
            if (in_array($attr_name, $this->force_hidden ) ) continue;
            if ($attr['mand'] && !$this->attrs_vals[$attr_name]) {
                    $this->errors[$attr_name] = 'Поле не заполнено';
                    $valid = false;
            } elseif ( $this->attrs_vals[$attr_name] ) { 
                    $err = $attr['attr_obj']->validate();
                    if ( $err && $err !== true  ) {
                        $this->errors[$attr_name] = $err;
                        $valid = false;
                    }
            }
            
        }
        return $valid;
    }
    
    function set_hidden( $attr_name ) {
       $this->force_hidden[] = $attr_name;
    }
    
    function set_no_hidden( $attr_arr = array() ) {
       foreach(  $this->attrs as $k => $v ) {
        if (!in_array($k, $attr_arr ) ) {
            $this->force_hidden[] = $k;
        }
       }
    }
    
    
    function setSqlWhere( $attr_name, $sql_where ) {
        if ( $this->attrs[$attr_name] ) {
            $this->attrs[$attr_name]['attr_obj']->setSqlWhere($sql_where);
        }
    }
    
    function addSqlWhere( $attr_name, $sql_where ) {
        if ( $this->attrs[$attr_name] ) {
            $this->attrs[$attr_name]['attr_obj']->setSqlWhere( $this->attrs[$attr_name]['attr_obj']->sql_where . $sql_where);
        }
    }

    function setError($attr_name, $error) {
        $this->errors[$attr_name] = $error;
    }

    function getSize( $sql_where = '' ) {
        $db2 = $this->getDB()->q( $this->getSelectEx(' count(*) as `c` ', $sql_where ) );
        $db2->nr();
        return intval( $db2->f('c') );   
    }

    function getList( $sql_where = '', $value = 'title' ) {
        $db2 = $this->getDB()->q( $this->getSelect( $sql_where ) );
        $lst = array();
        while($db2->nr()) {
            $lst[$db2->f('id')] = $db2->f($value); 
        }
        return $lst;
    }

    function itemObjLog() {
        global $auth;
        // array('shop_item', 'shop_item_variants','shop_item_chars','shop_item_images')
        if (!in_array($this->obj_name, array('shop_item', 'shop_item_variants',/*'shop_item_chars',*/'shop_item_images') ) ) return;
        $skipFields = array('nostock_tmp', 'stock_data', 'stock_roz', 'in_stock', 'stock_im', 'in_stock_im');
      
        $data = array();
        $data['datetime'] = time();
        $data['ip'] = $_SERVER['REMOTE_ADDR'];
        $data['user_id'] = $auth->user_id;
        $data['obj_name'] = $this->obj_name;
        $data['script_name'] = $_SERVER['PWD'].'/'.$_SERVER['PHP_SELF'];
        switch( $this->obj_name ) {
            case 'shop_item':
                $data['item_id']  = $this->id;
                $data['variant_id']  = 0;
            break;
            case 'shop_item_variants':
                $data['item_id']  = $this->get('item_id');
                $data['variant_id']  = $this->id;
            break;
            default:
                $data['item_id']  = $this->get('item_id');
                $data['variant_id']  = $this->id;
            break;
        }
        
        $old = array();
        if ($this->id ) {
            /* OldVals */
            $curdb = $this->getDB();
            $db2 = $curdb->q('SELECT * FROM `'.$this->obj_name.'` WHERE `id` = ' . intval( $this->id ) );
            if ( $db2->nr() ) {
                    $old = $db2->lastRow;
            }
        }
        
        foreach( $this->diffed as $field ) {
            
            if (in_array( $field, $skipFields ) ) continue;
            
            
            $newval = $this->attrs_vals[$field];
            $oldval = $old[$field];
           
            if ($newval == $oldval ) continue;

            $data['field_name'] = $this->attrs[$field]['name'];
            
            if (!$newval) {
                $data['change_type'] = 3;   
            } elseif( !$oldval ) {
                $data['change_type'] = 1;   
            } else {
                $data['change_type'] = 2;   
            }
            $data['old_value'] = db_escape_string($oldval);
            $data['new_value'] = db_escape_string($newval);
            $sql = 'INSERT DELAYED INTO `shop_item_change_log` (`'.implode("`, `",array_keys($data)).'`) VALUES ("'.implode('", "', $data).'") ';
            $curdb = $this->getDB();
            $db2 = $curdb->q( $sql );
        }
    }


    function getCollection($sql_where = "", $show_deleted = 0) {
        return new cobject_collection($this, $sql_where, $show_deleted);
    }

}

/**
 * Класс для коллекции объектов. 
 * Так удобнее работать с blade шаблонами
 */
class cobject_collection {
    var $obj;
    var $db;

    // Готовим запрос в базу
    function __construct($obj, $sql_where, $show_deleted = 0) {
        $this->obj = $obj;
        $this->db = $this->obj->getDB()->q($this->obj->getSelect($sql_where));
    }

    // Следующий экземпляр коллекции
    public function next() {
        if ($this->db->nr()) {
            $this->obj->lRow($this->db);
            return true;
        } else {
            return false;
        }
    }
    // Получение свойства
    public function __get($key) {
       return $this->render($key);
    }

    // Получение реального значения свойства
    public function get($key) {
        return $this->obj->get($key);
    }

    // Отрисовка
    public function render($key, $crop = false) {
        if (isset($this->obj->attrs[$key])) {
            if ($this->obj->recursion_depth < 3 ) {
                return $this->obj->attrs[$key]['attr_obj']->render_text($crop);
            } else {
                return $this->obj->get($key);
            }
        } else {
            if ($key == 'id') return $this->obj->id;
            return '';
        }
    }
}

?>