<?php 
/**
 * Pimcore
 *
 * This source file is subject to the GNU General Public License version 3 (GPLv3)
 * For the full copyright and license information, please view the LICENSE.md and gpl-3.0.txt
 * files that are distributed with this source code.
 *
 * @copyright  Copyright (c) 2009-2016 pimcore GmbH (http://www.pimcore.org)
 * @license    http://www.pimcore.org/license     GNU General Public License version 3 (GPLv3)
 */

namespace Pimcore\Cache\Backend;

use Pimcore\Db;

class MysqlTable extends \Zend_Cache_Backend implements \Zend_Cache_Backend_ExtendedInterface {

    /**
     * @var \Zend_Db_Adapter_Abstract
     */
    protected $db;

    /**
     * @param string $id
     * @param bool $doNotTestCacheValidity
     * @return false|null|string
     */
    public function load($id, $doNotTestCacheValidity = false) {
        $data = $this->getDb()->fetchRow("SELECT data,expire FROM cache WHERE id = ?", $id);
        if($data && isset($data["expire"]) && $data["expire"] > time()) {
            return $data["data"];
        }
        return null;
    }

    /**
     * @return \Zend_Db_Adapter_Abstract
     */
    protected function getDb () {
        if(!$this->db) {
            // we're using a new mysql connection here to avoid problems with active (nested) transactions
            \Logger::debug("Initialize dedicated MySQL connection for the cache adapter");
            $this->db = Db::getConnection();
        }
        return $this->db;
    }

    /**
     * @param string $tag
     * @return array
     */
    protected function getItemsByTag($tag) {
        $itemIds = $this->getDb()->fetchCol("SELECT id FROM cache_tags WHERE tag = ?", $tag);
        return $itemIds;
    }

    /**
     * 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) {

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

        $this->getDb()->beginTransaction();

        try {
            $this->getDb()->insertOrUpdate("cache", array(
                "data" => $data,
                "id" => $id,
                "expire" => time() + $lifetime,
                "mtime" => time()
            ));

            if (count($tags) > 0) {
                while ($tag = array_shift($tags)) {
                    $this->getDb()->insertOrUpdate("cache_tags", array(
                        "id" => $id,
                        "tag" => $tag
                    ));
                }
            }
            $this->getDb()->commit();
        } catch (\Exception $e) {
            \Logger::error($e);
            $this->getDb()->rollBack();
            $this->truncate();
            return false;
        }

        return true;
    }

    /**
     * @param  string $id
     * @return bool true if OK
     */
    public function remove($id) {

        $this->getDb()->beginTransaction();

        try {
            $this->getDb()->delete("cache", "id = " . $this->getDb()->quote($id));
            $this->getDb()->delete("cache_tags", "id = '".$id."'");

            $this->getDb()->commit();
        } catch (\Exception $e) {
            $this->getDb()->rollBack();
            $this->truncate();
            return false;
        }

        return true;
    }

    /** 
     * Clean some cache records
     *
     * Available modes are :
     * 'all' (default)  => remove all cache entries ($tags is not used)
     * 'old'            => remove too old cache entries ($tags is not used)
     * 'matchingTag'    => remove cache entries matching all given tags
     *                     ($tags can be an array of strings or a single string)
     * 'notMatchingTag' => remove cache entries not matching one of the given tags
     *                     ($tags can be an array of strings or a single string)
     *
     * @param  string $mode Clean mode
     * @param  array  $tags Array of tags
     * @return boolean True if no problem
     */
    public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, $tags = array()) {

        if ($mode == \Zend_Cache::CLEANING_MODE_ALL) {
            $this->truncate();
        }
        if ($mode == \Zend_Cache::CLEANING_MODE_OLD) {
            $this->getDb()->delete("cache", "expire < unix_timestamp() OR mtime < (unix_timestamp()-864000)");
        }
        if ($mode == \Zend_Cache::CLEANING_MODE_MATCHING_TAG || $mode == \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG) {
            foreach ($tags as $tag) {
                $items = $this->getItemsByTag($tag);
                $quotedIds = array();

                $this->getDb()->beginTransaction();

                try {
                    foreach ($items as $item) {
                        // We call delete directly here because the ID in the cache is already specific for this site
                        $quotedId = $this->getDb()->quote($item);
                        $this->getDb()->delete("cache", "id = " . $quotedId);
                        $quotedIds[] = $quotedId;
                    }

                    if(count($quotedIds) > 0) {
                        $this->getDb()->delete("cache_tags", "id IN (" . implode(",", $quotedIds) . ")");
                    }

                    $this->getDb()->commit();
                } catch (\Exception $e) {
                    $this->getDb()->rollBack();
                    $this->truncate();
                    \Logger::error($e);
                }
            }
        }
        if ($mode == \Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG) {
            
            $condParts = array("1=1");
            foreach ($tags as $tag) {
                $condParts[] = "tag != '" . $tag . "'";
            }

            $itemIds = $this->getDb()->fetchCol("SELECT id FROM cache_tags WHERE ".implode(" AND ",$condParts));
            foreach ($itemIds as $item) {
                $this->remove($item);
            }

        }

        return true;
    }
    
    protected function truncate() {
        $this->getDb()->query("TRUNCATE TABLE `cache`");
        $this->getDb()->query("TRUNCATE TABLE `cache_tags`");
        $this->getDb()->query("ALTER TABLE `cache_tags` ENGINE=InnoDB");
    }

    /**
     * @param  string $id
     * @return array tags for given id
     */
    protected function getTagsById($id) {
        $itemIds = $this->getDb()->fetchCol("SELECT tag FROM cache_tags WHERE id = ?", $id);
        return $itemIds;
    }

    /**
     * @param array $tags
     * @return array
     */
    public function getIdsMatchingAnyTags($tags = array()) {
        $tags_ = array();
        foreach($tags as $tag) {
            $tags_[] = $this->getDb()->quote($tag);
        }

        $itemIds = $this->getDb()->fetchCol("SELECT id FROM cache_tags WHERE tag IN (".implode(",",$tags_).")");
        return $itemIds;
    }


    /**
     * @param array $tags
     * @return array
     */
    public function getIdsMatchingTags($tags = array()) {

        $tags_ = array();
        foreach($tags as $tag) {
            $tags_[] = " tag = ".$this->getDb()->quote($tag);
        }

        $itemIds = $this->getDb()->fetchCol("SELECT id FROM cache_tags WHERE ".implode(" AND ",$tags_));
        return $itemIds;
    }

    public function getMetadatas($id) {

        $data = $this->getDb()->fetchRow("SELECT mtime,expire FROM cache WHERE id = ?", $id);

        if (is_array($data) && isset($data["mtime"])) {
            return array(
                'expire' => $data["expire"],
                'tags' => array(),
                'mtime' => $data["mtime"]
            );
        }
        return false;
    }

    /**
     * @return array
     */
    public function getCapabilities() {
        return array(
            'automatic_cleaning' => false,
            'tags' => true,
            'expired_read' => false,
            'priority' => false,
            'infinite_lifetime' => false,
            'get_list' => false
        );
    }

    /**
     * Return an array of stored tags
     *
     * @return array array of stored tags (string)
     */
    public function getTags()
    {
        return $this->getDb()->fetchAll("SELECT DISTINCT (id) FROM cache_tags");
    }

    public function test($id)
    {
        $data = $this->getDb()->fetchRow("SELECT mtime,expire FROM cache WHERE id = ?", $id);
        if ($data && isset($data["expire"]) && time() < $data["expire"]) {
            return $data["mtime"];
        }
        return false;
    }

    /**
     * Return the filling percentage of the backend storage
     *
     * @throws \Zend_Cache_Exception
     * @return int integer between 0 and 100
     */
    public function getFillingPercentage()
    {
        return 0;
    }

    /**
     * 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)
    {
        $data = $this->getDb()->fetchRow("SELECT mtime,expire FROM cache WHERE id = ?", $id);
        if ($data && isset($data["expire"]) && time() < $data["expire"]) {
            $lifetime = (int) ($data["expire"] - $data["mtime"]);
            $this->getDb()->update("cache", array("expire" => (time() + $lifetime + (int) $extraLifetime)));
        }
        return true;
    }

    public function getIds()
    {
        return $this->getDb()->fetchAll("SELECT id from cache");
    }

    public function getIdsNotMatchingTags($tags = array())
    {
        return array();
    }
}
