<?php


/**
 * Колко голям да бъде максималния обект, който се съхранява
 * в кеша не-компресиран?
 */
defIfNot('EF_CACHE_MAX_UNCOMPRESS', 10000);


/**
 * Максимален размер за полето на типа
 */
defIfNot('EF_CACHE_TYPE_SIZE', 16);


/**
 * Максимален размер за полето на манипулатора
 */
defIfNot('EF_CACHE_HANDLER_SIZE', 32);


/**
 * Сол за префика на ключовете
 */
defIfNot('CORE_CACHE_PREFIX_SALT', md5(EF_SALT . '_CORE_CACHE'));


/**
 * Клас 'core_Cache' - Кеширане на обекти, променливи или масиви за определено време
 *
 *
 * @category  bgerp
 * @package   core
 *
 * @author    Milen Georgiev <milen@download.bg>
 * @copyright 2006 - 2015 Experta OOD
 * @license   GPL 3
 *
 * @since     v 0.1
 * @link
 */
class core_Cache extends core_Manager
{
    /**
     * Заглавие
     */
    public $title = 'Кеширани обекти';
    
    
    /**
     * Наименование на единичния обект
     */
    public $singleTitle = 'Кеширан обект';
    
    
    /**
     * Кой може да го разглежда?
     */
    public $canList = 'admin';
    
    
    public $canAdd = 'no_one';
    
    
    public $canEdit = 'no_one';
    
    
    public $canDelete = 'no_one';
    
    
    /**
     * Дали за този модел ще се прави репликация на SQL заявките
     */
    public $doReplication = false;
    
    
    /**
     * Кои полета ще извличаме, преди изтриване на заявката
     */
    public $fetchFieldsBeforeDelete = 'id,key';
    
    
    /**
     * Дали да се използва кеширане в хита
     */
    public static $stopCaching = false;

    /**
     * Неща, подлежащи на начално зареждане
     */
    public $loadList = 'plg_Sorting';


    /**
     * Описание на модела (таблицата)
     */
    public function description()
    {
        $this->FLD('key', 'identifier(' . (EF_CACHE_TYPE_SIZE + EF_CACHE_HANDLER_SIZE + 3) . ')', 'caption=Ключ,notNull');
        $this->FLD('data', 'blob(16777215,serialize,compress)', 'caption=Данни,tdClass=td-clamp');
        $this->FLD('lifetime', 'int', 'caption=Живот,notNull');     // В секунди
        $this->load('plg_Created,plg_SystemWrapper,plg_RowTools');
        
        $this->setDbUnique('key');
        
        // $this->dbEngine = 'InnoDB';
    }
    
    
    /**
     * Връща съдържанието на кеша за посочения обект
     */
    public static function get($type, $handler, $keepMinutes = null, $depends = array())
    {
        $Cache = cls::get('core_Cache');
        
        $key = $Cache->getKey($type, $handler);
        
        if ($data = $Cache->getData($key, $keepMinutes)) {
            if ($dHash = $Cache->getDependsHash($depends)) {
                
                // Ако хешовете на кешираните данни и изчисления хеш не съвпадат -
                // изтриваме кеша и връщаме NULL
                if ($data->dHash != $dHash) {
                    $Cache->deleteData($key);
                    
                    Debug::log("Cache::get ${type}, ${handler} - other models are changed, no success");
                    
                    return false;
                }
            }
            
            // Увеличаваме времето на валидността на данните ????
            
            Debug::log("Cache::get ${type}, ${handler} - success");
            
            return $data->value;
        }
        
        Debug::log("Cache::get ${type}, ${handler} - no exists");
        
        return false;
    }
    
    
    /**
     * Записва обект в кеша
     */
    public static function set($type, $handler, $value, $keepMinutes = 1, $depends = array())
    {
        $Cache = cls::get('core_Cache');
        
        Debug::log("Cache::set ${type}, ${handler}");
        
        if (!$handler) {
            $handler = md5(json_encode($value));
        }
        
        $key = $Cache->getKey($type, $handler);
        
        $data = new stdClass();
        
        $data->value = $value;
        $data->dHash = $Cache->getDependsHash($depends);
        
        expect(is_numeric($keepMinutes));
        
        $Cache->setData($key, $data, $keepMinutes);
        
        return $handler;
    }
    
    
    /**
     * Изтрива всички обекти от указания тип
     */
    public static function removeByType($type)
    {
        $Cache = cls::get('core_Cache');
        $handler = null;
        $key = $Cache->getKey($type, $handler);
        $query = self::getQuery();
        while ($rec = $query->fetch(array("#key LIKE '%[#1#]'", "{$type}"))) {
            $Cache->deleteData($rec->key);
        }
    }
    
    
    /**
     * Изтрива обектите от указания тип(ове) (и манипулатор)
     */
    public static function remove($type, $handler = null)
    {
        $Cache = cls::get('core_Cache');
        
        if ($handler === null) {
            $type = arr::make($type);
            
            foreach ($type as $t) {
                $key = $Cache->getKey($t, $handler);
                $query = self::getQuery();
                while ($rec = $query->fetch(array("#key = '[#1#]'", "{$key}"))) {
                    $Cache->deleteData($rec->key);
                }
            }
        } else {
            $key = $Cache->getKey($type, $handler);
            $Cache->deleteData($key);
        }
    }
    
    
    /**
     * Извиква се след подготовката на toolbar-а за табличния изглед
     */
    public static function on_AfterPrepareListToolbar($mvc, &$res, $data)
    {
        $data->toolbar->addBtn('Изтриване на изтеклите записи', array(
            $mvc,
            'DeleteExpiredData',
            'ret_url' => true
        ));
        
        $data->toolbar->addBtn('Изтриване на всички записи', array(
            $mvc,
            'DeleteExpiredData',
            'all' => true,
            'ret_url' => true
        ));
        
        $data->toolbar->addBtn('Състояние на APCu', array(
            $mvc,
            'APCuCacheStatus',
            'all' => true,
            'ret_url' => true
        ));
        
        
        $data->toolbar->removeBtn('btnAdd');
        
        return $data;
    }
    
    
    /**
     * 'Ръчно' почистване на кеша
     */
    public function act_DeleteExpiredData()
    {
        requireRole('admin');
        
        return new Redirect(array('core_Cache'), $this->cron_DeleteExpiredData(Request::get('all')));
    }
    
    
    /**
     * След изтриване на записи на модела
     *
     * @param core_Mvc   $mvc
     * @param mixed      $res
     * @param core_Query $query
     */
    public static function on_AfterDelete($mvc, &$res, $query)
    {
        foreach ($query->getDeletedRecs() as $rec) {
            $mvc->deleteData($rec->key, true);
        }
    }
    
    
    /**
     * Почистване на обектите с изтекъл срок
     */
    public function cron_DeleteExpiredData($all = false)
    {
        $query = $this->getQuery();
        
        if ($all) {
            $query->where('1 = 1');
        } else {
            $query->where('#lifetime < ' . time());
        }
        
        $deletedRecs = 0;
        
        while ($rec = $query->fetch()) {
            $this->deleteData($rec->key, true);
            $deletedRecs += $this->delete($rec->id);
        }
        
        if ($all) {
            $msg = "Лог: Всички <b style='color:blue;'>{$deletedRecs}</b> кеширани записа бяха изтрити";
        } else {
            $msg = "Лог: <b style='color:blue;'>{$deletedRecs}</b> записа с изтекъл срок бяха изтрити";
        }
        
        return $msg;
    }
    
    
    /**
     * Изтрива целия кеш
     */
    public static function eraseFull()
    {
        // Почистване на всичкия изтекъл Кеш
        $cache = cls::get('core_Cache');
        $cache->cron_DeleteExpiredData(true);
        if (function_exists('apcu_clear_cache')) {
            apcu_clear_cache();
        } elseif (function_exists('apc_clear_cache')) {
            apc_clear_cache();
        }
    }
    
    
    /**
     * Подреждане - най-отгоре са последните записи
     */
    public static function on_AfterPrepareListFilter($mvc, &$data)
    {
        $data->query->orderBy('#createdOn', 'DESC');
    }
    
    
    /**
     * Подготвя ключовете
     */
    public function getKey(&$type, &$handler)
    {
        $handler = str::convertToFixedKey($handler, EF_CACHE_HANDLER_SIZE - 4, 12);
        $type = str::convertToFixedKey($type, EF_CACHE_TYPE_SIZE - 3, 8);
        
        $prefix = md5(EF_DB_NAME . '|' . CORE_CACHE_PREFIX_SALT);
        $prefix = substr($prefix, 0, 6);
        
        $key = "{$prefix}|{$handler}|{$type}";
        
        return $key;
    }
    
    
    /**
     * Подготвя хеш, който съответства на моментите на последното обновяване
     * на посочените в аргумента модели
     */
    public function getDependsHash($depends)
    {
        $depends = arr::make($depends);
        
        if (countR($depends)) {
            foreach ($depends as $id => $cls) {
                if (is_object($cls) || !strpos($cls, '::')) {
                    $obj[$id] = cls::get($cls);
                    $hash .= $obj[$id]->getDbTableUpdateTime();
                } else {
                    $hash .= call_user_func($cls);
                }
            }
            
            $hash = md5($hash);
        }
        
        return $hash;
    }
    
    
    /**
     * Връща съдържанието записано на дадения ключ
     */
    public function getData($key, $keepMinutes = null)
    {
        if (function_exists('apcu_fetch')) {
            $res = @apcu_fetch($key);
        } elseif (function_exists('apc_fetch')) {
            $res = @apc_fetch($key);
        } elseif (function_exists('xcache_get')) {
            $res = @xcache_get($key);
            if ($res) {
                $res = unserialize($res);
            }
        }
        
        if ($res) {
            
            return $res;
        }
        
        if ($rec = $this->fetch(array("#key = '[#1#]' AND #lifetime >= " . time(), $key), null, false)) {
            if ($keepMinutes) {
                $rec->lifetime = time() + $keepMinutes * 60;
                $this->save($rec, 'lifetime');
            }
            
            $data = $rec->data;
            if (!is_object($rec->data)) {
                if (ord($rec->data[0]) == 120 && ord($rec->data[1]) == 156) {
                    $data = gzuncompress($data);
                }
                
                $data = unserialize($data);
            }
            
            return $data;
        }
    }
    
    
    /**
     * Изтрива съдържанието на дадения ключ
     */
    public function deleteData($key, $onlyInMemory = false)
    {
        if (function_exists('apcu_delete')) {
            @apcu_delete($key);
        } elseif (function_exists('apc_delete')) {
            @apc_delete($key);
        } elseif (function_exists('xcache_unset')) {
            @xcache_unset($key);
        }
        
        if ($onlyInMemory) {
            
            return;
        }
        
        return $this->delete(array("#key = '[#1#]'", $key));
    }
    
    
    /**
     * Задава съдържанието на посочения ключ
     */
    public function setData($key, $data, $keepMinutes)
    {
        if (self::$stopCaching) {
            
            return false;
        }
        
        $saved = false;
        $keepSeconds = $keepMinutes * 60;
        
        if (function_exists('apcu_store')) {
            $saved = @apcu_store($key, $data, $keepSeconds);
            if (!$saved) {
                self::logNotice('Грешка при записване в APCU_STORE');
            }
        } elseif (function_exists('apc_store')) {
            $saved = @apc_store($key, $data, $keepSeconds);
            if (!$saved) {
                self::logNotice('Грешка при записване в APC_STORE');
            }
        } elseif (function_exists('xcache_set')) {
            $saved = @xcache_set($key, serialize($data), $keepSeconds);
            if (!$saved) {
                self::logNotice('Грешка при записване в XCACHE');
            }
        }
        
        $rec = new stdClass();
        
        // Задаваме ключа
        $rec->key = $key;
        
        if (!$saved) {
            
            // Сериализираме обекта
            $rec->data = serialize($data);
            
            // Ако е необходимо, компресираме данните
            if (strlen($rec->data) > EF_CACHE_MAX_UNCOMPRESS) {
                $rec->data = gzcompress($rec->data);
            }
        }
        
        // Задаваме крайното време за живот на данните
        $rec->lifetime = time() + $keepSeconds;
        
        $this->save($rec, null, 'REPLACE');
    }
    
    
    /**
     * Метоз за кратковременно кеширане с аргумент
     * - фукция, която извлича резултата
     */
    public static function getOrCalc($name, $param, $fn)
    {
        if (is_scalar($param)) {
            $key = md5(EF_DB_NAME . '|' . CORE_CACHE_PREFIX_SALT . "{$name}{$param}");
        } else {
            $key = md5(EF_DB_NAME . '|' . CORE_CACHE_PREFIX_SALT . $name . '|' . json_encode($param));
        }
        
        $Cache = cls::get('core_Cache');
        
        if ($resObj = $Cache->getData($key)) {
            $res = $resObj->res;
        } else {
            $res = $fn($param);
            
            $resObj = new stdClass();
            $resObj->res = $res;
            
            $Cache->setData($key, $resObj, 120);
        }
        
        return $res;
    }


    /**
     * Състояние на APCu кеша
     */
    public function act_APCuCacheStatus()
    {
        requireRole('debug');

        if (!function_exists('apcu_cache_info')) {
            $html = "<div style='padding:0.5em'><h1>Състояние на APCu кеша</h1>";
            $html .= "<div>APCu разширението не е инсталирано или активирано.</div>";
            $html .= "</div>";
            $html = $this->renderWrapping($html);
            return $html;
        }

        $cacheInfo = apcu_cache_info();
        $html = "<div style='padding:0.5em'><h1>Състояние на APCu кеша</h1>";

        if ($cacheInfo) {
            $html .= "<h2>Обща информация за кеша</h2>";
            $html .= "<table class='listTable' style='margin-top:1em'>
                          <tr style='background-color:#aaa; color:white;'>
                            <th><strong>Параметър</strong></th>
                            <th><strong>Стойност</strong></th>
                          </tr>";

            $cacheInfoGeneral = array(
                'Брой слотове' => $cacheInfo['num_slots'],
                'TTL' => $cacheInfo['ttl'],
                'Брой хитове' => $cacheInfo['num_hits'],
                'Брой пропуски' => $cacheInfo['num_misses'],
                'Брой добавяния' => $cacheInfo['num_inserts'],
                'Премахвания' => $cacheInfo['expunges'],
                'Време на стартиране' => date('Y-m-d H:i:s', $cacheInfo['start_time']),
                'Размер на паметта' => $cacheInfo['mem_size'] . ' байта',
                'Брой записи' => $cacheInfo['num_entries'],
            );

            foreach ($cacheInfoGeneral as $param => $value) {
                $html .= "<tr>
                            <td>{$param}</td>
                            <td>{$value}</td>
                          </tr>";
            }

            $html .= '</table>';


        } else {
            $html .= '<div>Не може да се извлече информация за APCu кеша.</div>';
        }

        $html .= '</div>';
        $html = $this->renderWrapping($html);
        return $html;
    }
}
