<?php

/**
 *      [Discuz!] (C)2001-2099 Comsenz Inc.
 *      This is NOT a freeware, use is subject to license terms
 *
 *      $Id: table_common_session.php 28051 2012-02-21 10:36:56Z zhangguosheng $
 */

if(!defined('IN_DISCUZ')) {
	exit('Access Denied');
}

class memory_common_session
{
	private $_pre_cache_key;

	const LUA_RETURN_DATA = <<<LUA
	local rs = {}
	for _, key in ipairs(sids) do
	local row = redis.call("hmget", prefix..key, "sid", "ip", "uid", "username", "groupid", "invisible", "action", "lastactivity", "lastolupdate", "fid", "tid")
	rs[#rs + 1] = row
	end
	return rs
LUA;

	public function __construct() {
		$this->_pre_cache_key = 'common_session_';
	}

	public function fetch($sid, $ip = false, $uid = false) {
		if(empty($sid)) {
			return array();
		}
		$session = $this->get_data_by_pk($sid);
		if($session && $ip !== false && $ip != "{$session['ip']}") {
			$session = array();
		}
		if($session && $uid !== false && $uid != $session['uid']) {
			$session = array();
		}
		return $session;
	}

	public function fetch_member($ismember = 0, $invisible = 0, $start = 0, $limit = 0) {
		if ($ismember < 1 || $ismember > 2) $ismember = 0; // $ismember只有0, 1, 2是合法值
		if ($invisible < 1 || $invisible > 2) $invisible = 0; // invisible只有0, 1, 2是合法值

		list($ss, $ee) = $this->get_start_and_end($start, $limit);
		($invisible == 2) ? $inv_idx = 0 : $inv_idx = 1; // $invisible == 2，相当于sql条件 invisible = 0
		($ismember == 2) ? $uid_idx = 0 : $uid_idx = 1; // $ismember == 2，相当于sql条件 uid = 0

		if ($ismember == 0 && $invisible == 0) {
			$script = <<<LUA
			local prefix = ARGV[1]
			local start = ARGV[2]
			local stop = ARGV[3]
			local sids = redis.call('ZREVRANGE', prefix..'idx_lastactivity', start, stop)
LUA;
			$data = memory('eval', $script . self::LUA_RETURN_DATA, array($ss, $ee), "fetch_member_1st", $this->_pre_cache_key);
		} elseif ($ismember == 0) {
			$script = <<<LUA
			local prefix = ARGV[1]
			local inv_idx = ARGV[2]
			local start = ARGV[3]
			local stop = ARGV[4]
			local sids = redis.call('ZREVRANGE', prefix..'idx_invisible_'..inv_idx, start, stop)
LUA;
			$data = memory('eval', $script . self::LUA_RETURN_DATA, array($inv_idx, $ss, $ee), "fetch_member_2nd", $this->_pre_cache_key);
		} elseif ($invisible == 0) {
			$script = <<<LUA
			local prefix = ARGV[1]
			local uid_idx = ARGV[2]
			local start = ARGV[3]
			local stop = ARGV[4]
			local sids = redis.call('ZREVRANGE', prefix..'idx_uid_group_'..uid_idx, start, stop)
LUA;
			$data = memory('eval', $script . self::LUA_RETURN_DATA, array($uid_idx, $ss, $ee), "fetch_member_3rd", $this->_pre_cache_key);
		} else {
			global $_G;
			$temp_uniq = substr(md5(substr(TIMESTAMP, 0, -3).substr($_G['config']['security']['authkey'], 3, -3)), 1, 8);
			$script = <<<LUA
			local prefix = ARGV[1]
			local inv_idx = ARGV[2]
			local uid_idx = ARGV[3]
			local out_surfix = ARGV[4]
			local start = ARGV[5]
			local stop = ARGV[6]
			local out_hash = prefix..'invisible_uid_'..out_surfix
			redis.call('ZINTERSTORE', out_hash, 2, prefix..'idx_invisible_'..inv_idx, prefix..'idx_uid_group_'..uid_idx, 'AGGREGATE', 'MIN')
			local sids = redis.call('ZREVRANGE', out_hash, start, stop)
			redis.call('DEL', out_hash)
LUA;
			$data = memory('eval', $script . self::LUA_RETURN_DATA, array($inv_idx, $uid_idx, $temp_uniq, $ss, $ee), "fetch_member_4th", $this->_pre_cache_key);
		}
		return $this->array_from_memory_result($data);
	}

	public function count_invisible($type = 1) {
		return memory('zcard', 'idx_invisible_' . $type, $this->_pre_cache_key);
	}

	public function count($type = 0) {
		switch ($type) {
			case 1:
				return memory('zcard', 'idx_uid_group_1', $this->_pre_cache_key);
			case 2:
				return memory('zcard', 'idx_uid_group_0', $this->_pre_cache_key);
			default:
				return memory('zcard', 'idx_lastactivity', $this->_pre_cache_key);
		}
	}

	public function delete_by_session($session, $onlinehold, $guestspan) {
		if(empty($session) || !is_array($session)) return;
		$onlinehold = time() - $onlinehold;
		$guestspan = time() - $guestspan;

		global $_G;
		$temp_uniq = substr(md5(substr(TIMESTAMP, 0, -3).substr($_G['config']['security']['authkey'], 3, -3)), 1, 8);
		$script = <<<LUA
		local rs = {}
		local prefix = ARGV[1]
		local sid = ARGV[2]
		local onlinehold = ARGV[3]
		local guestspan = ARGV[4]
		local userip = ARGV[5]
		local uid = ARGV[6]
		local out_surfix = ARGV[7]

		local function getdata(key)
		    local data = redis.call("HMGET", key, 'sid', 'ip', 'uid', 'invisible', 'fid')
		    if (data[1]) then
			return data
		    else
			return {}
		    end
		end

		local bysid = getdata(prefix..sid);
		if (#bysid > 0) then
		    redis.call("del", prefix..sid)
		    rs[#rs + 1] = bysid
		end

		-- lastactivity < onlinehold
		local byonlinehold = redis.call("ZRANGEBYSCORE", prefix.."idx_lastactivity", 0, onlinehold + 1);
		for _, sid in ipairs(byonlinehold) do
		    local data = getdata(prefix..sid);
		    if (#data > 0) then
			redis.call("del", prefix..sid)
			rs[#rs + 1] = data
		    end
		end

		-- uid = 0 and ip = userip
		local out_hash = prefix..'uid0_ip_'..out_surfix
		redis.call("ZINTERSTORE", out_hash, 2, prefix.."idx_uid_group_0", prefix.."idx_ip_"..userip, 'AGGREGATE', 'MIN')
		-- and lastactivity > guestspan
		local byguestspan = redis.call("ZRANGEBYSCORE", out_hash, guestspan + 1, '+inf')
		for _, sid in ipairs(byguestspan) do
		    local data = getdata(prefix..sid);
		    if ((#data > 0)) then
			redis.call("del", prefix..sid)
			rs[#rs + 1] = data
		    end
		end
		redis.call("DEL", out_hash)

		local byuid = redis.call("SMEMBERS", prefix.."idx_uid_"..uid);
		for _, sid in ipairs(byuid) do
		    local data = getdata(prefix..sid);
		    if (#data > 0) then
			redis.call("del", prefix..sid)
			rs[#rs + 1] = data
		    end
		end

		for _, row in ipairs(rs) do
		    redis.call("ZREM", prefix.."idx_ip_"..row[2], row[1])
		    redis.call("ZREM", prefix.."idx_invisible_"..row[4], row[1])
		    redis.call("ZREM", prefix.."idx_fid_"..row[5], row[1])
		    if (row[3] == '0') then
			redis.call("ZREM", prefix.."idx_uid_group_0", row[1])
		    else
			redis.call("ZREM", prefix.."idx_uid_group_1", row[1])
		    end
		    redis.call("ZREM", prefix.."idx_lastactivity", row[1])
		    redis.call("SREM", prefix.."idx_uid_"..row[3], row[1])
		end

		return #rs
LUA;
		memory('eval', $script, array($session['sid'], $onlinehold, $guestspan, $session['ip'], $session['uid'] ? $session['uid'] : -1, $temp_uniq), "delete_by_session", $this->_pre_cache_key);
	}

	public function fetch_by_uid($uid) {
		if(empty($uid)) {
			return false;
		}

		$sids = memory('smembers', 'idx_uid_' . $uid, $this->_pre_cache_key);
		foreach ($sids as $sid) {
			return $this->get_data_by_pk($sid);
		}
		return false;
	}

	public function fetch_all_by_uid($uids, $start = 0, $limit = 0) {
		if(empty($uids)) {
			return array();
		}

		if (!is_array($uids)) {
			$uids = array($uids);
		}

		$script = <<<LUA
		local prefix = ARGV[1]
		local start = ARGV[2]
		local limit = ARGV[3] - start + 1
		local argv_index = 4
		local sid_index = 0
		local rs = {}
		while (ARGV[argv_index]) do
		    local sids = redis.call('SMEMBERS', prefix..'idx_uid_'..ARGV[argv_index])
		    for _, key in ipairs(sids) do
			if (sid_index >= start) then
			    local row = redis.call("hmget", prefix..key, "sid", "ip", "uid", "username", "groupid", "invisible", "action", "lastactivity", "lastolupdate", "fid", "tid")
			    rs[#rs + 1] = row
			    if (#rs >= limit) then
				return rs
			    end
			end
			sid_index = sid_index + 1
		    end
		    argv_index = argv_index + 1
		end
		return rs
LUA;
		list($ss, $ee) = $this->get_start_and_end($start, $limit);
		$data = memory('eval', $script, array_merge(array($ss, $ee), $uids), "fetch_all_by_uid", $this->_pre_cache_key);
		return $this->array_from_memory_result($data);
	}

	public function update_by_uid($uid, $data) {
		if(!($uid = dintval($uid)) || empty($data) || !is_array($data)) {
			return 0;
		}

		$script = <<<LUA
		local prefix = ARGV[1]
		local uid = ARGV[2]
		local sids = redis.call('SMEMBERS', prefix..'idx_uid_'..uid)
LUA;
		$r = memory('eval', $script . self::LUA_RETURN_DATA, array($uid), "update_by_uid_query", $this->_pre_cache_key);
		$items = $this->array_from_memory_result($r);

		memory('pipeline');
		foreach ($items as $olditem) {
			$sid = $olditem['sid'];
			$data['sid'] = $sid; 	// $data中可能没有sid
			memory('hmset', $sid, $data, 0, $this->_pre_cache_key);
			$this->update_memory_index($sid, $data, $olditem);
		}
		memory('commit');
	}

	public function update_max_rows($max_rows) {
		return TRUE;
	}

	public function clear() {
		return TRUE;
	}

	public function count_by_fid($fid) {
		$fid = dintval($fid);
		if (!$fid) return 0;
		global $_G;
		$temp_uniq = substr(md5(substr(TIMESTAMP, 0, -3).substr($_G['config']['security']['authkey'], 3, -3)), 1, 8);
		$script = <<<LUA
		local prefix = ARGV[1]
		local fid = ARGV[2]
		local out_surfix = ARGV[3]
		local out_hash = prefix..'uid_fid_inv_'..out_surfix
		-- uid > 0 and fid=fid and invisible = 0
		redis.call("ZINTERSTORE", out_hash, 3, prefix.."idx_uid_group_1", prefix.."idx_fid_"..fid, prefix.."idx_invisible_0", 'AGGREGATE', 'MIN')
		local rs = redis.call("ZCARD", out_hash)
		redis.call("DEL", out_hash)
		return rs
LUA;
		$data = memory('eval', $script, array($fid, $temp_uniq), "count_by_fid", $this->_pre_cache_key);
		return $data;
	}

	public function fetch_all_by_fid($fid, $limit = 12) {
		$fid = dintval($fid);
		if (!$fid) return array();

		global $_G;
		$temp_uniq = substr(md5(substr(TIMESTAMP, 0, -3).substr($_G['config']['security']['authkey'], 3, -3)), 1, 8);
		$script = <<<LUA
		local prefix = ARGV[1]
		local fid = ARGV[2]
		local limit = ARGV[3]
		local out_surfix = ARGV[4]
		local out_hash = prefix..'uid_fid_inv_'..out_surfix

		-- uid > 0 and fid=fid and invisible = 0
		redis.call("ZINTERSTORE", out_hash, 3, prefix.."idx_uid_group_1", prefix.."idx_fid_"..fid, prefix.."idx_invisible_0", 'AGGREGATE', 'MIN')

		local keys = redis.call("ZREVRANGE", out_hash, 0, limit - 1)
		redis.call("DEL", out_hash)
		local rs = {}
		for _, key in ipairs(keys) do
		    local row = redis.call("hmget", prefix..key, "uid", "groupid", "username", "invisible", "lastactivity")
		    rs[#rs + 1] = row
		end
		return rs
LUA;
		$data = memory('eval', $script, array($fid, $limit, $temp_uniq), "fetch_all_by_fid", $this->_pre_cache_key);
		$result = array();
		foreach ($data as $row) {
			$item = array();
			$item['uid'] = $row[0];
			$item['groupid'] = $row[1];
			$item['username'] = $row[2];
			$item['invisible'] = $row[3];
			$item['lastactivity'] = $row[4];
			$result[] = $item;
		}
		return $result;
	}

	public function count_by_ip($ip) {
		if (empty($ip)) return 0;
		return memory('zcard', 'idx_ip_' . $ip, $this->_pre_cache_key);
	}

	public function fetch_all_by_ip($ip, $start = 0, $limit = 0) {
		if (empty($ip)) return array();

		list($ss, $ee) = $this->get_start_and_end($start, $limit);
		$script = <<<LUA
		local prefix = ARGV[1]
		local ip = ARGV[2]
		local start = ARGV[3]
		local stop = ARGV[4]
		local sids = redis.call('ZREVRANGE', prefix..'idx_ip_'..ip, start, stop)
LUA;
		$data = memory('eval', $script . self::LUA_RETURN_DATA, array($ip, $ss, $ee), "fetch_all_by_ip", $this->_pre_cache_key);
		return $this->array_from_memory_result($data);
	}

	public function insert($data, $return_insert_id = false, $replace = false, $silent = false) {
		$id = $data['sid'];
		memory('pipeline');
		memory('hmset', $id, $data, 0, $this->_pre_cache_key);
		$this->update_memory_index($id, $data);
		memory('commit');
	}

	public function update($val, $data, $unbuffered = false, $low_priority = false) {
		$olddata = $this->get_data_by_pk($val);
		memory('pipeline');
		memory('hmset', $val, $data, 0, $this->_pre_cache_key);
		$this->update_memory_index($val, $data, $olddata);
		memory('commit');
	}

	private function update_memory_index($sid, $newdata, $olddata = array()) {
		if (!empty($olddata) && !isset($olddata['lastactivity'])) {
			return;
		}
		if (!empty($olddata) && !isset($newdata['lastactivity'])) {
			$newdata['lastactivity'] = $olddata['lastactivity'];
		}
		foreach ($newdata as $col => $value) {
			if (!in_array($col, array("ip", "uid", "fid", "lastactivity", "invisible"))) continue;
			if (isset($olddata[$col])) {
				if ($olddata[$col] === $value && $olddata['lastactivity'] === $newdata['lastactivity']) {
					continue;
				}
				switch ($col) {
					case 'ip':
						memory('zrem', "idx_ip_" . $olddata[$col], $sid, 0, $this->_pre_cache_key);
						break;
					case 'lastactivity':
						memory('zrem', 'idx_lastactivity', $sid, 0, $this->_pre_cache_key);
						break;
					case 'fid':
						memory('zrem', "idx_fid_" . $olddata[$col], $sid, 0, $this->_pre_cache_key);
						break;
					case 'invisible':
						memory('zrem', "idx_invisible_" . $olddata[$col], $sid, 0, $this->_pre_cache_key);
						break;
					case 'uid':
						memory('zrem', "idx_uid_group_" . ($olddata[$col] == 0 ? '0' : '1'), $sid, 0, $this->_pre_cache_key);
						memory('srem', "idx_uid_" . $olddata[$col], $sid, 0, $this->_pre_cache_key);
						break;
					default:
						continue 2;
				}
			}
			switch ($col) {
				case 'ip':
				case 'fid':
				case 'invisible':
					memory('zadd', 'idx_' . $col . "_" . $value, $sid, $newdata['lastactivity'], $this->_pre_cache_key);
					break;
				case 'lastactivity':
					memory('zadd', 'idx_lastactivity', $sid, $newdata['lastactivity'], $this->_pre_cache_key);
					break;
				case 'uid':
					memory('zadd', "idx_uid_group_" . ($value == 0 ? '0' : '1'), $sid, $newdata['lastactivity'], $this->_pre_cache_key);
					memory('sadd', 'idx_uid_' . $value, $sid, 0, $this->_pre_cache_key);
					break;
				default:
					continue 2;
			}
		}
	}

	private function array_from_memory_result($data) {
		$result = array();
		foreach ($data as $row) {
			$item = array();
			$item['sid'] = $row[0];
			$item['ip'] = $row[1];
			$item['uid'] = $row[2];
			$item['username'] = $row[3];
			$item['groupid'] = $row[4];
			$item['invisible'] = $row[5];
			$item['action'] = $row[6];
			$item['lastactivity'] = $row[7];
			$item['lastolupdate'] = $row[8];
			$item['fid'] = $row[9];
			$item['tid'] = $row[10];
			$result[] = $item;
		}
		return $result;
	}

	private function get_data_by_pk($sid) {
		$data = memory('hgetall', $sid, $this->_pre_cache_key);
		return $data;
	}

	private function get_start_and_end($start, $limit) {
		$limit = intval($limit > 0 ? $limit : 0);
		$start = intval($start > 0 ? $start : 0);
		if ($start > 0 && $limit > 0) {
			return array($start, $start + $limit - 1);
		} elseif ($limit > 0) {
			return array(0, $limit - 1);
		} elseif ($start > 0) {
			return array(0, $start - 1);
		} else {
			return array(0, -1);
		}
	}
}

?>