<?php

namespace Smush\Core\Media;

use Smush\Core\Animated_Status_Controller;
use Smush\Core\Array_Utils;
use Smush\Core\Backup_Size;
use Smush\Core\File_System;
use Smush\Core\Helper;
use Smush\Core\Settings;
use Smush\Core\Smush_File;
use WP_Error;
use WP_Smush;

class Media_Item extends Smush_File {
	private static $animated_meta_key = 'wp-smush-animated';
	private static $transparent_meta_key = 'wp-smush-transparent';
	private static $ignored_meta_key = 'wp-smush-ignore-bulk';

	private static $size_key_scaled = 'wp_scaled';
	private static $size_key_full = 'full';
	private static $backup_sizes_meta_key = '_wp_attachment_backup_sizes';
	private static $default_backup_key = 'smush-full';

	private $id;
	/**
	 * @var array|false
	 */
	private $metadata;
	/**
	 * @var string
	 */
	private $edit_url;
	/**
	 * @var string
	 */
	private $file;
	/**
	 * @var Media_Item_Size[]
	 */
	private $sizes;
	/**
	 * @var Settings
	 */
	private $plugin_settings;
	/**
	 * @var \WP_Post
	 */
	private $post;
	/**
	 * @var string[]
	 */
	private $animated_mime_types = array( 'image/gif' );
	/**
	 * @var bool
	 */
	private $animated;
	/**
	 * @var bool
	 */
	private $transparent;
	/**
	 * @var int
	 */
	private $ignored;
	/**
	 * @var int
	 */
	private $size_limit;
	/**
	 * @var WP_Error
	 */
	private $errors;
	/**
	 * @var array
	 */
	private $smushable_sizes;
	/**
	 * @var bool
	 */
	private $is_image;
	/**
	 * @var bool
	 */
	private $mime_type_supported;
	/**
	 * @var array[]
	 */
	private $missing_sizes;

	private $reset_properties = array(
		'metadata',
		'file',
		'sizes',
		'post',
		'animated',
		'ignored',
		'errors',
		'smushable_sizes',
		'is_image',
		'mime_type_supported',
		'missing_sizes',
		'backup_sizes',
		'mime_type',
		'attached_file',
		'original_image_path',
		'outdated_meta_values',
	);
	/**
	 * @var Backup_Size[]
	 */
	private $backup_sizes;
	/**
	 * @var string
	 */
	private $mime_type;
	/**
	 * @var string
	 */
	private $attached_file;
	/**
	 * @var false|string
	 */
	private $original_image_path;
	/**
	 * @var Array_Utils
	 */
	private $array_utils;
	private $registered_wp_sizes;
	private $outdated_meta_values = array();
	/**
	 * @var File_System
	 */
	private $fs;

	public function __construct( $id ) {
		$this->id = $id;

		$this->set_settings( Settings::get_instance() );
		$this->array_utils = new Array_Utils();
		$this->fs          = new File_System();
	}

	public static function get_animated_meta_key() {
		return self::$animated_meta_key;
	}

	public static function get_size_key_scaled() {
		return self::$size_key_scaled;
	}

	public static function get_default_backup_key() {
		return self::$default_backup_key;
	}

	public static function get_ignored_meta_key() {
		return self::$ignored_meta_key;
	}

	public static function get_backup_sizes_meta_key() {
		return self::$backup_sizes_meta_key;
	}

	public static function get_size_key_full() {
		return self::$size_key_full;
	}

	public static function get_transparent_meta_key() {
		return self::$transparent_meta_key;
	}

	public function size_limit_exceeded() {
		foreach ( $this->get_smushable_sizes() as $size ) {
			if ( $size->exceeds_size_limit() ) {
				return true;
			}
		}

		return false;
	}

	private function get_file_name_exceeding_limit() {
		foreach ( $this->get_smushable_sizes() as $size ) {
			if ( $size->exceeds_size_limit() ) {
				return $size->get_file_name();
			}
		}

		return '';
	}

	public function set_size_limit( $size_limit ) {
		$this->size_limit = $size_limit;
	}

	public function get_size_limit() {
		if ( is_null( $this->size_limit ) ) {
			$this->size_limit = Settings::get_instance()->get_file_size_limit();
		}
		return $this->size_limit;
	}

	public function get_human_size_limit() {
		return size_format( $this->get_size_limit() );
	}

	public function get_id() {
		return $this->id;
	}

	/**
	 * Checks whether important metadata exists for the media item.
	 *
	 * Missing metadata for a size does not mean there is a problem, for example data
	 * is not generated if the image is too small to generate a 'large' version.
	 * So we don't check metadata for sizes.
	 *
	 * @return bool
	 */
	public function has_wp_metadata() {
		$metadata = $this->get_wp_metadata();

		return is_array( $metadata )
		       && ! empty( $metadata )
		       && ! empty( $metadata['file'] );
	}

	private function get_missing_sizes() {
		if ( is_null( $this->missing_sizes ) ) {
			$this->missing_sizes = wp_get_missing_image_subsizes( $this->get_id() );
		}

		return $this->missing_sizes;
	}

	/**
	 * TODO: maybe add an error for this
	 * @return bool
	 */
	public function has_missing_sizes() {
		return ! empty( $this->get_missing_sizes() );
	}

	public function get_wp_metadata() {
		if ( empty( $this->metadata ) ) {
			$this->metadata = $this->fetch_wp_metadata();
		}

		return $this->metadata;
	}

	private function fetch_wp_metadata() {
		$attachment_metadata = wp_get_attachment_metadata( $this->get_id() );

		return $this->array_utils->ensure_array( $attachment_metadata );
	}

	/**
	 * @return void
	 */
	private function update_wp_metadata() {
		$updated_attachment_meta = $this->make_attachment_meta();
		if ( ! $this->arrays_same( $this->get_wp_metadata(), $updated_attachment_meta ) ) {
			wp_update_attachment_metadata( $this->get_id(), $updated_attachment_meta );
		}
	}

	private function file_name_from_path( $file_path ) {
		return wp_basename( $file_path );
	}

	/**
	 * TODO: use this instead of Helper::get_image_media_link
	 * @return string
	 */
	public function get_edit_url() {
		if ( is_null( $this->edit_url ) ) {
			$this->edit_url = $this->prepare_edit_url();
		}

		return $this->edit_url;
	}

	private function prepare_edit_url() {
		$mode     = get_user_option( 'media_library_mode' );
		$image_id = $this->get_id();
		if ( 'grid' === $mode ) {
			$edit_link = admin_url( "upload.php?item={$image_id}" );
		} else {
			$edit_link = admin_url( "post.php?post={$image_id}&action=edit" );
		}

		return $edit_link;
	}

	public function get_edit_link() {
		$name = $this->get_full_or_scaled_size()->get_file_name();
		$url = $this->get_edit_url();

		return "<a href='$url'>$name</a>";
	}

	/**
	 * File dir relative to the uploads directory e.g. 2023/05/. Includes trailing slash.
	 * @return string
	 */
	public function get_relative_file_dir() {
		$relative_file_dir = dirname( $this->get_relative_file_path() );
		if ( '.' === $relative_file_dir ) {
			return '';
		}
		return trailingslashit( $relative_file_dir );
	}

	/**
	 * The relative file path e.g. 2023/05/image.png
	 *
	 * @return string
	 */
	public function get_relative_file_path() {
		if ( is_null( $this->file ) ) {
			$this->file = $this->prepare_relative_file_path();
		}

		return $this->file;
	}

	private function prepare_relative_file_path() {
		$file = (string) $this->get_array_value( $this->get_wp_metadata(), 'file' );

		if ( empty( $file ) ) {
			/**
			 * If metadata is missing we still want some of our functions to work, e.g. backup and restore
			 *
			 * Using _wp_attached_file meta because:
			 * 1. get_attached_file returns the full path but the _wp_attached_file meta has the relative path we need
			 * 2. get_attached_file is filtered which can interfere with our code e.g. the S3 module changes attached file, but we don't want that
			 */
			$file = $this->get_post_meta( '_wp_attached_file' );
		}

		return $file;
	}

	private function get_post_meta( $key ) {
		return get_post_meta( $this->get_id(), $key, true );
	}

	public function has_size( $key ) {
		$sizes = $this->get_sizes();

		return ! empty( $sizes[ $key ] );
	}

	public function has_scaled_size() {
		return $this->has_size( self::$size_key_scaled );
	}

	public function has_full_size() {
		return $this->has_size( self::$size_key_full );
	}

	/**
	 * @param $key
	 *
	 * @return Media_Item_Size
	 */
	public function get_size( $key ) {
		return $this->get_array_value( $this->get_sizes(), $key );
	}

	public function get_scaled_size() {
		return $this->get_size( self::$size_key_scaled );
	}

	public function get_full_size() {
		return $this->get_size( self::$size_key_full );
	}

	public function get_sizes() {
		if ( is_null( $this->sizes ) ) {
			$this->sizes = $this->prepare_sizes();
		}

		return $this->sizes;
	}

	/**
	 * The 'main' size is the size has get_attached_file as the file path
	 *
	 * @return Media_Item_Size
	 */
	public function get_main_size() {
		return $this->get_scaled_or_full_size();
	}

	private function prepare_sizes() {
		$media_item_sizes = array();

		$metadata_sizes = $this->get_wp_metadata_sizes();
		foreach ( $metadata_sizes as $size_key => $metadata_size ) {
			$registered_size = $this->array_utils->ensure_array( $this->get_registered_wp_size( $size_key ) );
			$metadata_size   = $this->array_utils->ensure_array( $metadata_size );
			$size            = $this->initialize_size( $size_key, array_merge( $registered_size, $metadata_size ) );

			if ( $size ) {
				$media_item_sizes[ $size_key ] = $size;
			}
		}

		$scaled_size = $this->prepare_scaled_size();
		if ( $scaled_size ) {
			$media_item_sizes[ self::$size_key_scaled ] = $scaled_size;
		}

		$full_size = $this->prepare_full_size();
		if ( $full_size ) {
			$media_item_sizes[ self::$size_key_full ] = $full_size;
		}

		return $media_item_sizes;
	}

	public function prepare_scaled_size() {
		$file = $this->get_attached_file();
		if ( $file && $this->separate_original_image_path_exists() ) {
			$wp_size_metadata = $this->attachment_metadata_as_size_metadata( $file );

			return $this->initialize_size( self::$size_key_scaled, $wp_size_metadata );
		}

		return null;
	}

	private function separate_original_image_path_exists() {
		$original_image = $this->get_original_image_path();
		$main_file      = $this->get_attached_file();

		return $original_image !== $main_file;
	}

	public function prepare_full_size() {
		$original_image_exists = $this->separate_original_image_path_exists();

		if ( $original_image_exists ) {
			$original_image_file = $this->get_original_image_path();
			$image_size          = $this->fs->file_exists( $original_image_file )
				? $this->fs->getimagesize( $original_image_file )
				: false;
			if ( ! $image_size ) {
				return null;
			}

			return $this->initialize_size( self::$size_key_full, array(
				'file'      => $this->file_name_from_path( $original_image_file ),
				'width'     => $image_size[0],
				'height'    => $image_size[1],
				'mime-type' => $this->get_mime_type(),
				'filesize'  => $this->fs->filesize( $original_image_file ),
			) );
		} else {
			$main_file        = $this->get_attached_file();
			$wp_size_metadata = $this->attachment_metadata_as_size_metadata( $main_file );

			return $this->initialize_size( self::$size_key_full, $wp_size_metadata );
		}
	}

	public function has_smushable_sizes() {
		return ! empty( $this->get_smushable_sizes() );
	}

	/**
	 * @return Media_Item_Size[]
	 */
	public function get_smushable_sizes() {
		if ( is_null( $this->smushable_sizes ) ) {
			$this->smushable_sizes = $this->prepare_smushable_sizes();
		}

		return $this->smushable_sizes;
	}

	private function prepare_smushable_sizes() {
		$sizes = array();
		foreach ( $this->get_sizes() as $size_key => $size ) {
			if ( $size->is_smushable() ) {
				$sizes[ $size_key ] = $size;
			}
		}

		return $sizes;
	}

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

	/**
	 * @return array|mixed
	 */
	private function get_wp_metadata_sizes() {
		// TODO: media items created before a certain WP version might not have the scaled size so that needs to be normalized for all wp versions
		$metadata = $this->get_wp_metadata();

		return empty( $metadata['sizes'] )
			? array()
			: $metadata['sizes'];
	}

	private function get_wp_metadata_size( $size_key ) {
		$metadata = $this->get_wp_metadata_sizes();

		return empty( $metadata[ $size_key ] )
			? array()
			: $metadata[ $size_key ];
	}

	public function is_skipped() {
		return $this->is_ignored() ||
		       $this->is_animated();
	}

	public function is_mime_type_supported() {
		if ( is_null( $this->mime_type_supported ) ) {
			$this->mime_type_supported = $this->check_is_mime_type_supported();
		}

		return $this->mime_type_supported;
	}

	private function check_is_mime_type_supported() {
		$mime_type = $this->get_mime_type();
		$supported = in_array( $mime_type, $this->get_supported_mime_types(), true );

		return apply_filters( 'wp_smush_resmush_mime_supported', $supported, $mime_type );
	}

	public function is_image() {
		if ( is_null( $this->is_image ) ) {
			$this->is_image = $this->check_is_image();
		}

		return $this->is_image;
	}

	private function check_is_image() {
		return wp_attachment_is_image( $this->get_id() );
	}

	private function is_smushable_filter() {
		return apply_filters( 'wp_smush_is_smushable', true, $this->get_id(), $this->get_supported_mime_types() );
	}

	public function is_ignored() {
		if ( is_null( $this->ignored ) ) {
			$this->ignored = $this->prepare_ignored();
		}

		return $this->ignored;
	}

	private function prepare_ignored() {
		return (bool) $this->get_post_meta( self::$ignored_meta_key );
	}

	public function set_ignored( $ignored ) {
		$this->ignored = $ignored;

		$this->set_outdated( self::$ignored_meta_key );
	}

	/**
	 * @return void
	 */
	private function update_ignored_meta() {
		if ( ! $this->is_outdated( self::$ignored_meta_key ) ) {
			return;
		}

		if ( $this->is_ignored() ) {
			update_post_meta( $this->get_id(), self::$ignored_meta_key, true );
		} else {
			delete_post_meta( $this->get_id(), self::$ignored_meta_key );
		}
	}

	private function smush_image_filter() {
		return apply_filters( 'wp_smush_image', true, $this->get_id() );
	}

	/**
	 * Checking if a file is really animated is an expensive operation because we look at file frames, so here we check only a meta value and do the actual checking right before bulk smush.
	 *
	 * @return bool
	 * @see Animated_Status_Controller
	 */
	public function is_animated() {
		if ( ! $this->has_animated_mime_type() ) {
			return false;
		}

		if ( is_null( $this->animated ) ) {
			$this->animated = (bool) $this->get_post_meta( self::$animated_meta_key );
		}

		return $this->animated;
	}

	/**
	 * @param $animated
	 *
	 * @return bool
	 */
	public function set_animated( $animated ) {
		if ( ! $this->has_animated_mime_type() ) {
			return false;
		}

		$this->animated = (bool) $animated;
		$this->set_outdated( self::$animated_meta_key );

		return true;
	}

	/**
	 * @return void
	 */
	private function update_animated_meta() {
		if ( $this->is_outdated( self::$animated_meta_key ) ) {
			update_post_meta( $this->get_id(), self::$animated_meta_key, $this->is_animated() ? 1 : 0 );
		}
	}

	public function animated_meta_exists() {
		$animated_meta_value = $this->get_post_meta( self::$animated_meta_key );

		// Post meta default is empty string so a bool means there is a row in the meta table
		return is_numeric( $animated_meta_value );
	}

	/**
	 * Checking if a file is really transparent is an expensive operation because we look at file contents, so here we check only a meta value and do the actual checking elsewhere.
	 */
	public function is_transparent() {
		if ( ! $this->is_png() ) {
			return false;
		}

		if ( is_null( $this->transparent ) ) {
			$this->transparent = (bool) $this->get_post_meta( self::$transparent_meta_key );
		}

		return $this->transparent;
	}

	public function set_transparent( $transparent ) {
		if ( ! $this->is_png() ) {
			return false;
		}

		$this->transparent = (bool) $transparent;
		$this->set_outdated( self::$transparent_meta_key );

		return true;
	}

	/**
	 * @return void
	 */
	private function update_transparent_meta() {
		if ( ! $this->is_png() ) {
			// Maybe the mime type has changed, and we should delete the transparent meta value added when the mime type was PNG
			if ( $this->transparent_meta_exists() ) {
				delete_post_meta( $this->get_id(), self::$transparent_meta_key );
			}
		} else {
			if ( $this->is_outdated( self::$transparent_meta_key ) ) {
				// Unlike most other meta values we will not delete the meta because even a false value is useful: it tells us we have checked transparency before.
				update_post_meta( $this->get_id(), self::$transparent_meta_key, $this->is_transparent() ? 1 : 0 );
			}
		}
	}

	public function transparent_meta_exists() {
		$transparent_meta_value = $this->get_post_meta( self::$transparent_meta_key );

		// Post meta default is empty string so a bool means there is a row in the meta table
		return is_numeric( $transparent_meta_value );
	}

	public function is_valid() {
		return ! empty( $this->get_wp_metadata() ) && $this->has_attached_file();
	}

	private function has_attached_file() {
		return ! empty( $this->get_attached_file() );
	}

	/**
	 * @return bool
	 */
	public function has_animated_mime_type() {
		return in_array( $this->get_mime_type(), $this->animated_mime_types, true );
	}

	private function get_missing_file_name() {
		foreach ( $this->get_smushable_sizes() as $size ) {
			if ( ! $size->file_exists() ) {
				return $size->get_file_name();
			}
		}

		return '';
	}

	private function files_exist() {
		foreach ( $this->get_smushable_sizes() as $size ) {
			if ( ! $size->file_exists() ) {
				return false;
			}
		}

		return true;
	}

	public function set_settings( $settings ) {
		$this->plugin_settings = $settings;
	}

	private function get_post() {
		if ( is_null( $this->post ) ) {
			$this->post = get_post( $this->get_id() );
		}

		return $this->post;
	}

	public function get_mime_type() {
		if ( is_null( $this->mime_type ) ) {
			$this->mime_type = $this->fetch_post_mime_type();
		}

		return $this->mime_type;
	}

	private function fetch_post_mime_type() {
		$post = $this->get_post();
		return empty( $post )
			? ''
			: $post->post_mime_type;
	}

	public function set_mime_type( $mime_type ) {
		$this->mime_type = $mime_type;
	}

	/**
	 * @return void
	 */
	private function update_post_mime_type() {
		if ( $this->get_mime_type() !== $this->fetch_post_mime_type() ) {
			wp_update_post(
				array(
					'ID'             => $this->get_id(),
					'post_mime_type' => $this->get_mime_type(),
				)
			);
		}
	}

	public function save() {
		$this->update_ignored_meta();

		if ( $this->is_valid() ) {
			// We don't want to touch the rest of the stuff if the item is not valid.
			// For example if we don't have metadata to begin with then don't try to update it now.

			$this->update_animated_meta();

			$this->update_transparent_meta();

			$this->update_attached_file();

			$this->update_post_mime_type();

			$this->update_wp_metadata();

			$this->update_backup_sizes();
		}

		// Force everything to be reloaded from DB
		$this->reset();
	}

	public function prepare_errors() {
		$errors = new WP_Error();

		if ( ! $this->is_image() ) {
			$errors->add(
				'not_an_image',
				esc_html__( "Attachment is not an image so it can't be smushed.", 'wp-smushit' )
			);
		}

		if ( ! $this->is_mime_type_supported() ) {
			$errors->add(
				'unsupported_mime_type',
				/* translators: %s: Image mime type */
				sprintf( esc_html__( 'The mime type %s is not supported by Smush.', 'wp-smushit' ), $this->get_mime_type() )
			);
		}

		if ( ! $this->has_wp_metadata() ) {
			$errors->add( 'no_file_meta', esc_html__( 'No file data found in image meta', 'wp-smushit' ) );
		}

		if ( ! $this->has_attached_file() ) {
			$errors->add( 'no_attached_file', esc_html__( 'Missing attached file data in image metadata', 'wp-smushit' ) );
		}

		// Verify missing the full size due to the original image not found for wp.com since we only allowed the full size.
		// @see Photon_Controller::only_handle_full_size().
		if ( ! $this->get_scaled_or_full_size() ) {
			$original_file = $this->get_original_image_path();
			$errors->add(
				'file_not_found',
				/* translators: %s: The missing file name */
				sprintf( esc_html__( 'Skipped (%s), File not found.', 'wp-smushit' ), basename( $original_file ) )
			);
		} elseif ( ! $this->files_exist() ) {
			$errors->add(
				'file_not_found',
				/* translators: %s: The missing file name */
				sprintf( esc_html__( 'Skipped (%s), File not found.', 'wp-smushit' ), $this->get_missing_file_name() )
			);
		}

		if ( $this->size_limit_exceeded() ) {
			$errors->add(
				'size_limit',
				/* translators: 1: Exceeded size limit file name, 2: Image size limit */
				sprintf( esc_html__( 'Skipped (%1$s), file size limit of %2$s exceeded', 'wp-smushit' ), $this->get_file_name_exceeding_limit(), $this->get_human_size_limit() )
			);
		}

		if ( ! $this->smush_image_filter() || ! $this->is_smushable_filter() ) {
			$errors->add(
				'skipped_filter',
				/* translators: %s: Smush image filter */
				sprintf( esc_html__( 'Skipped with %s filter.', 'wp-smushit' ), ! $this->smush_image_filter() ? 'wp_smush_image' : 'wp_smush_is_smushable' )
			);
		}

		return $errors;
	}

	/**
	 * @return WP_Error
	 */
	public function get_errors() {
		if ( is_null( $this->errors ) ) {
			$this->errors = $this->prepare_errors();
		}

		return $this->errors;
	}

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

	/**
	 * @param $file
	 *
	 * @return bool
	 */
	private function file_path_has_scaled_postfix( $file ) {
		return false !== strpos( $file, '-scaled.' );
	}

	public function get_dir() {
		$upload_dir = wp_upload_dir();
		$basedir    = untrailingslashit( $upload_dir['basedir'] );
		$file_dir   = $this->get_relative_file_dir();

		return "$basedir/$file_dir";
	}

	public function get_base_url() {
		$upload_dir     = wp_upload_dir();
		$upload_dir_url = untrailingslashit( $upload_dir['baseurl'] );
		$file_dir       = $this->get_relative_file_dir();

		return "$upload_dir_url/$file_dir";
	}

	/**
	 * Transform the metadata for a size, so it can be used to initialize a size object
	 * @return array|false
	 */
	private function attachment_metadata_as_size_metadata( $file_path ) {
		$size_metadata = array(
			// Size data is expected to have just the file name instead of path.
			'file'      => $this->file_name_from_path( $file_path ),
			// Size data is expected to have 'mime-type'.
			'mime-type' => $this->get_mime_type(),
		);
		if ( $this->fs->file_exists( $file_path ) ) {
			// Some older WP versions don't have filesize in wp_metadata.
			$size_metadata['filesize'] = $this->fs->filesize( $file_path );
		}
		return array_merge(
			$this->get_wp_metadata(),
			$size_metadata
		);
	}

	private function initialize_size( $key, $metadata ) {
		$size = new Media_Item_Size(
			$key,
			$this->get_id(),
			$this->get_dir(),
			$this->get_base_url(),
			$metadata
		);

		return apply_filters( 'wp_smush_media_item_size', $size, $key, $metadata, $this );
	}

	/**
	 * @return array
	 */
	private function get_registered_wp_sizes() {
		if ( is_null( $this->registered_wp_sizes ) ) {
			$this->registered_wp_sizes = Helper::get_image_sizes();
		}

		return $this->registered_wp_sizes;
	}

	private function get_registered_wp_size( $size_key ) {
		return $this->array_utils->get_array_value( $this->get_registered_wp_sizes(), $size_key );
	}

	public function set_registered_wp_sizes( $registered_wp_sizes ) {
		$this->registered_wp_sizes = $registered_wp_sizes;
	}

	public function reset() {
		foreach ( $this->reset_properties as $property ) {
			$this->$property = null;
		}
	}

	/**
	 * @return false|string
	 */
	public function get_attached_file() {
		if ( is_null( $this->attached_file ) ) {
			$this->attached_file = get_attached_file( $this->get_id() );
		}

		return $this->attached_file;
	}

	/**
	 * @return void
	 */
	private function update_attached_file() {
		$main_size             = $this->get_main_size();
		$updated_attached_file = $main_size->get_file_path();
		if ( $updated_attached_file !== $this->get_attached_file() ) {
			update_attached_file( $this->get_id(), $updated_attached_file );
		}
	}

	private function make_attachment_meta() {
		$sizes = array();
		foreach ( $this->get_sizes() as $size_key => $size ) {
			if ( $size_key === self::$size_key_full || $size_key === self::$size_key_scaled ) {
				continue;
			}

			$sizes[ $size_key ] = array(
				'file'      => $size->get_file_name(),
				'width'     => $size->get_width(),
				'height'    => $size->get_height(),
				'mime-type' => $size->get_mime_type(),
				'filesize'  => $size->get_filesize(),
			);
		}

		$short_dir = $this->get_relative_file_dir();
		$main_size = $this->get_main_size();
		$new_meta  = array(
			'file'     => "$short_dir{$main_size->get_file_name()}",
			'width'    => $main_size->get_width(),
			'height'   => $main_size->get_height(),
			'filesize' => $main_size->get_filesize(),
			'sizes'    => $sizes,
		);
		if ( $this->separate_original_image_path_exists() && $this->has_full_size() ) {
			// If the original image exists then we must have used it when preparing the full size,
			// use it now to update the original_image value in the meta
			$new_meta['original_image'] = $this->get_full_size()->get_file_name();
		}

		return array_merge( $this->get_wp_metadata(), $new_meta );
	}

	private function arrays_same( $array1, $array2 ) {
		if (
			! is_array( $array1 )
			|| ! is_array( $array2 )
			|| count( $array1 ) !== count( $array2 )
		) {
			return false;
		}

		return $this->array_utils->array_hash( $array1 ) === $this->array_utils->array_hash( $array2 );
	}

	/**
	 * @return false|string
	 */
	public function get_original_image_path() {
		if ( is_null( $this->original_image_path ) ) {
			$this->original_image_path = wp_get_original_image_path( $this->get_id() );
		}

		return $this->original_image_path;
	}

	public function get_backup_sizes() {
		if ( is_null( $this->backup_sizes ) ) {
			$this->backup_sizes = $this->prepare_backup_sizes();
		}

		return $this->backup_sizes;
	}

	/**
	 * @param $backup_sizes Backup_Size[]
	 *
	 * @return void
	 */
	private function set_backup_sizes( $backup_sizes ) {
		$this->backup_sizes = $backup_sizes;

		$this->set_outdated( self::$backup_sizes_meta_key );
	}

	/**
	 * @return void
	 */
	private function update_backup_sizes() {
		if ( ! $this->is_outdated( self::$backup_sizes_meta_key ) ) {
			return;
		}

		$updated_backup_sizes_meta = $this->make_backup_sizes_meta();
		if ( ! $this->arrays_same( $this->get_backup_sizes_meta(), $updated_backup_sizes_meta ) ) {
			update_post_meta( $this->get_id(), self::$backup_sizes_meta_key, $updated_backup_sizes_meta );
		}
	}

	private function prepare_backup_sizes() {
		$backup_sizes      = array();
		$backup_sizes_meta = $this->get_backup_sizes_meta();

		foreach ( $backup_sizes_meta as $backup_size_key => $backup_size_meta ) {
			$backup_size = new Backup_Size( $this->get_dir() );
			$backup_size->from_array( $backup_size_meta );
			$backup_sizes[ $backup_size_key ] = $backup_size;
		}

		return $backup_sizes;
	}

	/**
	 * @return Backup_Size|null
	 */
	public function get_default_backup_size() {
		return $this->get_backup_size( self::$default_backup_key );
	}

	/**
	 * @param $file_name
	 * @param $width
	 * @param $height
	 * @param $key
	 *
	 * @return void
	 */
	public function add_backup_size( $file_name, $width, $height, $key = null ) {
		if ( is_null( $key ) ) {
			$key = self::$default_backup_key;
		}
		$backup_sizes         = $this->get_backup_sizes();
		$dir                  = $this->get_dir();
		$backup_size          = ( new Backup_Size( $dir ) )->set_file( $file_name )
		                                                   ->set_width( $width )
		                                                   ->set_height( $height );
		$backup_sizes[ $key ] = $backup_size;
		$this->set_backup_sizes( $backup_sizes );
	}

	public function make_backup_sizes_meta() {
		return array_map( function ( $backup_size ) {
			return $backup_size->to_array();
		}, $this->get_backup_sizes() );
	}

	/**
	 * @return array|mixed
	 */
	private function get_backup_sizes_meta() {
		$backup_sizes_meta = $this->get_post_meta( self::$backup_sizes_meta_key );

		return empty( $backup_sizes_meta ) ? array() : $backup_sizes_meta;
	}

	/**
	 * @param $key
	 *
	 * @return Backup_Size|null
	 */
	public function get_backup_size( $key ) {
		return $this->get_array_value( $this->get_backup_sizes(), $key );
	}

	public function remove_default_backup_size() {
		$this->remove_backup_size( self::$default_backup_key );
	}

	public function remove_backup_size( $key ) {
		$backup_sizes = $this->get_backup_sizes();
		if ( isset( $backup_sizes[ $key ] ) ) {
			unset( $backup_sizes[ $key ] );
		}
		$this->set_backup_sizes( $backup_sizes );

		$this->set_outdated( self::$backup_sizes_meta_key );
	}

	public function get_scaled_or_full_size() {
		return $this->has_scaled_size()
			? $this->get_scaled_size()
			: $this->get_full_size();
	}

	public function get_full_or_scaled_size() {
		return $this->has_full_size()
			? $this->get_full_size()
			: $this->get_scaled_size();
	}

	public function get_size_urls() {
		return array_map( function ( $size ) {
			return $size->get_file_url();
		}, $this->get_sizes() );
	}

	public function get_size_paths() {
		return array_map( function ( $size ) {
			return $size->get_file_path();
		}, $this->get_sizes() );
	}

	private function is_outdated( $key ) {
		$outdated_values = empty( $this->outdated_meta_values ) ? array() : $this->outdated_meta_values;

		return ! empty( $outdated_values[ $key ] );
	}

	private function set_outdated( $key ) {
		if ( empty( $this->outdated_meta_values ) ) {
			$this->outdated_meta_values = array();
		}

		$this->outdated_meta_values[ $key ] = true;
	}

	public function is_png() {
		$mime = $this->get_mime_type();

		return 'image/png' === $mime || 'image/x-png' === $mime;
	}

	public function can_be_restored() {
		// Note that we don't check if file exists because the file might be on a remote server e.g. s3
		return ! empty( $this->get_default_backup_size() );
	}

	public function is_large() {
		$file_size = $this->get_full_or_scaled_size()->get_filesize();
		$cut_off   = $this->plugin_settings->get_large_file_cutoff();

		return $file_size > $cut_off;
	}

	public function set_wp_metadata( $metadata ) {
		$this->metadata = $metadata;
	}

	public function set_attached_file( $attached_file ) {
		$this->attached_file = $attached_file;
	}

	public function set_original_image_path( $original_image_path ) {
		$this->original_image_path = $original_image_path;
	}
}