<?php

namespace Smush\Core\Media;

use Smush\Core\Backups\Backups;
use Smush\Core\Helper;
use Smush\Core\Smush\Smush_Optimization;
use Smush\Core\Stats\Global_Stats;
use WDEV_Logger;
use WP_Error;

class Media_Item_Optimizer {
	private static $error_meta_key = 'wp-smush-optimization-errors';

	/**
	 * @var Media_Item_Optimization[]
	 */
	private $optimizations;

	/**
	 * @var Media_Item
	 */
	private $media_item;
	/**
	 * @var Backups
	 */
	private $backups;
	/**
	 * @var WDEV_Logger
	 */
	private $logger;
	/**
	 * @var Global_Stats
	 */
	private $global_stats;

	/**
	 * @var WP_Error
	 */
	private $errors;

	/**
	 * Restoration errors.
	 *
	 * @var WP_Error
	 */
	private $restoration_errors;

	/**
	 * @param $media_item Media_Item
	 */
	public function __construct( $media_item ) {
		$this->media_item   = $media_item;
		$this->backups      = new Backups();
		$this->logger       = Helper::logger();
		$this->global_stats = Global_Stats::get();
	}

	/**
	 * @return Media_Item_Optimization[]
	 */
	public function get_optimizations() {
		if ( is_null( $this->optimizations ) ) {
			$this->optimizations = $this->initialize_optimizations();
		}

		return $this->optimizations;
	}

	public function set_optimizations( $optimizations ) {
		$this->optimizations = $optimizations;
	}

	private function initialize_optimizations() {
		return apply_filters( 'wp_smush_optimizations', array(), $this->media_item );
	}

	/**
	 * TODO: check the uses for this method to make sure they are prepared to receive null
	 *
	 * @param $key
	 *
	 * @return Media_Item_Optimization|null
	 */
	public function get_optimization( $key ) {
		return $this->get_array_value( $this->get_optimizations(), $key );
	}

	/**
	 * @param $key
	 *
	 * @return Media_Item_Stats
	 */
	public function get_stats( $key ) {
		$optimization = $this->get_optimization( $key );
		if ( $optimization ) {
			return $optimization->get_stats();
		}

		return new Media_Item_Stats();
	}

	public function get_total_stats() {
		$total_stats = new Media_Item_Stats();
		foreach ( $this->get_optimizations() as $optimization ) {
			$total_stats->add( $optimization->get_stats() );
		}

		return $total_stats;
	}

	/**
	 * @param $optimization_key
	 * @param $size_key
	 *
	 * @return Media_Item_Stats
	 */
	public function get_size_stats( $optimization_key, $size_key ) {
		$optimization = $this->get_optimization( $optimization_key );
		if ( $optimization ) {
			return $optimization->get_size_stats( $size_key );
		}

		return new Media_Item_Stats();
	}

	public function get_total_size_stats( $size_key ) {
		$total_stats = new Media_Item_Stats();
		foreach ( $this->get_optimizations() as $optimization ) {
			$total_stats->add( $optimization->get_size_stats( $size_key ) );
		}

		return $total_stats;
	}

	public function get_optimized_sizes_count() {
		$size_count = 0;
		foreach ( $this->get_optimizations() as $optimization ) {
			$optimized_sizes_count = $optimization->get_optimized_sizes_count();
			if ( $optimized_sizes_count > $size_count ) {
				$size_count = $optimized_sizes_count;
			}
		}

		return $size_count;
	}

	/**
	 * Whether the media item was optimized at some point. It may need to be reoptimized.
	 *
	 * @return bool
	 */
	public function is_optimized() {
		foreach ( $this->get_optimizations() as $optimization ) {
			if ( $optimization->is_optimized() ) {
				return true;
			}
		}

		return false;
	}

	public function should_optimize() {
		foreach ( $this->get_optimizations() as $optimization ) {
			if ( $optimization->should_optimize() ) {
				return true;
			}
		}

		return false;
	}

	public function should_reoptimize() {
		$should_reoptimize = false;
		foreach ( $this->get_optimizations() as $optimization ) {
			if ( $optimization->should_reoptimize() ) {
				$should_reoptimize = true;
			}
		}

		return apply_filters( 'wp_smush_should_resmush', $should_reoptimize, $this->media_item->get_id() );
	}

	public function optimize() {
		if ( $this->restore_in_progress() ) {
			$this->logger->log( 'Prevented auto-smush during restore.' );

			return false;
		}

		if ( $this->in_progress() ) {
			$this->handle_error( 'in_progress', 'Smush already in progress' );

			return false;
		}

		$third_party_errors = new WP_Error();
		$media_item         = $this->media_item;

		/**
		 * Fires before Smushing a file.
		 *
		 * @param int $attachment_id Attachment ID.
		 * @param array $ref_meta Metadata.
		 * @param WP_Error $third_party_errors A WP_Error object allowing third-parties to stop the smush process.
		 */
		do_action(
			'wp_smush_before_smush_attempt',
			$media_item->get_id(),
			$media_item->get_wp_metadata(),
			$third_party_errors
		);

		if ( $third_party_errors->has_errors() ) {
			$this->logger->log( 'Got errors from a third-party while executing the wp_smush_before_smush_file action.' );

			return false;
		}

		if ( $media_item->has_errors() || $media_item->is_skipped() ) {
			$this->adjust_global_stats_lists();

			return false;
		}

		do_action(
			'wp_smush_before_smush_file',
			$media_item->get_id(),
			$media_item->get_wp_metadata()
		);

		$this->set_in_progress_transient();

		$this->backups->maybe_create_backup( $media_item, $this );

		$optimized = $this->run_optimizations();

		if ( $optimized ) {
			do_action(
				'wp_smush_after_smush_successful',
				$media_item->get_id(),
				$media_item->get_wp_metadata()
			);

			$this->delete_previous_optimization_errors();
		} else {
			$this->handle_optimization_errors();
		}

		// This needs to be triggered after handle_optimization_errors so that get_errors will return correct errors
		do_action(
			'wp_smush_after_smush_file',
			$media_item->get_id(),
			$media_item->get_wp_metadata(),
			$optimized ? array() : $this->get_errors()
		);

		$this->delete_in_progress_transient();

		return $optimized;
	}

	public function restore() {
		if ( $this->in_progress() || $this->restore_in_progress() ) {
			return false;
		}

		$this->set_restore_in_progress_transient();

		$this->reset_restoration_errors();

		$restoration_attempted = false;
		$restored              = false;

		// First, allow one of the optimizations to handle the restoration process
		foreach ( $this->get_optimizations() as $optimization ) {
			if ( $optimization->can_restore() ) {
				$restoration_attempted = true;
				$restored              = $optimization->restore();
				break;
			}
		}

		if ( ! $restoration_attempted ) {
			// Try the standard restoration
			$restored = $this->backups->restore_backup( $this->media_item );
		}

		if ( $restored ) {
			// Before deleting all data subtract the stats
			$this->global_stats->subtract_item_stats( $this->media_item );
			$this->global_stats->subtract_optimized_images_count( $this->get_optimized_sizes_count() );

			// Delete all the optimization data
			$this->delete_data();

			// Delete optimization errors.
			$this->delete_previous_optimization_errors();

			// Once all data has been deleted, adjust the lists
			$this->global_stats->adjust_lists_for_media_item( $this->media_item );
		} else {
			$this->set_restoration_errors( $this->backups->get_errors() );
		}

		$this->delete_restore_in_progress_transient();

		return $restored;
	}

	public function save() {
		foreach ( $this->get_optimizations() as $optimization ) {
			$optimization->save();
		}
	}

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

	/**
	 * @param Media_Item_Size $full_size
	 *
	 * @return boolean
	 */
	public function should_optimize_size( $full_size ) {
		$should_optimize_size = false;
		foreach ( $this->get_optimizations() as $optimization ) {
			if ( $optimization->should_optimize_size( $full_size ) ) {
				$should_optimize_size = true;
				break;
			}
		}

		return $should_optimize_size;
	}

	public function delete_data() {
		foreach ( $this->get_optimizations() as $optimization ) {
			$optimization->delete_data();
		}
	}

	/**
	 * @return bool
	 */
	private function run_optimizations() {
		$all_optimized = true;
		foreach ( $this->get_optimizations() as $optimization ) {
			if ( $optimization->should_optimize() ) {
				$current_optimized = $optimization->optimize();
				$all_optimized     = $all_optimized && $current_optimized;
			}
		}

		return $all_optimized;
	}

	private function adjust_global_stats_lists() {
		$this->global_stats->adjust_lists_for_media_item( $this->media_item );
	}

	private function set_in_progress_transient() {
		set_transient( $this->in_progress_transient_key(), 1, HOUR_IN_SECONDS );
	}

	private function delete_in_progress_transient() {
		delete_transient( $this->in_progress_transient_key() );
	}

	public function in_progress() {
		return (bool) get_transient( $this->in_progress_transient_key() );
	}

	private function in_progress_transient_key() {
		return 'smush-in-progress-' . $this->media_item->get_id();
	}

	private function set_restore_in_progress_transient() {
		set_transient( $this->restore_in_progress_transient_key(), 1, HOUR_IN_SECONDS );
	}

	private function delete_restore_in_progress_transient() {
		delete_transient( $this->restore_in_progress_transient_key() );
	}

	public function restore_in_progress() {
		return (bool) get_transient( $this->restore_in_progress_transient_key() );
	}

	private function restore_in_progress_transient_key() {
		return 'wp-smush-restore-' . $this->media_item->get_id();
	}

	/**
	 * @param $code
	 * @param $error_message
	 *
	 * @return void
	 */
	private function handle_error( $code, $error_message ) {
		$this->logger->error( $error_message );
		$this->set_errors( new WP_Error( $code, $error_message ) );
		$this->update_errors_meta();
	}

	public function get_errors() {
		if ( is_null( $this->errors ) ) {
			$this->errors = $this->fetch_errors_from_meta();
		}

		return $this->errors;
	}

	private function set_errors( $errors ) {
		$this->errors = $errors;
	}

	public function has_errors() {
		return $this->get_errors()->has_errors();
	}

	private function set_optimization_errors() {
		$errors = new WP_Error();
		// Add optimization errors
		foreach ( $this->get_optimizations() as $optimization ) {
			if ( $optimization->has_errors() ) {
				$errors->merge_from( $optimization->get_errors() );
			}
		}

		$this->set_errors( $errors );
	}

	private function fetch_errors_from_meta() {
		$wp_error = new WP_Error();
		$errors   = get_post_meta( $this->media_item->get_id(), self::$error_meta_key, true );

		if ( empty( $errors ) || ! is_array( $errors ) ) {
			return $wp_error;
		}

		foreach ( $errors as $error_code => $error_message ) {
			if ( empty( $error_message ) ) {
				continue;
			}

			if ( is_array( $error_message ) ) {
				foreach ( $error_message as $error ) {
					$wp_error->add( $error_code, $error );
				}
			} else {
				$wp_error->add( $error_code, $error_message );
			}
		}

		return $wp_error;
	}

	private function update_errors_meta() {
		$errors_array = array();
		foreach ( $this->errors->get_error_codes() as $error_code ) {
			$errors_array[ $error_code ] = $this->errors->get_error_messages( $error_code );
		}

		if ( ! empty( $errors_array ) ) {
			update_post_meta( $this->media_item->get_id(), self::$error_meta_key, $errors_array );
		}
	}

	/**
	 * @return void
	 */
	private function handle_optimization_errors() {
		$this->set_optimization_errors();
		$this->update_errors_meta();
	}

	private function delete_previous_optimization_errors() {
		if ( $this->has_errors() ) {
			delete_post_meta( $this->media_item->get_id(), self::$error_meta_key );
			$this->set_errors( null );
		}
	}

	/**
	 * Reset restoration errors.
	 *
	 * @return void
	 */
	private function reset_restoration_errors() {
		$this->restoration_errors = null;
	}

	/**
	 * Set restoration errors.
	 *
	 * @param WP_Error $errors Restoration errors.
	 */
	private function set_restoration_errors( $errors ) {
		$this->restoration_errors = $errors;
	}

	/**
	 * Get restoration errors.
	 *
	 * @return WP_Error
	 */
	public function get_restoration_errors() {
		if ( ! $this->restoration_errors ) {
			$this->restoration_errors = new WP_Error();
		}

		return $this->restoration_errors;
	}

	/**
	 * Get error_meta_key.
	 *
	 * @return string
	 */
	public static function get_error_meta_key() {
		return self::$error_meta_key;
	}

}
