<?php

namespace Smush\Core\Stats;

use Smush\Core\Array_Utils;
use Smush\Core\Attachment_Id_List;
use Smush\Core\Media\Media_Item;
use Smush\Core\Media\Media_Item_Cache;
use Smush\Core\Media\Media_Item_Optimization_Global_Stats;
use Smush\Core\Media\Media_Item_Optimizer;
use Smush\Core\Media\Media_Item_Query;
use Smush\Core\Media\Media_Item_Stats;
use Smush\Core\Settings;
use Smush\Core\Threads\JSON_Record;

class Global_Stats {
	private static $global_stats_option_id = 'wp_smush_global_stats_json';
	private static $optimize_list_option_id = 'wp-smush-optimize-list-json';
	private static $reoptimize_list_option_id = 'wp-smush-reoptimize-list-json';
	private static $error_list_option_id = 'wp-smush-error-items-list-json';
	private static $ignore_list_option_id = 'wp-smush-ignored-items-list-json';
	private static $animated_list_option_id = 'wp-smush-animated-items-list-json';

	/**
	 * @var Global_Stats
	 */
	private static $instance;

	/**
	 * @var Media_Item_Optimization_Global_Stats[]
	 */
	private $optimization_stats;

	/**
	 * @var Attachment_Id_List
	 */
	private $optimize_list;

	/**
	 * @var Attachment_Id_List
	 */
	private $reoptimize_list;

	/**
	 * @var Attachment_Id_List
	 */
	private $error_list;

	/**
	 * @var Attachment_Id_List
	 */
	private $ignore_list;

	/**
	 * @var Attachment_Id_List
	 */
	private $animated_list;

	/**
	 * @var Media_Item_Cache
	 */
	private $media_item_cache;

	/**
	 * @var Array_Utils
	 */
	private $array_utils;

	private $media_item_query;

	/**
	 * @var JSON_Record
	 */
	private $global_stats_record;

	public function __construct() {
		$this->optimize_list   = new Attachment_Id_List( self::$optimize_list_option_id );
		$this->reoptimize_list = new Attachment_Id_List( self::$reoptimize_list_option_id );
		$this->error_list      = new Attachment_Id_List( self::$error_list_option_id );
		$this->ignore_list     = new Attachment_Id_List( self::$ignore_list_option_id );
		$this->animated_list   = new Attachment_Id_List( self::$animated_list_option_id );

		$this->media_item_cache    = Media_Item_Cache::get_instance();
		$this->array_utils         = new Array_Utils();
		$this->media_item_query    = new Media_Item_Query();
		$this->global_stats_record = new JSON_Record( self::$global_stats_option_id );
	}

	public static function get_global_stats_option_id() {
		return self::$global_stats_option_id;
	}

	public static function get() {
		if ( empty( self::$instance ) ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * @return Media_Item_Optimization_Global_Stats_Persistable[]
	 */
	public function get_persistable_stats_for_optimizations() {
		if ( is_null( $this->optimization_stats ) ) {
			$this->optimization_stats = $this->initialize_stats_for_optimizations();
		}

		return $this->optimization_stats;
	}

	private function initialize_stats_for_optimizations() {
		return apply_filters( 'wp_smush_global_optimization_stats', array() );
	}

	/**
	 * @param $optimization_key
	 *
	 * @return Media_Item_Optimization_Global_Stats
	 */
	public function create_global_stats_object( $optimization_key ) {
		return apply_filters(
			'wp_smush_optimization_global_stats_instance',
			new Media_Item_Optimization_Global_Stats(),
			$optimization_key
		);
	}

	/**
	 * @param $optimization_key
	 *
	 * @return Media_Item_Optimization_Global_Stats_Persistable
	 */
	public function get_persistable_stats_for_optimization( $optimization_key ) {
		$stats = $this->get_array_value(
			$this->get_persistable_stats_for_optimizations(),
			$optimization_key
		);
		if ( $stats ) {
			return $stats;
		} else {
			return new Media_Item_Optimization_Global_Stats_Persistable();
		}
	}

	private function get_array_value( $array, $key ) {
		return $array && isset( $array[ $key ] )
			? $array[ $key ]
			: null;
	}

	public function delete_global_stats_option() {
		$this->global_stats_record->delete();
	}

	private function get_global_stats_option_value( $key ) {
		return $this->global_stats_record->get_value( $key );
	}

	private function update_global_stats_option_value( $key, $value ) {
		$this->global_stats_record->set_values( array( $key => $value ) );
	}

	public function is_outdated() {
		if ( $this->is_media_library_empty() ) {
			return false;
		}

		$stats_updated_timestamp = $this->get_stats_updated_timestamp();
		if ( empty( $stats_updated_timestamp ) ) {
			// The scan has never been run
			return true;
		}

		$rescan_required_timestamp = $this->get_rescan_required_timestamp();
		if ( $rescan_required_timestamp > $stats_updated_timestamp ) {
			return true;
		}

		$stored_settings_digest = $this->get_stored_settings_digest();
		return $stored_settings_digest !== $this->calculate_settings_digest();
	}

	private function is_media_library_empty() {
		if ( 0 !== $this->get_image_attachment_count() ) {
			// Cached attachment count is not empty, so we definitely have some media items.
			// No need to make a DB call.
			return false;
		}

		return 0 === $this->media_item_query->get_image_attachment_count();
	}

	public function mark_as_outdated() {
		$this->update_rescan_required_timestamp( time() );
	}

	private function calculate_settings_digest() {
		$digest_parts = array();
		$keys         = array(
			'original',
			'lossy',
			'strip_exif',
		);

		$keys = apply_filters( 'wp_smush_global_stats_digest_keys', $keys, $this );

		$settings = Settings::get_instance();
		foreach ( $keys as $key ) {
			$digest_parts[] = "$key-" . (int) $settings->get( $key );
		}

		$options = array(
			'wp-smush-image_sizes',
		);

		$options = apply_filters( 'wp_smush_global_stats_digest_options', $options, $this );

		foreach ( $options as $option ) {
			$option_value      = $settings->get_setting( $option );
			$option_value      = empty( $option_value ) || ! is_array( $option_value ) ? array() : $option_value;
			$option_value_hash = ( new Array_Utils() )->array_hash( $option_value );

			if ( is_array( $option_value ) ) {
				$digest_parts[] = "$option-" . $option_value_hash;
			}
		}

		return implode( '-', $digest_parts );
	}

	private function get_stored_settings_digest() {
		return $this->get_global_stats_option_value( 'settings_digest' );
	}

	public function update_settings_digest() {
		$this->update_global_stats_option_value( 'settings_digest', $this->calculate_settings_digest() );
	}

	public function get_stats_update_started_timestamp() {
		return (int) $this->get_global_stats_option_value( 'stats_update_started_timestamp' );
	}

	public function update_stats_update_started_timestamp( $timestamp ) {
		$this->update_global_stats_option_value( 'stats_update_started_timestamp', $timestamp );
	}

	public function get_stats_updated_timestamp() {
		return (int) $this->get_global_stats_option_value( 'stats_updated_timestamp' );
	}

	public function update_stats_updated_timestamp( $timestamp ) {
		$this->update_global_stats_option_value( 'stats_updated_timestamp', $timestamp );
	}

	public function get_rescan_required_timestamp() {
		return (int) $this->get_global_stats_option_value( 'rescan_required_timestamp' );
	}

	public function update_rescan_required_timestamp( $timestamp ) {
		$this->update_global_stats_option_value( 'rescan_required_timestamp', $timestamp );
	}

	public function get_image_attachment_count() {
		$count = (int) $this->get_global_stats_option_value( 'image_attachment_count' );
		return max( $count, 0 );
	}

	/**
	 * Get the percentage of media items that failed optimization.
	 *
	 * @return float
	 */
	public function get_optimization_failed_percent() {
		$error_count     = $this->media_item_query->get_optimization_errors_count();
		$optimized_count = $this->get_total_optimizable_items_count() - $this->get_remaining_count();

		if ( $optimized_count <= 0 || $error_count <= 0 ) {
			return 0;
		}

		return ( $error_count / $optimized_count ) * 100;
	}

	public function add_image_attachment_count( $image_attachment_count ) {
		$this->global_stats_record->add_to_values(
			array( 'image_attachment_count' => $image_attachment_count )
		);
	}

	public function subtract_image_attachment_count( $image_attachment_count ) {
		$this->global_stats_record->subtract_from_values(
			array( 'image_attachment_count' => $image_attachment_count )
		);
	}

	public function get_optimized_images_count() {
		$count = (int) $this->get_global_stats_option_value( 'optimized_images_count' );
		return max( $count, 0 );
	}

	public function add_optimized_images_count( $optimized_images_count ) {
		$this->global_stats_record->add_to_values(
			array( 'optimized_images_count' => $optimized_images_count )
		);
	}

	public function subtract_optimized_images_count( $optimized_images_count ) {
		$this->global_stats_record->subtract_from_values(
			array( 'optimized_images_count' => $optimized_images_count )
		);
	}

	public function get_sum_of_optimization_global_stats() {
		$stats = new Media_Item_Stats();

		foreach ( $this->get_persistable_stats_for_optimizations() as $optimization ) {
			$stats->add( $optimization->get_stats() );
		}

		return $stats;
	}

	/**
	 * @return Attachment_Id_List
	 */
	public function get_optimize_list() {
		return $this->optimize_list;
	}

	public function get_redo_ids() {
		return array_merge(
			$this->get_reoptimize_list()->get_ids(),
			$this->get_error_list()->get_ids()
		);
	}

	public function get_redo_count() {
		return $this->get_reoptimize_list()->get_count()
		       + $this->get_error_list()->get_count();
	}

	/**
	 * @return Attachment_Id_List
	 */
	public function get_reoptimize_list() {
		return $this->reoptimize_list;
	}

	/**
	 * @return Attachment_Id_List
	 */
	public function get_error_list() {
		return $this->error_list;
	}

	/**
	 * @return Attachment_Id_List
	 */
	public function get_ignore_list() {
		return $this->ignore_list;
	}

	public function get_animated_list() {
		return $this->animated_list;
	}

	public function to_array() {
		$array = array(
			'is_outdated'            => $this->is_outdated(),
			'image_attachment_count' => $this->get_image_attachment_count(),
			'optimized_images_count' => $this->get_optimized_images_count(),
		);

		foreach ( $this->get_persistable_stats_for_optimizations() as $optimization_key => $optimization_stats ) {
			$array[ $optimization_key ] = $optimization_stats->get_stats()->to_array();
		}

		$array['optimize_list']    = $this->optimize_list->get_ids();
		$array['optimize_count']   = $this->optimize_list->get_count();
		$array['reoptimize_list']  = $this->reoptimize_list->get_ids();
		$array['reoptimize_count'] = $this->reoptimize_list->get_count();
		$array['error_list']       = $this->error_list->get_ids();
		$array['error_count']      = $this->error_list->get_count();
		$array['ignore_list']      = $this->ignore_list->get_ids();
		$array['ignore_count']     = $this->ignore_list->get_count();
		$array['animated_list']    = $this->animated_list->get_ids();
		$array['animated_count']   = $this->animated_list->get_count();

		$total_stats              = $this->get_sum_of_optimization_global_stats();
		$array['size_before']     = $total_stats->get_size_before();
		$array['size_after']      = $total_stats->get_size_after();
		$array['savings_percent'] = $total_stats->get_percent();

		$array['remaining_count'] = $this->get_remaining_count();

		$array['percent_optimized'] = $this->get_percent_optimized();
		$array['percent_metric']    = $this->get_percent_metric();
		$array['grade_class']       = $this->get_grade_class();

		$array['total_optimizable_items_count'] = $this->get_total_optimizable_items_count();
		$array['skipped_ids']                   = $this->get_skipped_ids();
		$array['skipped_count']                 = $this->get_skipped_count();

		return $array;
	}

	/**
	 * @return int
	 */
	public function get_remaining_count() {
		return $this->optimize_list->get_count()
		       + $this->reoptimize_list->get_count()
		       + $this->error_list->get_count();
	}


	public function reset() {
		$this->get_reoptimize_list()->delete_ids();
		$this->get_optimize_list()->delete_ids();
		$this->get_error_list()->delete_ids();
		$this->get_ignore_list()->delete_ids();
		$this->get_animated_list()->delete_ids();

		$this->delete_global_stats_option();
		foreach ( $this->get_persistable_stats_for_optimizations() as $persistable_stats_for_optimization ) {
			$persistable_stats_for_optimization->reset();
		}
	}


	/**
	 * Total number of items that could be optimized, this includes items that have already been optimized.
	 *
	 * For a count that only contains items yet to be optimized/reoptimized {@see self::get_remaining_count()}
	 *
	 * @return int
	 */
	public function get_total_optimizable_items_count() {
		return $this->get_image_attachment_count() - $this->get_skipped_count();
	}

	public function get_skipped_count() {
		return count( $this->get_skipped_ids() );
	}

	public function get_skipped_ids() {
		$skipped_ids = array_merge(
			$this->get_ignore_list()->get_ids(),
			$this->get_animated_list()->get_ids()
		);

		return $this->array_utils->fast_array_unique( $skipped_ids );
	}

	public function get_percent_optimized() {
		$total_optimizable_count = $this->get_total_optimizable_items_count();
		$remaining_count         = $this->get_remaining_count();
		if (
			$total_optimizable_count === 0 ||
			$total_optimizable_count <= $remaining_count
		) {
			return 0;
		}
		$percent_optimized = floor( ( $total_optimizable_count - $remaining_count ) * 100 / $total_optimizable_count );
		if ( $percent_optimized > 100 ) {
			$percent_optimized = 100;
		} elseif ( $percent_optimized < 0 ) {
			$percent_optimized = 0;
		}

		return $percent_optimized;
	}

	public function get_percent_metric() {
		$percent_optimized = $this->get_percent_optimized();

		return 0.0 === (float) $percent_optimized ? 100 : $percent_optimized;
	}

	public function get_grade_class() {
		$total_optimizable_items_count = $this->get_total_optimizable_items_count();
		if ( 0 === $total_optimizable_items_count ) {
			$grade = 'sui-grade-dismissed';
		} else {
			$percent_optimized = $this->get_percent_optimized();

			$grade = 'sui-grade-f';
			if ( $percent_optimized >= 60 && $percent_optimized < 90 ) {
				$grade = 'sui-grade-c';
			} elseif ( $percent_optimized >= 90 ) {
				$grade = 'sui-grade-a';
			}
		}

		return $grade;
	}

	/**
	 * @param $media_item Media_Item
	 *
	 * @return void
	 */
	public function remove_media_item( $media_item ) {
		$attachment_id = $media_item->get_id();

		// Remove from all the lists
		$this->remove_from_all_lists( $attachment_id );

		// Remove stats
		$this->subtract_item_stats( $media_item );
	}

	public function adjust_for_attachment( $attachment_id ) {
		$media_item = $this->media_item_cache->get( $attachment_id );
		$this->adjust_for_media_item( $media_item );
	}

	/**
	 * When the status of a media item changes this method can make the necessary changes to the global stats
	 *
	 * @param $media_item Media_Item
	 *
	 * @return void
	 */
	public function adjust_for_media_item( $media_item ) {
		$this->adjust_lists_for_media_item( $media_item );

		$belongs_in_stats = ! $media_item->is_skipped() && ! $media_item->has_errors();
		if ( $belongs_in_stats ) {
			$this->add_item_stats( $media_item );
		} else {
			$this->subtract_item_stats( $media_item );
		}
	}

	/**
	 * @param $media_item Media_Item
	 *
	 * @return void
	 */
	private function add_item_stats( $media_item ) {
		$optimizer = new Media_Item_Optimizer( $media_item );
		foreach ( $this->get_persistable_stats_for_optimizations() as $optimization_key => $optimization_global_stats ) {
			$optimization = $optimizer->get_optimization( $optimization_key );
			if ( $optimization && $optimization->is_optimized() ) {
				$optimization_global_stats->add_item_stats( $media_item->get_id(), $optimization->get_stats() );
			}
		}
	}

	/**
	 * @param $media_item Media_Item
	 *
	 * @return void
	 */
	public function subtract_item_stats( $media_item ) {
		$optimizer = new Media_Item_Optimizer( $media_item );
		foreach ( $this->get_persistable_stats_for_optimizations() as $optimization_key => $optimization_global_stats ) {
			$optimization = $optimizer->get_optimization( $optimization_key );
			if ( $optimization && $optimization->is_optimized() ) {
				$optimization_global_stats->subtract_item_stats( $media_item->get_id(), $optimization->get_stats() );
			}
		}
	}

	/**
	 * @param $attachment_id
	 *
	 * @return void
	 */
	private function remove_from_all_lists( $attachment_id ) {
		$this->get_optimize_list()->remove_id( $attachment_id );
		$this->get_reoptimize_list()->remove_id( $attachment_id );
		$this->get_error_list()->remove_id( $attachment_id );
		$this->get_ignore_list()->remove_id( $attachment_id );
		$this->get_animated_list()->remove_id( $attachment_id );
	}

	/**
	 * Also:
	 * @see Global_Stats_Controller::accumulate_attachment_ids()
	 */
	public function adjust_lists_for_media_item( $media_item ) {
		$attachment_id = $media_item->get_id();
		$optimizer     = new Media_Item_Optimizer( $media_item );

		// First remove from all the lists.
		$this->remove_from_all_lists( $attachment_id );

		// Now add only to the lists where it belongs.
		if ( $media_item->is_ignored() ) {
			$this->get_ignore_list()->add_id( $attachment_id );
		} elseif ( $media_item->is_animated() ) {
			$this->get_animated_list()->add_id( $attachment_id );
		} elseif ( $media_item->has_errors() ) {
			$this->get_error_list()->add_id( $attachment_id );
		} else {
			if ( $optimizer->is_optimized() ) {
				if ( $optimizer->should_reoptimize() ) {
					$this->get_reoptimize_list()->add_id( $attachment_id );
				}
			} else {
				if ( $optimizer->should_optimize() ) {
					$this->get_optimize_list()->add_id( $attachment_id );
				}
			}
		}
	}
}
