<?php
/**
 * File to load notifications.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * Class for the entry point of notifications.
 */
class SimpleStats_Notifications_Loader {

	/**
	 * Hold the instance.
	 *
	 * @var self
	 */
	private static $instance;

	/**
	 * The name of the option used to store the data.
	 *
	 * @var string
	 */
	private $option_name = 'simplestats_notifications';

	/**
	 * The key used to store in the options' table the last run times for notifications.
	 *
	 * @var string
	 */
	private $last_run_key = 'simplestats_notifications_run';

	/**
	 * Here we will store all the notification classes.
	 *
	 * @var SimpleStats_Notification_Event[]
	 */
	private $notifications = array();

	/**
	 * Here we will store value getting from DB.
	 *
	 * @var array
	 */
	private $last_runs;

	/**
	 * Notification option value.
	 *
	 * @var array
	 */
	private $option;

	/**
	 * Get instance of class.
	 *
	 * @return self
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Constructor function of the class.
	 */
	private function __construct() {
		$this->register_notifications();
	}

	/**
	 * Get notifications to response to vue.
	 *
	 * @return array
	 */
	public function get_ajax_notifications() {
		// It will generate once in a day.
		$this->maybe_generate_notifications();

		return $this->get_option( false );
	}

	/**
	 * Take decision to generate all notifications.
	 *
	 * @return void
	 */
	private function maybe_generate_notifications() {
		$last_runs = $this->get_notifications_last_run();

		if ( empty( $last_runs['last_checked'] ) ) {
			$this->generate_notifications();

			return;
		}

		// If time (1 day) passed, re-check all notifications.
		if ( $last_runs['last_checked'] + DAY_IN_SECONDS < time() ) {
			$this->generate_notifications();
		}
	}

	/**
	 * Register notifications classes.
	 *
	 * @return void
	 */
	private function register_notifications() {
		require_once __DIR__ . '/notification-event.php';
		require_once __DIR__ . '/notification-welcome.php';

		$this->notifications[] = new SimpleStats_Notification_Welcome();
	}

	/**
	 * Generate all notifications.
	 *
	 * @return void
	 */
	private function generate_notifications() {
		$last_runs = $this->get_notifications_last_run();
		$time_now  = time();

		// Loop through all notification classes
		foreach ( $this->notifications as $notification_event ) {
			if ( empty( $notification_event->id ) ) {
				continue;
			}

			// Check we need to add or not.
			if ( ! $this->check_interval( $notification_event, $last_runs, $time_now ) ) {
				continue;
			}

			$added_notification = $this->maybe_add_notification( $notification_event->prepare() );

			// Update the last run date as right now.
			$this->update_last_run( $notification_event->id, $time_now );

			// Avoid adding multiple notifications at the same time.
			if ( $added_notification ) {
				break;
			}
		}

		// Update the option with the new times.
		$this->save_last_runs();
	}

	/**
	 * Get the stored option for the last run times.
	 *
	 * @return array
	 */
	private function get_notifications_last_run() {
		if ( ! isset( $this->last_runs ) ) {
			$this->last_runs = get_option( $this->last_run_key );
		}

		if ( empty( $this->last_runs ) ) {
			$this->last_runs = array(
				'notifications' => array(),
				'last_checked'  => 0,
			);
		}

		return $this->last_runs;
	}

	/**
	 * Update the last run time with a default of time.
	 *
	 * @param string $notification_id The notification id to update the last run time for.
	 * @param int $time The timestamp to store the last run time.
	 */
	private function update_last_run( $notification_id, $time = 0 ) {
		if ( ! $time ) {
			$time = time();
		}

		$this->last_runs['notifications'][ $notification_id ] = $time;
	}

	/**
	 * Update the option stored in the db with the last run times.
	 */
	private function save_last_runs() {
		$this->last_runs['last_checked'] = time();

		update_option( $this->last_run_key, $this->last_runs, false );
	}

	/**
	 * Check notification interval.
	 *
	 * @param SimpleStats_Notification_Event $notification_event
	 * @param array $last_runs
	 * @param int $time_now
	 *
	 * @return bool
	 */
	private function check_interval( $notification_event, $last_runs, $time_now ) {
		// If we need to add notification immediately.
		if ( ! $notification_event->interval ) {
			return true;
		}

		// Last-run times live under the 'notifications' key — that is where
		// update_last_run() writes them. Reading them from the top level (as this
		// did before) meant the lookup below never found an entry, so every
		// interval-based notification was permanently stuck on the "first seen"
		// branch and could never display.
		$runs = isset( $last_runs['notifications'] ) && is_array( $last_runs['notifications'] )
			? $last_runs['notifications']
			: array();

		// If notification has interval and has not been added to $last_runs.
		if ( empty( $runs[ $notification_event->id ] ) ) {
			// Update the last run date as right now.
			$this->update_last_run( $notification_event->id, $time_now );

			return false;
		}

		// Has run before so let's check if enough days passed since the last run.
		$time_since = $runs[ $notification_event->id ] + $notification_event->interval * DAY_IN_SECONDS;

		return $time_since < $time_now;
	}

	/**
	 * Add notification to option.
	 *
	 * @param SimpleStats_Notification_Event $notification
	 *
	 * @return bool
	 */
	private function maybe_add_notification( $notification ) {
		// Check is already dismissed.
		if ( $this->is_dismissed( $notification ) ) {
			return false;
		}

		// Check we should display the notification or not.
		if ( $notification->should_display() ) {
			return $this->add_notification( $notification );
		}

		return false;
	}

	/**
	 * Check if a notification has been dismissed before
	 *
	 * @param SimpleStats_Notification_Event $notification
	 *
	 * @return bool
	 */
	private function is_dismissed( $notification ) {
		$option = $this->get_option();

		foreach ( $option['dismissed'] as $item ) {
			if ( isset( $item['id'] ) && $item['id'] === $notification->id ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Get option value.
	 *
	 * Entries are normalized to plain arrays on the way out, so callers never
	 * have to care whether a row was written by this version (array) or by
	 * 1.0.0 (serialized object).
	 *
	 * @param bool $cache Reference property cache if available.
	 *
	 * @return array
	 */
	private function get_option( $cache = true ) {
		if ( $this->option && $cache ) {
			return $this->option;
		}

		$option = get_option( $this->option_name, array() );

		$this->option = array(
			'events'    => $this->normalize_list( isset( $option['events'] ) ? $option['events'] : array() ),
			'dismissed' => $this->normalize_list( isset( $option['dismissed'] ) ? $option['dismissed'] : array() ),
		);

		return $this->option;
	}

	/**
	 * Normalize a stored list of notifications to a re-indexed array of arrays.
	 *
	 * @param mixed $list Raw value from the option.
	 *
	 * @return array
	 */
	private function normalize_list( $list ) {
		if ( ! is_array( $list ) || empty( $list ) ) {
			return array();
		}

		$normalized = array();

		foreach ( $list as $item ) {
			$item = $this->normalize_notification( $item );

			// Drop rows with no id — nothing can match or dismiss them.
			if ( ! empty( $item['id'] ) ) {
				$normalized[] = $item;
			}
		}

		return $normalized;
	}

	/**
	 * Coerce a single stored notification into a plain array.
	 *
	 * Rows written by 1.0.0 are serialized SimpleStats_Notification_Event
	 * objects. Casting handles both a properly-unserialized object and a
	 * __PHP_Incomplete_Class (the option read before the class files load) —
	 * the cast exposes the public properties in either case, and PHP's marker
	 * key is the only thing that has to be dropped.
	 *
	 * @param mixed $item Array, object, or junk.
	 *
	 * @return array
	 */
	private function normalize_notification( $item ) {
		if ( is_object( $item ) ) {
			$item = (array) $item;
			unset( $item['__PHP_Incomplete_Class_Name'] );
		}

		return is_array( $item ) ? $item : array();
	}

	/**
	 * Add notification to option.
	 *
	 * @param SimpleStats_Notification_Event $notification
	 *
	 * @return bool
	 */
	private function add_notification( $notification ) {
		$option = $this->get_option( false );

		$current_notifications = $option['events'];

		// Check notification is already added.
		foreach ( $current_notifications as $item ) {
			if ( isset( $item['id'] ) && $item['id'] === $notification->id ) {
				return false;
			}
		}

		// Stored as a plain array, never as a serialized object — see
		// SimpleStats_Notification_Event::to_array().
		array_unshift( $current_notifications, $notification->to_array() );

		update_option(
			$this->option_name,
			array(
				'events'    => $current_notifications,
				'dismissed' => $option['dismissed'],
			),
			false
		);

		return true;
	}

	/**
	 * Dismiss notification by id
	 *
	 * @param $id
	 * @return void
	 */
	public function dismiss_by_id( $id ) {
		// Read straight from the DB: a cached copy from earlier in the request
		// could be missing a notification another request just dismissed, and
		// writing it back would resurrect it.
		$option = $this->get_option( false );
		$type   = 'events';

		// Remove notification and add in dismissed array.
		foreach ( $option[ $type ] as $key => $notification ) {
			if ( isset( $notification['id'] ) && $notification['id'] === (string) $id ) {
				// Add notification to dismissed array.
				array_unshift( $option['dismissed'], $notification );
				// Remove notification from events.
				unset( $option[ $type ][ $key ] );
				break;
			}
		}

		// Re-index after the unset so the stored list stays a JSON array rather
		// than becoming an object with numeric keys on the next read.
		$option[ $type ] = array_values( $option[ $type ] );

		update_option( $this->option_name, $option, false );

		// Keep the in-request cache consistent with what we just wrote.
		$this->option = $option;
	}
}
