<?php
namespace W3TC;

/**
 * Manages data statistics.
 * Metrics:
 *
 */
class UsageStatistics_StorageWriter {
	public static $slot_interval_secs = 60;
	private $keep_history_interval_secs = 600;

	private $cache_storage;

	/**
	 * Cached values, just keep state between calls
	 */
	private $hotspot_endtime;
	private $new_hotspot_endtime = 0;
	private $now;

	/**
	 * begin_ sets the state what should it perform later
	 * finish reacts to that state and finishes flushing
	 * values:
	 *   not_needed - no flushinig required now
	 *   require_db - database access has to be available to decide
	 *   flushing_began_by_cache - that process has been selected to
	 *     flush hotspot data based on cache data state, still that has to be
	 *     verified in database
	 */
	private $flush_state;



	public function __construct() {
		$this->cache_storage = Dispatcher::get_usage_statistics_cache();
	}



	public function counter_add( $metric, $value ) {
		if ( !is_null( $this->cache_storage ) )
			$this->cache_storage->counter_add( $metric, $value );
	}



	public function get_hotspot_end() {
		$option_storage = $this->get_option_storage();
		return $option_storage->get_hotspot_end();
	}



	private function get_option_storage() {
		if ( is_multisite() )
			return new _OptionStorageWpmu();
		else
			return new _OptionStorageSingleSite();
	}


	public function maybe_flush_hotspot_data() {
		$result = $this->begin_flush_hotspot_data();
		if ( $result == 'not_needed' )
			return;

		$this->finish_flush_hotspot_data();
	}



	/**
	 * Returns if finish_* should be called.
	 * It tries to pass as litte processes as possible to
	 * flushing_begain if multiple processes come here
	 * at the same time when hotspot time ended.
	 */
	public function begin_flush_hotspot_data() {
		if ( is_null( $this->hotspot_endtime ) ) {
			// if cache not recognized - means nothing is cached at all
			// so stats not collected
			if ( is_null( $this->cache_storage ) )
				return 'not_needed';

			$v = $this->cache_storage->get( 'hotspot_endtime' );
			$this->hotspot_endtime = ( isset( $v['content'] ) ? $v['content'] : 0 );
		}

		$hotspot_endtime_int = (int)$this->hotspot_endtime;
		$this->now = time();

		if ( $hotspot_endtime_int <= 0 ) {
			$this->flush_state = 'require_db';
		} else if ( $this->now < $hotspot_endtime_int ) {
				$this->flush_state = 'not_needed';
			} else {
			// rand value makes value unique for each process,
			// so as a result next replace works as a lock
			// passing only single process further
			$this->new_hotspot_endtime = $this->now + self::$slot_interval_secs +
				( rand( 1, 9999 ) / 10000.0 );

			$succeeded = $this->cache_storage->set_if_maybe_equals( 'hotspot_endtime',
				array( 'content' => $this->hotspot_endtime ),
				array( 'content' => $this->new_hotspot_endtime ) );
			$this->flush_state =
				( $succeeded ? 'flushing_began_by_cache' : 'not_needed' );
		}

		return $this->flush_state;
	}



	public function finish_flush_hotspot_data() {
		$option_storage = $this->get_option_storage();

		if ( $this->flush_state == 'not_needed' )
			return;

		if ( $this->flush_state != 'require_db' &&
			$this->flush_state != 'flushing_began_by_cache' )
			throw new Exception( 'unknown usage stats state ' . $this->flush_state );

		// check whats there in db
		$this->hotspot_endtime = $option_storage->get_hotspot_end();
		$hotspot_endtime_int = (int)$this->hotspot_endtime;

		if ( $this->now < $hotspot_endtime_int ) {
			// update cache, since there is something old/missing in cache
			$this->cache_storage->set( 'hotspot_endtime',
				array( 'content' => $this->hotspot_endtime ) );
			return;   // not neeeded really, db state after
		}
		if ( $this->new_hotspot_endtime <= 0 )
			$this->new_hotspot_endtime = $this->now +
				self::$slot_interval_secs +
				( rand( 1, 9999 ) / 10000.0 );

		if ( $hotspot_endtime_int <= 0 ) {
			// no data in options, initialization
			$this->cache_storage->set( 'hotspot_endtime',
				array( 'content' => $this->new_hotspot_endtime ) );
			update_site_option( 'w3tc_stats_hotspot_start', time() );
			$option_storage->set_hotspot_end( $this->new_hotspot_endtime );
			return;
		}

		// try to become the process who makes flushing by
		// performing atomic database update

		// rand value makes value unique for each process,
		// so as a result next replace works as a lock
		// passing only single process further
		$succeeded = $option_storage->prolong_hotspot_end(
			$this->hotspot_endtime, $this->new_hotspot_endtime );
		if ( !$succeeded )
			return;

		$this->cache_storage->set( 'hotspot_endtime',
			array( 'content' => $this->new_hotspot_endtime ) );


		// flush data
		$metrics = array();
		$metrics = apply_filters( 'w3tc_usage_statistics_metrics', $metrics );

		$metric_values = array();
		$metric_values['timestamp_start'] = get_site_option( 'w3tc_stats_hotspot_start' );
		$metric_values['timestamp_end'] = $hotspot_endtime_int;

		// try to limit time between get and reset of counter value
		// to loose as small as posssible
		foreach ( $metrics as $metric ) {
			$metric_values[$metric] = $this->cache_storage->counter_get( $metric );
			$this->cache_storage->counter_set( $metric, 0 );
		}

		$history_encoded = get_site_option( 'w3tc_stats_history' );
		$history = null;
		if ( !empty( $history_encoded ) )
			$history = json_decode( $history_encoded, true );
		if ( !is_array( $history ) )
			$history = array();

		$time_keep_border = time() - $this->keep_history_interval_secs;

		if ( $hotspot_endtime_int < $time_keep_border )
			$history = array(
				array(
					'timestamp_start' => $time_keep_border,
					'timestamp_end' => (int)$this->new_hotspot_endtime -
					self::$slot_interval_secs - 1
				)
			);   // this was started too much time from now
		else {
			// add collected
			$history[] = $metric_values;

			// if we empty place later - fill it
			for ( ;; ) {
				$metric_values = array(
					'timestamp_start' => $metric_values['timestamp_end']
				);
				$metric_values['timestamp_end'] =
					$metric_values['timestamp_start'] + self::$slot_interval_secs;
				if ( $metric_values['timestamp_end'] < $this->now )
					$history[] = $metric_values;
				else
					break;
			}

			// make sure we have at least one value in history
			for ( ;count( $history ) > 1; ) {
				if ( !isset( $history[0]['timestamp_end'] ) ||
					$history[0]['timestamp_end'] < $time_keep_border )
					array_shift( $history );
				else
					break;
			}
		}

		update_site_option( 'w3tc_stats_hotspot_start', $this->now );
		update_site_option( 'w3tc_stats_history', json_encode( $history ) );
	}
}



/**
 * Can update option by directly incrementing current value,
 * not via get+set operation
 */
class _OptionStorageSingleSite {
	private $option_hotspot_end = 'w3tc_stats_hotspot_end';



	public function get_hotspot_end() {
		global $wpdb;

		$row = $wpdb->get_row( $wpdb->prepare(
				'SELECT option_value ' .
				'FROM ' . $wpdb->options . ' ' .
				'WHERE option_name = %s LIMIT 1',
				$this->option_hotspot_end ) );

		if ( !is_object( $row ) )
			return false;

		$v = $row->option_value;
		return $v;
	}



	public function set_hotspot_end( $new_value ) {
		update_site_option( $this->option_hotspot_end, $new_value );
	}



	/**
	 * Performs atomic update of option value
	 * from old to new value. Makes sure that only single process updates it.
	 * Only single process gets true return value when multiple tries to do that.
	 */
	public function prolong_hotspot_end( $old_value, $new_value ) {
		global $wpdb;

		$q = $wpdb->prepare(
			'UPDATE ' . $wpdb->options . ' ' .
			'SET option_value = %s ' .
			'WHERE option_name = %s AND option_value = %s', $new_value,
			$this->option_hotspot_end, $old_value );

		$result = $wpdb->query( $q );
		$succeeded = ( $result > 0 );

		return $succeeded;
	}
}



/**
 * Can update option by directly incrementing current value,
 * not via get+set operation
 */
class _OptionStorageWpmu {
	private $option_hotspot_end = 'w3tc_stats_hotspot_end';



	public function get_hotspot_end() {
		global $wpdb;

		$row = $wpdb->get_row( $wpdb->prepare(
				'SELECT meta_value ' .
				'FROM ' . $wpdb->sitemeta . ' ' .
				'WHERE site_id = %d AND meta_key = %s',
				$wpdb->siteid, $this->option_hotspot_end ) );

		if ( !is_object( $row ) )
			return false;

		$v = $row->meta_value;
		return $v;
	}

	/**
	 * Performs atomic update of option value
	 * from old to new value. Makes sure that only single process updates it.
	 * Only single process gets true return value when multiple tries to do that.
	 */
	public function set_hotspot_end( $new_value ) {
		update_site_option( $this->option_hotspot_end, $new_value );
	}



	/**
	 * Performs atomic update of option value
	 * from old to new value. Makes sure that only single process updates it.
	 * Only single process gets true return value when multiple tries to do that.
	 */
	public function prolong_hotspot_end( $old_value, $new_value ) {
		global $wpdb;

		$result = $wpdb->query( $wpdb->prepare(
				'UPDATE ' . $wpdb->sitemeta . ' ' .
				'SET meta_value = %s ' .
				'WHERE site_id = %d AND meta_key = %s AND meta_value = %s',
				$new_value, $wpdb->siteid, $this->option_hotspot_end, $old_value ) );
		$succeeded = ( $result > 0 );

		return $succeeded;
	}
}
