<?php

/**
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package fusionLib
 * @copyright Copyright (c) 2008 - 2015 fusionLib. All rights reserved.
 * @link http://fusionlib.com
 */

/**
 * flRequestThrottle Class
 *
 * Class to implement request throttling.
 *
 * @package fusionLib
 */
class flRequestThrottle {

	/**
	 * The database object.
	 *
	 * @var flDb
	 */
	private $db;

	/**
	 * The time frame over which requests are tracked.
	 *
	 * @var int
	 */
	private $timeFrame;

	/**
	 * The limit of request per time frame.
	 *
	 * @var int
	 */
	private $limit;

	/**
	 * The number of requests remaining.
	 *
	 * @var int
	 */
	private $remaining;

	/**
	 * The number of seconds wait required before a request can be made.
	 *
	 * @var int
	 */
	private $wait;

	/**
	 * The maximum time to throttle a request for.
	 * @var int
	 */
	private $maxThrottle;

	/**
	 * Construct the object.
	 *
	 * @param flDb $db The database object.
	 * @param int $timeFrame The time frame to track requests through in seconds e.g. 60
	 * @param int $limit The number of requests per time frame e.g. 100
	 * @param int $maxThrottle The maximum number of seconds to throttle a request for; 0 = no limit.
	 */
	function __construct($db, $timeFrame, $limit, $maxThrottle = 0) {
		$this->db = $db;
		$this->timeFrame = $timeFrame;
		$this->limit = $limit;
	}

	/**
	 * Get the request time frame.
	 *
	 * @return int Time frame in seconds.
	 */
	function getTimeFrame() {
		return $this->timeFrame;
	}

	/**
	 * Get the request limit.
	 *
	 * @return int The limit.
	 */
	function getLimit() {
		return $this->limit;
	}

	/**
	 * Get the requests left before no more will be accepted.
	 *
	 * @return int Remaining requests.
	 */
	function getRemaining() {
		return $this->remaining;
	}

	/**
	 * Get the number of seconds before requests will be accepted again.
	 *
	 * @return int The number of seconds.
	 */
	function getWait() {
		return $this->wait;
	}

	/**
	 * Test if the request should be throttled.
	 *
	 * @param string $key The database key e.g. client IP for a request or UUID for API
	 * request tracking.
	 * @param bool $throw true if an exception should be thrown
	 * @return bool true if the request is permitted or false if it is denied.
	 * @throws flRequestThrottleException
	 */
	function requestAllowed($key, $throw = true) {
		$now = new flDate();
		$key = md5($key);

		// Get the previous data or insert a new record
		if(!($row = $this->db->fetchRow('SELECT lasthit,throttle FROM fl_throttle WHERE id=?', $key)))
			$this->db->query('INSERT INTO fl_throttle (id,lasthit,throttle) VALUES(?, ?, ?)', $key, $now->toString(), '0');
		// Verify rate
		else {
			$lastHit = new flDate($row['lasthit']);
			$t = max(0, $row['throttle'] - ($now->getTimestamp() - $lastHit->getTimestamp())) + ($this->timeFrame / $this->limit);
			if($this->maxThrottle)
				$t = min($t, $this->maxThrottle);
			$this->db->query('UPDATE fl_throttle SET lasthit=?, throttle=? WHERE id=?', $now->toString(), $t, $key);

			$this->remaining = max(0, floor(($this->timeFrame - $t) * ($this->limit / $this->timeFrame)));
			if($t > $this->timeFrame) {
				$this->wait = ceil($t - $this->timeFrame);
				if($throw)
					throw new flRequestThrottleException($this);
				return false;
			}
			else
				$this->wait = 0;
		}

		// Remove entries that have not updated in 24 hours
		if(mt_rand(1, 10) == 1)
			$this->db->query('DELETE FROM fl_throttle WHERE lasthit<?', $now->setTimestamp($now->getTimestamp() - 86400));

		return true;
	}
}
