<?php



/**

 * Adapted by pimcore-crew to work with pimcore

 *

 * @author     Olivier Bregeras (Stunti) (olivier.bregeras@gmail.com)

 * @author     Anton Stöckl (anton@stoeckl.de)

 * @package    Pimcore

 * @copyright  Copyright (c) 2009 Stunti. (http://www.stunti.org)

 * @copyright  Copyright (c) 2011 Anton Stöckl (http://www.stoeckl.de)

 * @license    http://framework.zend.com/license/new-bsd     New BSD License

 */



namespace Pimcore\Cache\Backend;



class Mongodb extends \Zend_Cache_Backend implements \Zend_Cache_Backend_ExtendedInterface

{



    const DEFAULT_HOST = '127.0.0.1';

    const DEFAULT_PORT =  27017;

    const DEFAULT_DBNAME = 'pimcore_cache';

    const DEFAULT_COLLECTION = 'cache_items';



    /**

     * @var Mongo

     */

    protected $_conn;



    /**

     * @var MongoDB

     */

    protected $_db;



    /**

     * @var MongoCollection

     */

    protected $_collection;



    /**

     * Available options

     *

     * =====> (array) servers :

     * an array of mongodb server ; each mongodb server is described by an associative array :

     * 'host' => (string) : the name of the mongodb server

     * 'port' => (int) : the port of the mongodb server

     * 'collection' => (string) : name of the collection to use

     * 'dbname' => (string) : name of the database to use

     *

     * @var array available options

     */

    protected $_options = array(

        'host'       => self::DEFAULT_HOST,

        'port'       => self::DEFAULT_PORT,

        'collection' => self::DEFAULT_COLLECTION,

        'dbname'     => self::DEFAULT_DBNAME,

        'optional'   => array()

    );



    /**

     * @return void

     */

    public function __construct($options)

    {

        if (!extension_loaded('mongo')) {

            \Zend_Cache::throwException('The MongoDB extension must be loaded for using this backend !');

        }

        parent::__construct($options);



        // Merge the options passed in; overridding any default options

        $this->_options = array_merge($this->_options, $options);



        $this->_conn       = new \MongoClient('mongodb://' . $this->_options['host'] . ':' . $this->_options['port'], $this->_options['optional']);

        $this->_db         = $this->_conn->selectDB($this->_options['dbname']);

        $this->_collection = $this->_db->selectCollection($this->_options['collection']);



        $this->_collection->ensureIndex(array('t' => 1), array('background' => true));

        $this->_collection->ensureIndex(array('expire' => 1), array('background' => true));

    }



    /**

     * Expires a record (mostly used for testing purposes)

     * @param string $id

     * @return void

     */

    public function ___expire($id)

    {

        $cursor = $this->get($id);

        if ($tmp = $cursor->getNext()) {

            $tmp['l'] = -10;

            $this->_collection->save($tmp);

        }

    }



    /**

     * Test if a cache is available for the given id and (if yes) return it (false else)

     *

     * @param  string  $id  Cache id

     * @param  boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested

     * @return string|false cached datas

     */

    public function load($id, $doNotTestCacheValidity = false)

    {

        try {

            $cursor = $this->get($id);

            if ($tmp = $cursor->getNext()) {

                if ($doNotTestCacheValidity || !$doNotTestCacheValidity && ($tmp['created_at'] + $tmp['l']) >= time()) {

                    return $tmp['d'];

                }

                return false;

            }

        } catch (\Exception $e) {

            return false;

        }



        return false;

    }



    /**

     * Test if a cache is available or not (for the given id)

     *

     * @param  string $id Cache id

     * @return mixed|false (a cache is not available) or "last modified" timestamp (int) of the available cache record

     */

    public function test($id)

    {

        try {

            $cursor = $this->get($id);

            if ($tmp = $cursor->getNext()) {

                return $tmp['created_at'];

            }

        } catch (\Exception $e) {

            return false;

        }



        return false;

    }



    /**

     * Save some string datas into a cache record

     *

     * Note : $data is always "string" (serialization is done by the

     * core not by the backend)

     *

     * @param  string $data             Datas to cache

     * @param  string $id               Cache id

     * @param  array  $tags             Array of strings, the cache record will be tagged by each string entry

     * @param  int    $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime)

     * @return boolean True if no problem

     */

    public function save($data, $id, $tags = array(), $specificLifetime = false)

    {

        try {

            $lifetime = $this->getLifetime($specificLifetime);

            $result = $this->set($id, $data, $lifetime, $tags);

        } catch (\Exception $e) {

            return false;

        }



        return $result;

    }



    /**

     * Remove a cache record

     *

     * @param  string $id Cache id

     * @return boolean True if no problem

     */

    public function remove($id)

    {

        try {

            $result = $this->_collection->remove(array('_id' => $id));

        } catch (\Exception $e) {

            return false;

        }



        return $result;

    }



    /**

     * Clean some cache records (protected method used for recursive stuff)

     *

     * Available modes are :

     * \Zend_Cache::CLEANING_MODE_ALL (default)    => remove all cache entries ($tags is not used)

     * \Zend_Cache::CLEANING_MODE_OLD              => remove too old cache entries ($tags is not used)

     * \Zend_Cache::CLEANING_MODE_MATCHING_TAG     => remove cache entries matching all given tags

     *                                               ($tags can be an array of strings or a single string)

     * \Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}

     *                                               ($tags can be an array of strings or a single string)

     * \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags

     *                                               ($tags can be an array of strings or a single string)

     *

     * @param  string $dir  Directory to clean

     * @param  string $mode Clean mode

     * @param  array  $tags Array of tags

     * @throws \Zend_Cache_Exception

     * @return boolean True if no problem

     */

    public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, $tags = array())

    {

        switch ($mode) {

            case \Zend_Cache::CLEANING_MODE_ALL:

                return $this->_conn->dropDB($this->_options['dbname']);

                break;

            case \Zend_Cache::CLEANING_MODE_OLD:

                return $this->_collection->remove(array('expire' => array('$lt' => time())));

                break;

            case \Zend_Cache::CLEANING_MODE_MATCHING_TAG:

                return $this->_collection->remove(array('t' => array('$all' => $tags)));

                break;

            case \Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:

                return $this->_collection->remove(array('t' => array('$nin' => $tags)));

                break;

            case \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG:

                return $this->_collection->remove(array('t' => array('$in' => $tags)));

                break;

            default:

                \Zend_Cache::throwException('Invalid mode for clean() method');

                break;

        }

    }



    /**

     * Return true if the automatic cleaning is available for the backend

     *

     * @return boolean

     */

    public function isAutomaticCleaningAvailable()

    {

        return false;

    }



    /**

     * Set the frontend directives

     *

     * @param  array $directives Assoc of directives

     * @throws \Zend_Cache_Exception

     * @return void

     */

    public function setDirectives($directives)

    {

        parent::setDirectives($directives);

        $lifetime = $this->getLifetime(false);

        if ($lifetime === null) {

            // #ZF-4614 : we tranform null to zero to get the maximal lifetime

            parent::setDirectives(array('lifetime' => 0));

        }

        return $this;

    }



    /**

     * Return an array of stored cache ids

     *

     * @return array array of stored cache ids (string)

     */

    public function getIds()

    {

        $cursor = $this->_collection->find();

        $ret = array();

        while ($tmp = $cursor->getNext()) {

            $ret[] = $tmp['_id'];

        }



        return $ret;

    }



    /**

     * Return an array of stored tags

     *

     * @return array array of stored tags (string)

     */

    public function getTags()

    {

        $cmd['mapreduce'] = $this->_options['collection'];



        $cmd['map']       = 'function(){

                                this.t.forEach(

                                    function(z){

                                        emit( z , { count : 1 } );

                                    }

                                );

                            };';



        $cmd['reduce']    = 'function( key , values ){

                                var total = 0;

                                for ( var i=0; i<values.length; i++ )

                                    total += values[i].count;

                                return { count : total };

                            };';



        $cmd['out'] = array('replace' => 'getTagsCollection');



        $res2 = $this->_db->command($cmd);



        $res3 = $this->_db->selectCollection('getTagsCollection')->find();



        $res = array();

        foreach ($res3 as $key => $val) {

            $res[] = $key;

        }



        $this->_db->dropCollection($res2['result']);



        return $res;

    }



    public function drop()

    {

        return $this->_collection->drop();

    }



    /**

     * Return an array of stored cache ids which match given tags

     *

     * In case of multiple tags, a logical AND is made between tags

     *

     * @param array $tags array of tags

     * @return array array of matching cache ids (string)

     */

    public function getIdsMatchingTags($tags = array())

    {

        $cursor =  $this->_collection->find(

            array('t' => array('$all' => $tags))

        );

        $ret = array();



        while ($tmp = $cursor->getNext()) {

            $ret[] = $tmp['_id'];

        }



        return $ret;

    }



    /**

     * Return an array of stored cache ids which don't match given tags

     *

     * In case of multiple tags, a logical OR is made between tags

     *

     * @param array $tags array of tags

     * @return array array of not matching cache ids (string)

     */

    public function getIdsNotMatchingTags($tags = array())

    {

        $cursor =  $this->_collection->find(

            array('t' => array('$nin' => $tags))

        );

        $ret = array();



        while ($tmp = $cursor->getNext()) {

            $ret[] = $tmp['_id'];

        }



        return $ret;

    }



    /**

     * Return an array of stored cache ids which match any given tags

     *

     * In case of multiple tags, a logical AND is made between tags

     *

     * @param array $tags array of tags

     * @return array array of any matching cache ids (string)

     */

    public function getIdsMatchingAnyTags($tags = array())

    {

        $cursor =  $this->_collection->find(

            array('t' => array('$in' => $tags))

        );



        $ret = array();

        while ($tmp = $cursor->getNext()) {

            $ret[] = $tmp['_id'];

        }



        return $ret;

    }



    /**

     * No way to find the remaining space right now. So return 1.

     *

     * @throws \Zend_Cache_Exception

     * @return int integer between 0 and 100

     */

    public function getFillingPercentage()

    {

        return 1;

    }



    /**

     * Return an array of metadatas for the given cache id

     *

     * The array must include these keys :

     * - expire : the expire timestamp

     * - tags : a string array of tags

     * - mtime : timestamp of last modification time

     *

     * @param string $id cache id

     * @return array array of metadatas (false if the cache id is not found)

     */

    public function getMetadatas($id)

    {

        $cursor = $this->get($id);



        if ($tmp = $cursor->getNext()) {

            $data = $tmp['d'];

            $mtime = $tmp['created_at'];

            $lifetime = $tmp['l'];

            return array(

                'expire' => $mtime + $lifetime,

                'tags' => $tmp['t'],

                'mtime' => $mtime

            );

        }



        return false;

    }



    /**

     * Give (if possible) an extra lifetime to the given cache id

     *

     * @param string $id cache id

     * @param int $extraLifetime

     * @return boolean true if ok

     */

    public function touch($id, $extraLifetime)

    {

        $cursor = $this->get($id);

        if ($tmp = $cursor->getNext()) {

            $data = $tmp['d'];

            $mtime = $tmp['created_at'];

            $lifetime = $tmp['l'];

            $tags = $tmp['t'];

            $newLifetime = $lifetime - (time() - $mtime) + $extraLifetime;

            if ($newLifetime <=0) {

                return false;

            }



            $result = $this->set($id, $data, $newLifetime,$tags);

            return $result;

        }



        return false;

    }



    /**

     * Return an associative array of capabilities (booleans) of the backend

     *

     * The array must include these keys :

     * - automatic_cleaning (is automating cleaning necessary)

     * - tags (are tags supported)

     * - expired_read (is it possible to read expired cache records

     *                 (for doNotTestCacheValidity option for example))

     * - priority does the backend deal with priority when saving

     * - infinite_lifetime (is infinite lifetime can work with this backend)

     * - get_list (is it possible to get the list of cache ids and the complete list of tags)

     *

     * @return array associative of with capabilities

     */

    public function getCapabilities()

    {

        return array(

            'automatic_cleaning' => true,

            'tags' => true,

            'expired_read' => true,

            'priority' => false,

            'infinite_lifetime' => true,

            'get_list' => true

        );

    }



    /**

     * @param int $id

     * @param array $data

     * @param int $lifetime

     * @param mixed $tags

     * @return boolean

     */

    function set($id, $data, $lifetime, $tags)

    {

        $now = time();



        // set the lifetime to 1 year if it is null

        if(!$lifetime) {

            $lifetime = (86400*365);

        }



        return $this->_collection->save(

            array(

                '_id' => $id,

                'd' => $data,

                'created_at' => $now,

                'l' => $lifetime,

                'expire' =>  $now + $lifetime,

                't' => $tags

            )

        );

    }



    /**

     * @param int $id

     * @return array|false

     */

    function get($id)

    {

        return $this->_collection->find(array('_id' => $id));

    }



}



