<?php
    /*   
     Copyright (c) 2012, Paul G Talaga
     All rights reserved.
     
     Redistribution and use in source and binary forms, with or without
     modification, are permitted provided that the following conditions are met:
     * Redistributions of source code must retain the above copyright
     notice, this list of conditions and the following disclaimer.
     * Redistributions in binary form must reproduce the above copyright
     notice, this list of conditions and the following disclaimer in the
     documentation and/or other materials provided with the distribution.
	 
     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     DISCLAIMED. IN NO EVENT SHALL Paul G Talaga BE LIABLE FOR ANY
     DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     */
    
    // Wrapper class to PECL memcache, provides duplication with no locality 
	//	advantage (yet).
    // We use the findServer function of the PECL Memcache client to detect the 
	//	storage server.  Thus, to duplicate data we append data to the key
    //  until a different server is chosen.  If your duplication value is equal 
	//	to or greater than the number of servers we'll just store on all.
    // A GET will choose randomly from the duplicated keys until all return 
	//	false, thus, a read miss will query your duplication
    //  level number of servers until it fails.  Consider limiting?  Thus this 
	//	is is like RAID 1.
    // NOTE: Due to a read possibly taking a long time (timeout x duplication), 
	//	consider reducing the timeout.
    class Memcache_Dup {
        
        // constructor funciton
        function __construct(){
            $this->num_dups = 2;
            $this->num_servers = 0;
            $this->mem = new MemcachePool();
            //$this->mem = new Memcache();
            //echo 'Dup';     	
        }
        
        function __destruct(){
            unset($this->mem);
        }
        function setDups($num){
            $this->num_dups = $num;
        }
        function getDups(){
            return $this->num_dups;
        }
        
        function setCompressThreshold($val){
            return $this->mem->setCompressThreshold($val);
        }
        
 /**************************************************************/    
        function getKeys(&$key,$set = 0){
            // Returns an array of keys to store (or get) based on the 
			//	duplication factor and # of servers.
            // First in returned array should be in local rack
            // NOTE: findServer returns ip:port, so this will NOT garuntee 
			//	duplicates on different machines!
            $slist = array(0 => $this->mem->findServer($key));
            $ret = array(0 => $key);
            $append = 0;
            while(count($slist) < $this->num_dups && count($slist) < $this->num_servers){
                $append++;
                $new_key = $key . '#' . $append;
                $s = $this->mem->findServer($new_key);
                if(!in_array($s,$slist,TRUE)){
                    $slist[] = $s;
                    $ret[] = $new_key;
                    
                }
            }
            // Sort in increasing order, then rotate so next largest ip is first.
            // Assuming your IP's are given in order through all racks, 
            // next largest memcache server IP may be in same rack
            // NOT ELEGANT!
            $ret2 = array();
            $sv = explode('.',$_SERVER['SERVER_ADDR']);
            array_multisort($slist,$ret);
            $m = explode('.',$slist[0]);
            $m = explode(':',$m[3]);
            $i = 0;
            while($sv[3] > $m[0] && $i < 50){
                $s = array_shift($slist);
                array_push($slist,$s);
                $s = array_shift($ret);
                array_push($ret,$s);
                $m = explode('.',$slist[0]);
                $m = explode(':',$m[3]);
                $i++;
            }   
            return $ret; 
        }    
        
/**************************/         
        function addServer($host, $tcp_port = 11211, $udp_port = 0, $persistent = true, $weight = 1, $timeout = 1, $retry_interval = 15, $status = true, $rack = -1){
            // Add a server.  Rack doesn't matter.  Future releases will use!
            $this->num_servers++;
            $ret = $this->mem->addServer($host, $tcp_port, $udp_port, $persistent, $weight, $timeout, $retry_interval);
            return $ret;
        }    
/**************************/         
        function connect($host, $tcp_port = 11211, $udp_port = 0, $persistent = true, $weight = 1, $timeout = 1, $retry_interval = 15, $rack = -1){
            return $this->mem->connect($host, $tcp_port, $udp_port, $persistent, $weight, $timeout, $retry_interval);
        }
 /**************************************************************/    
        function add(&$key,&$value, $flag = 0,$expire = 0){
            // Add item with key
            // Return true if ANY call returns true, and will set failed adds 
			//	via set so they all match
            // TODO: look at uses for add and decide if this is the best policy!
            $ret = FALSE;
            $repair = array();
            foreach($this->getKeys($key) as $k){
                if($this->mem->add($k,$value,$flag,$expire) === TRUE){
                    $ret = TRUE;
                }else{
                    $repair[] = $k;
                }
            }
            if($ret && count($repair) > 0){
                foreach($repair as $k){
                    $this->mem->set($k,$value,$flag,$expire);
                }
            }
            return $ret;
            // WHOA! Need to deal with $key beeing an array already!
        }        
/**************************/         
        function delete($key,$timeout = 0){
            // Delete data with key.  
            $keys = $this->getKeys($key);
            return $this->mem->delete($keys,$timeout);
        }
/**************************/         
        function flush(){
            // flush 
            return $this->mem->flush();
        }
/**************************/         
        function get(&$key, &$flags = 0, &$cas = ''){
            // Randomly pick a key to retrieve, and keep picking if it fails
            foreach($this->getKeys($key) as $k){
                $ret = $this->mem->get($k);
                if($ret !== FALSE){
                    return $ret;
                }
            }
            return FALSE;
        }
/**************************/ 
        function replace($key, $value, $flag = 0, $expire = 0){
            // Replace should return false if the item doesn't already exist.
            // Return false if all return false
            $ret = FALSE;
            $repair = array();
            foreach($this->getKeys($key) as $k){
                if($this->mem->replace($k,$value,$flag,$expire) === TRUE){
                    $ret = TRUE;
                }else{// if any succeded, set the broken ones to match
                    $repair[] = $k;
                }
            }
            if($ret == TRUE && count($repair) > 0){
                foreach($repair as $k){
                    $this->mem->set($k,$value,$flag,$expire);
                }
            }
            return $ret; 
            $keys = $this->getKeys($key);
            return $this->mem->replace(array_combine($keys,array_fill(0,count($keys),$value)),$flag,$expire);
        }
/**************************/ 
        function decrement($key, $amt = 1){ // TODO: deal with defval and exptime!!!!
            // Decrement 
            // Will return decremented value if any return a value (and all same), 
			//	otherwise false
            // Those which return false (does not exist), will be set with the 
			//	value
             $ret = FALSE;
            $repair = array();
            foreach($this->getKeys($key) as $k){
                $val = $this->mem->decrement($k,$amt);
                if($ret == FALSE && $val !== FALSE){ // First time around
                    $ret = $val;
                }else if($ret !== FALSE && $val !== FALSE && $ret != $val){ 
					// Got something earlier, and different now, fix
                    $repair[] = $k;
                }
            }
            if($ret !== FALSE && count($repair) > 0){
                foreach($repair as $k){
                    $this->mem->set($k,$ret);
                }
            }
            return $ret;
        }
/**************************/        
        function increment(&$key, $amt = 1){
            // Increment 
            // Will return incremented value if all are equal, otherwise false
            $ret = FALSE;
            $repair = array();
            foreach($this->getKeys($key) as $k){
                $val = $this->mem->increment($k,$amt);
                if($ret == FALSE && $val !== FALSE){ // First time around
                    $ret = $val;
                }else if($ret !== FALSE && $val !== FALSE && $ret != $val){ 
					// Got something earlier, and different now, fix
                    $repair[] = $k;
                }
            }
            if($ret !== FALSE && count($repair) > 0){
                foreach($repair as $k){
                    $this->mem->set($k,$ret);
                }
            }
            return $ret;
        }
/**************************/         
        function set(&$key,&$value,$flag = 0,$expire = 0){
            // Set for all keys
            // Can use setMulti!
            $keys = $this->getKeys($key,1);
            $ret = $this->mem->set(array_combine($keys,array_fill(0,count($keys),$value)),null,$flag,$expire);
            return $ret;
        }
/**************************/         
        function getStats($type, $slabid, $limit = 100){
            return $this->mem->getStats($type,$slabid,$limig);
        }
/**************************/         
        function findServer($key){
            return $this->mem->findServer($key);
        }        
/**************************/         
        function setLocalRack($rack){
            // Dummy function to ease testing of other configs
        }
        
        	 
    }
?>
