<?php
class memcacheCache extends Cache {
  var $settings = array();
  var $memcache;
  
  function page_fast_cache() {
    return TRUE;
  }
  
  function __construct($bin) {
    global $conf;
    // Assign the servers on the following order: bin specific -> default specific -> localhost port 11211
    $this->settings['servers'] = $conf['cacherouter'][$this->name]['server'];
    if (!isset($conf['cacherouter'][$this->name]['server'])) {
      if (isset($conf['cacherouter']['default']['server'])) {
        $this->settings['servers'] = $conf['cacherouter']['default']['server'];
      }
      else {
        $this->settings['servers'] = array('localhost:11211');
      }
    }
    $this->settings['compress'] = isset($conf['cacherouter'][$this->name]['compress']) ? MEMCACHE_COMPRESSED : 0;
    $this->settings['shared'] = isset($conf['cacherouter'][$this->name]['shared']) ?
                                $conf['cacherouter'][$this->name]['shared'] : TRUE;
                                
    parent::__construct($bin);
    
    $this->connect();
  }
  
  function get($key) {
    // Attempt to pull from static cache.
    $cache = parent::get($key);
    if (isset($cache)) {
      return $cache;
    }
    
    // Get from memcache
    $cache = $this->memcache->get($this->key($key));
    
    // Update static cache 
    parent::set($key, $cache);
    
    return $cache;
  }
  
  function set($key, $value, $expire = CACHE_PERMENANT, $headers = NULL) {
    if ($expire == CACHE_TEMPORARY) {
      $expire = 180;
    }
    
    // Create new cache object.
    $cache = new stdClass;
    $cache->cid = $key;
    $cache->created = time();
    $cache->expire = $expire;
    $cache->headers = $headers;
    $cache->data = $value;
    
    if (!empty($key) && !empty($value)) {
      if ($this->settings['shared']) {
        $this->lock();

        // Get lookup table to be able to keep track of bins
        $lookup = $this->memcache->get('lookup_' . $this->name);

        // If the lookup table is empty, initialize table
        if (empty($lookup)) {
          $lookup = array();
        }

        // Get full key for storage and set it to 1 so we can keep track of the bin
        $full_key = $this->key($key);
        $lookup[$full_key] = 1;

        // Attempt to store full key and value
        if (!$this->memcache->set($full_key, $cache, $this->settings['compress'], $expire)) {
          unset($lookup[$full_key]);
          $return = FALSE;
        }
        else {
          // Update static cache
          parent::set($key, $cache);
          $return = TRUE;
        }

        // Resave the lookup table (even on failure)
        $this->memcache->set('lookup_' . $this->name, $lookup, $this->settings['compress'], $expire);  

        // Remove lock.
        $this->unlock();
      }
      else {
        // Update memcache
        return $this->memcache->set($this->key($key), $cache, $this->settings['compress'], $expire);
      }
    }
  }
  
  function delete($key) {
    // Delete from static cache
    parent::delete($key);
    
    // Remove from memcache.
    return $this->memcache->delete($this->key($key));

  }
  
  function flush() {
    // Flush static cache
    parent::flush();
    
    // If this is a shared cache, we need to cycle through the lookup table and remove individual
    // items directly
    if ($this->settings['shared']) {
      $this->lock();

      // Get lookup table to be able to keep track of bins
      $lookup = $this->memcache->get('lookup_' . $this->name);

      // If the lookup table is empty, remove lock and return
      if (empty($lookup)) {
        $this->unlock();
        return TRUE;
      }

      // Cycle through keys and remove each entry from the cache
      foreach ($lookup as $k => $v) {
        if ($this->memcache->delete($k)) {
          unset($lookup[$k]);
        }
      }

      // Resave the lookup table (even on failure)
      $this->memcache->set('lookup_' . $this->name, $lookup, $this->settings['compress'], 0);

      // Remove lock
      $this->unlock();
    }
    else {
      // Flush memcache
      return $this->memcache->flush();
    }
  }
  
  function lock() {
    // Lock once by trying to add lock file, if we can't get the lock, we will loop
    // for 3 seconds attempting to get lock.  If we still can't get it at that point,
    // then we give up and return FALSE.
    if ($this->memcache->add('lock_' . $this->name, $this->settings['compress'], 0) === FALSE) {
      $time = time();
      while ($this->memcache->add('lock_' . $this->name, $this->settings['compress'], 0) === FALSE) {
        if (time() - $time >= 3) {
          return FALSE;
        }
      }
    }
    return TRUE;
  }
  
  function unlock() {
    return $this->memcache->delete('lock_' . $this->name);   
  }
  
  function connect() {
    $this->memcache =& new Memcache;
    foreach ($this->settings['servers'] as $server) {
      list($host, $port) = explode(':', $server);
      if (!$this->memcache->addServer($host, $port)) {
        drupal_set_message("Unable to connect to memcache server $host:$port");
      }
    }
  }
  
  function close() {
    $this->memcache->close();
  }
}