<?php

# Make sure we have defined start time.
defined('START_TIME') or define('START_TIME', key_exists('REQUEST_TIME_FLOAT', $_SERVER) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime(true));

/**
 * Master class for controlling the time measurements.
 */
class Timer {

	/** @var TimerItem[] Pointers to the active items. * */
	private $stack = [];

	/** @var TimerItem[]  All of the single measurement timer items. * */
	private $items = [];

	/** @var TimerLoopItem[] The list of all loops. * */
	private $loops = [];

	/**
	 * Start a single measurement.
	 *
	 * @param string $name The name of the measurement.
	 *
	 * @throws Exception
	 */
	public static function start($name) {
		$instance = static::getInstance();

		# Sub item
		if (sizeof($instance->stack)) {
			$pointer = end($instance->stack);
			$children = $pointer->getChildren();

			# Top item
		} else {
			$pointer = null;
			$children = $instance->items;
		}

		# There must not be the same item twice
		if (key_exists($name, $children)) {
			throw new \Exception("Timer item named `{$name}` already exists in " . ($pointer ? '`' . $pointer->getName() . '` timer item' : 'top item'));
		}

		# Initialize the item
		$item = new TimerItem($name);
		if ($pointer !== null) {
			$pointer->addChild($item);
		} else {
			$instance->items[$item->getName()] = $item;
		}

		# Set the pointer
		$instance->stack[] =& $item;
	}

	/**
	 * End and measure a singe item.
	 *
	 * @param string $name The name of the measurement.
	 *
	 * @return float The total elapsed time of this item, in milliseconds.
	 * @throws Exception
	 */
	public static function end($name) {
		$instance = static::getInstance();

		# Validate: there must be a pointer
		if (sizeof($instance->stack) === 0) {
			throw new \Exception("Tried to end the timer item named `{$name}` there are no active timer items of any name.");
		}

		# Validate: names must match
		$pointer = array_pop($instance->stack);
		if ($pointer->getName() !== $name) {
			$active = $pointer->getName();
			throw new \Exception("Tried to end the timer item named `{$name}` but the timer item named `{$active}` is active.");
		}

		# Measure
		$pointer->end();
		return $pointer->getTime();
	}

	/**
	 * Start the measurement for the looped item.
	 *
	 * @param string $name The name of the looped item.
	 */
	public static function startLoop($name) {
		$instance = static::getInstance();

		# Create if not exists
		if (!key_exists($name, $instance->loops)) {
			$instance->loops[$name] = new TimerLoopItem($name);
		}

		# Start
		$instance->loops[$name]->start();
	}

	/**
	 * Measure a single loop.
	 *
	 * @param string $name The name of the looped item.
	 *
	 * @throws Exception
	 */
	public static function endLoop($name) {
		$instance = static::getInstance();

		# Validate
		if (!key_exists($name, $instance->loops)) {
			throw new \Exception("No timer loop item with the name {$name} defined.");
		}

		# Increment
		$instance->loops[$name]->measure();
	}

	/**
	 * Get the instance of the timer.
	 *
	 * @return Timer The active instance of the timer.
	 */
	public static function getInstance() {
		static $instance = null;
		if ($instance === null) {
			$instance = new Timer();
		}

		return $instance;
	}

	/**
	 * Get the total time elapsed since we defined the START_TIME constant.
	 *
	 * @return int mixed The total time elapsed, in milliseconds.
	 */
	public static function elapsed() {
		return microtime(true) - START_TIME;
	}

	/**
	 * Get all measured items.
	 *
	 * @return TimerItem[] The list of measured items.
	 */
	public static function getItems() {
		return static::getInstance()->items;
	}

	/**
	 * Get all measured loop items.
	 *
	 * @return TimerLoopItem[] The list of measured loop items.
	 */
	public static function getLoopItems() {
		return static::getInstance()->loops;
	}

}

/**
 * A timer item is used to measure the elapsed time of a single event.
 */
class TimerItem {

	/** @var string The name of item. * */
	private $name;

	/** @var float The time when the task started, in milliseconds. * */
	private $start = 0;

	/** @var float The total elapsed time, in milliseconds. * */
	private $time = 0;

	/** @var TimerItem[] The list of children items. * */
	private $children = [];

	/**
	 * Initialize the item and start measuring.
	 *
	 * @param string $name The name for item.
	 */
	function __construct($name) {
		$this->name = $name;
		$this->start = microtime(true);
	}

	/**
	 * End the measurement for this item.
	 *
	 * @return float The time elapsed, in milliseconds.
	 */
	function end() {
		return $this->time = microtime(true) - $this->start;
	}

	/**
	 * Add a child.
	 *
	 * @param TimerItem $child The chlid to add.
	 *
	 * @throws Exception
	 */
	function addChild(TimerItem $child) {

		# Make sure the child does not exist
		$name = $child->getName();
		if (key_exists($name, $this->children)) {
			throw new \Exception("Timer item named `{$name}` already exists in `" . $this->getName() . "` .");
		}

		$this->children[$child->getName()] = $child;
	}

	/**
	 * Get the name of the item.
	 *
	 * @return string The name of the item.
	 */
	function getName() {
		return $this->name;
	}

	/**
	 * Get the elapsed time of the item.
	 *
	 * @return float The total elapsed time, in milliseconds.
	 */
	function getTime() {
		return $this->time;
	}

	/**
	 * Get children items.
	 *
	 * @return array The list of children.
	 */
	function getChildren() {
		return $this->children;
	}

}

/**
 * A loop item is used to measure total elapsed time of recurring events.
 */
class TimerLoopItem {

	/** @var string The name of this loop. * */
	private $name;

	/** @var float The total elapsed time, in milliseconds. * */
	private $time = 0;

	/** @var int The number of loops. * */
	private $count = 0;

	/** @var float[] The start time of a single loop measurement. * */
	private $started = [];

	/**
	 * Initialize the loop item.
	 *
	 * @param string $name The name for this loop.
	 */
	public function __construct($name) {
		$this->name = $name;
	}

	/**
	 * Start a measurement of a single loop.
	 */
	public function start() {
		$this->started[] = microtime(true);
	}

	/**
	 * Add a time to the loop item.
	 *
	 * @return float The total time elapsed, in milliseconds.
	 * @throws Exception
	 */
	public function measure() {
		if ($this->name == 'validate')
			debug('OFF ' . $this->name) && flush();

		# Make sure we have started the loop timer
		if (empty($this->started)) {
			throw new \Exception("Loop timer with the name `{$this->name}` has not been started, but tried to be measured.");
		}

		# Add the time
		$this->time += microtime(true) - array_pop($this->started);
		$this->count++;

		return $this->time;
	}

	/**
	 * Get the name of this loop item.
	 *
	 * @return string The assigned name of this loop item.
	 */
	public function getName() {
		return $this->name;
	}

	/**
	 * Get the total elapsed time.
	 *
	 * @return float The total elapsed time.
	 */
	public function getTotalTime() {
		return $this->time;
	}

	/**
	 * Get the number of loops.
	 *
	 * @return int The number of loops.
	 */
	public function getCount() {
		return $this->count;
	}

}
