<?php
/**
 * File containing the class Sensei_Enrolment_Provider_Journal.
 *
 * @package sensei
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * This class represents a journal for a single provider.
 */
class Sensei_Enrolment_Provider_Journal implements JsonSerializable {
	const DEFAULT_HISTORY_SIZE        = 30;
	const DEFAULT_MESSAGE_LOG_SIZE    = 30;
	const HISTORY_TIMESTAMP_PRECISION = 0.001;

	/**
	 * The history of the provider's status. A null enrolment status marks a deletion of a provider. Each element of
	 * the array has the format:
	 *     [
	 *         'timestamp' => Timestamp of the status change,
	 *          'enrolment_status' => true|false|null
	 *     ]
	 *
	 * @var array
	 */
	private $history;

	/**
	 * The message log of the provider. Each element of the array has the format:
	 *     [
	 *         'timestamp' => Timestamp of the message,
	 *          'message' => The actual message
	 *     ]
	 *
	 * @var array
	 */
	private $message_log;

	/**
	 * Class constructor.
	 *
	 * @param array $history     The status history.
	 * @param array $message_log The message log.
	 */
	private function __construct( $history, $message_log ) {
		$this->history     = $history;
		$this->message_log = $message_log;
	}

	/**
	 * Restore a journal from a JSON string.
	 *
	 * @param array $data Serialized state of object.
	 *
	 * @return self|false
	 */
	public static function from_serialized_array( $data ) {

		if ( empty( $data ) ) {
			return false;
		}

		$history     = isset( $data['h'] ) ? array_filter( array_map( [ __CLASS__, 'deserialize_history_entry' ], $data['h'] ) ) : [];
		$message_log = isset( $data['l'] ) ? array_filter( array_map( [ __CLASS__, 'deserialize_log_entry' ], $data['l'] ) ) : [];

		return new self( $history, $message_log );
	}

	/**
	 * Create an empty journal.
	 *
	 * @return self
	 */
	public static function create() {
		return new self( [], [] );
	}

	/**
	 * Sanitize a log entry.
	 *
	 * @param  array $log_entry Non-sanitized log entry.
	 *
	 * @return array
	 */
	private static function deserialize_log_entry( $log_entry ) {
		if ( ! is_array( $log_entry ) ) {
			return false;
		}

		if ( 2 !== count( $log_entry ) ) {
			return false;
		}

		if ( ! isset( $log_entry['t'], $log_entry['m'] ) ) {
			return false;
		}

		return [
			'timestamp' => (int) $log_entry['t'],
			'message'   => sanitize_text_field( $log_entry['m'] ),
		];
	}

	/**
	 * Helper method to deserialize a history entry.
	 *
	 * @param array $entry The serialized history entry.
	 *
	 * @return array|bool
	 */
	private static function deserialize_history_entry( $entry ) {
		if ( ! isset( $entry['t'] ) || ! array_key_exists( 's', $entry ) ) {
			return false;
		}

		return [
			'timestamp'        => (float) $entry['t'],
			'enrolment_status' => null === $entry['s'] ? null : (bool) $entry['s'],
		];
	}

	#[\ReturnTypeWillChange]
	/**
	 * Return object that can be serialized by `json_encode()`.
	 *
	 * @return array
	 */
	public function jsonSerialize() {
		$history     = array_map( [ __CLASS__, 'serialize_history_entry' ], $this->history );
		$message_log = array_map( [ __CLASS__, 'serialize_log_entry' ], $this->message_log );

		$result = [];

		if ( ! empty( $history ) ) {
			$result['h'] = $history;
		}

		if ( ! empty( $message_log ) ) {
			$result['l'] = $message_log;
		}

		return $result;
	}

	/**
	 * Helper method to serialize a history entry.
	 *
	 * @param array $entry The deserialized history entry.
	 *
	 * @return array
	 */
	private function serialize_history_entry( $entry ) {
		return [
			't' => $entry['timestamp'],
			's' => $entry['enrolment_status'],
		];
	}

	/**
	 * Helper method to serialize a message log entry.
	 *
	 * @param array $entry The serialized log entry.
	 *
	 * @return array
	 */
	private function serialize_log_entry( $entry ) {
		return [
			't' => $entry['timestamp'],
			'm' => $entry['message'],
		];
	}

	/**
	 * Adds a message to the log.
	 *
	 * @param string $message The message.
	 */
	public function add_log_message( $message ) {
		/**
		 * Filter the maximum amount of log messages that are going to be stored for each user, course and provider.
		 *
		 * @since 3.0.0
		 *
		 * @hook sensei_enrolment_message_log_size
		 *
		 * @param {int} $message_log_size Default message log size.
		 * @return {int} Filtered message log size.
		 */
		$message_log_size = apply_filters( 'sensei_enrolment_message_log_size', self::DEFAULT_MESSAGE_LOG_SIZE );

		$message_entry = [
			'timestamp' => time(),
			'message'   => sanitize_text_field( $message ),
		];
		array_unshift( $this->message_log, $message_entry );
		array_splice( $this->message_log, $message_log_size );
	}

	/**
	 * Update the current enrolment status. If the status is not changed, this method has no effect.
	 *
	 * @param bool $enrolment_status The enrolment status.
	 *
	 * @return bool True if there was an update, false otherwise.
	 */
	public function update_enrolment_status( $enrolment_status ) {

		if ( empty( $this->history ) || $this->history[0]['enrolment_status'] !== $enrolment_status ) {
			$this->add_status(
				[
					'timestamp'        => microtime( true ),
					'enrolment_status' => $enrolment_status,
				]
			);

			return true;
		}

		return false;
	}

	/**
	 * Delete the enrolment status. A deleted enrolment status is marked by null.
	 *
	 * @return bool True if there was a deletion, false otherwise.
	 */
	public function delete_enrolment_status() {
		if ( ! empty( $this->history ) && null !== $this->history[0]['enrolment_status'] ) {
			$this->add_status(
				[
					'timestamp'        => microtime( true ),
					'enrolment_status' => null,
				]
			);

			return true;
		}

		return false;
	}

	/**
	 * Get the enrolment status at a specified timestamp.
	 *
	 * @param int $timestamp The timestamp to retrieve the status for.
	 *
	 * @return array The historical status entry.
	 */
	public function get_status_at( $timestamp ) {

		foreach ( $this->history as $status ) {
			if ( $status['timestamp'] < $timestamp || abs( $status['timestamp'] - $timestamp ) < self::HISTORY_TIMESTAMP_PRECISION ) {
				return $status;
			}
		}

		return [
			'timestamp'        => $timestamp,
			'enrolment_status' => null,
		];
	}

	/**
	 * Get the status history.
	 *
	 * @return array
	 */
	public function get_history() {
		return $this->history;
	}

	/**
	 * Get the message log.
	 *
	 * @return array
	 */
	public function get_logs() {
		return $this->message_log;
	}

	/**
	 * Helper method to add a status entry to the history.
	 *
	 * @param array $status The history entry.
	 */
	private function add_status( $status ) {
		/**
		 * Filter the maximum amount of historical entries that are going to be stored for each user, course and provider.
		 *
		 * @since 3.0.0
		 *
		 * @hook sensei_enrolment_history_size
		 *
		 * @param {int} $history_size Default history size.
		 * @return {int} Filtered history size.
		 */
		$history_size = apply_filters( 'sensei_enrolment_history_size', self::DEFAULT_HISTORY_SIZE );

		array_unshift( $this->history, $status );
		array_splice( $this->history, $history_size );
	}
}
