<?php
/*
* Tiny Compress Images - WordPress plugin.
* Copyright (C) 2015-2018 Tinify B.V.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

class Tiny_Image {
	const ORIGINAL = 0;

	private $settings;
	private $id;
	private $name;
	private $wp_metadata;
	private $sizes = array();
	private $statistics = array();

	public function __construct(
			$settings,
			$id,
			$wp_metadata = null,
			$tiny_metadata = null,
			$active_sizes = null,
			$active_tinify_sizes = null
	) {
		$this->settings = $settings;
		$this->id = $id;
		$this->wp_metadata = $wp_metadata;
		$this->parse_wp_metadata();
		$this->parse_tiny_metadata( $tiny_metadata );
		$this->detect_duplicates( $active_sizes, $active_tinify_sizes );
	}

	private function parse_wp_metadata() {
		if ( ! is_array( $this->wp_metadata ) ) {
			$this->wp_metadata = wp_get_attachment_metadata( $this->id );
		}
		if ( ! is_array( $this->wp_metadata ) ) {
			return;
		}
		if ( ! isset( $this->wp_metadata['file'] ) ) {
			/* No file metadata found, this might be another plugin messing with
			   metadata. Simply ignore this! */
			return;
		}

		$upload_dir = wp_upload_dir();
		$path_prefix = $upload_dir['basedir'] . '/';
		$path_info = pathinfo( $this->wp_metadata['file'] );
		if ( isset( $path_info['dirname'] ) ) {
			$path_prefix .= $path_info['dirname'] . '/';
		}

		/* Do not use pathinfo for getting the filename.
			 It doesn't work when the filename starts with a special character. */
		$path_parts = explode( '/', $this->wp_metadata['file'] );
		$this->name = end( $path_parts );
		$filename = $path_prefix . $this->name;
		$this->sizes[ self::ORIGINAL ] = new Tiny_Image_Size( $filename );

		if ( isset( $this->wp_metadata['sizes'] ) && is_array( $this->wp_metadata['sizes'] ) ) {
			foreach ( $this->wp_metadata['sizes'] as $size_name => $info ) {
				$this->sizes[ $size_name ] = new Tiny_Image_Size( $path_prefix . $info['file'] );
			}
		}
	}

	private function detect_duplicates( $active_sizes, $active_tinify_sizes ) {
		$filenames = array();

		if ( is_array( $this->wp_metadata )
			&& isset( $this->wp_metadata['file'] )
			&& isset( $this->wp_metadata['sizes'] )
			&& is_array( $this->wp_metadata['sizes'] ) ) {

			if ( null == $active_sizes ) {
				$active_sizes = $this->settings->get_sizes();
			}
			if ( null == $active_tinify_sizes ) {
				$active_tinify_sizes = $this->settings->get_active_tinify_sizes();
			}

			foreach ( $this->wp_metadata['sizes'] as $size_name => $size ) {
				if ( $this->sizes[ $size_name ]->has_been_compressed()
					&& array_key_exists( $size_name, $active_sizes ) ) {
					$filenames = $this->duplicate_check( $filenames, $size['file'], $size_name );
				}
			}
			foreach ( $this->wp_metadata['sizes'] as $size_name => $size ) {
				if ( in_array( $size_name, $active_tinify_sizes, true ) ) {
					$filenames = $this->duplicate_check( $filenames, $size['file'], $size_name );
				}
			}
			foreach ( $this->wp_metadata['sizes'] as $size_name => $size ) {
				if ( array_key_exists( $size_name, $active_sizes ) ) {
					$filenames = $this->duplicate_check( $filenames, $size['file'], $size_name );
				}
			}
			foreach ( $this->wp_metadata['sizes'] as $size_name => $size ) {
				$filenames = $this->duplicate_check( $filenames, $size['file'], $size_name );
			}
		}
	}

	private function duplicate_check( $filenames, $file, $size_name ) {
		if ( isset( $filenames[ $file ] ) ) {
			if ( $filenames[ $file ] != $size_name ) {
				$this->sizes[ $size_name ]->mark_duplicate( $filenames[ $file ] );
			}
		} else {
			$filenames[ $file ] = $size_name;
		}
		return $filenames;
	}

	private function parse_tiny_metadata( $tiny_metadata ) {
		if ( is_null( $tiny_metadata ) ) {
			$tiny_metadata = get_post_meta( $this->id, Tiny_Config::META_KEY, true );
		}
		if ( $tiny_metadata ) {
			foreach ( $tiny_metadata as $size => $meta ) {
				if ( ! isset( $this->sizes[ $size ] ) ) {
					if ( self::is_retina( $size ) && Tiny_Settings::wr2x_active() ) {
						$size_name = rtrim( $size, '_wr2x' );
						if ( 'original' === $size_name ) {
							$size_name = '0';
						}
						$retina_path = wr2x_get_retina(
							$this->sizes[ $size_name ]->filename
						);
						$this->sizes[ $size ] = new Tiny_Image_Size( $retina_path );
					} else {
						$this->sizes[ $size ] = new Tiny_Image_Size();
					}
				}
				$this->sizes[ $size ]->meta = $meta;
			}
		}
	}

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

	public function get_name() {
		return $this->name;
	}

	public function get_wp_metadata() {
		return $this->wp_metadata;
	}

	public function file_type_allowed() {
		return in_array( $this->get_mime_type(), array( 'image/jpeg', 'image/png', 'image/webp') );
	}

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

	public function download_missing_image_sizes() {
		global $as3cf;

		if ( ! $as3cf || ! $as3cf->is_plugin_setup() ) {
			error_log( 'offload s3 plugin not configured..' );
			return;
		}

		$s3_data = get_post_meta( $this->id, 'amazonS3_info', true );
		if ( ! $s3_data ) {
			return;
		}

		$path = dirname( $s3_data['key'] );

		foreach ( $this->sizes as $size_name => $size ) {
			$local_file_path = get_home_path() . $s3_data['key'];

			$s3_data['key'] = wp_normalize_path( $path . '/' . basename( $size->filename ) );
			$as3cf->plugin_compat->copy_s3_file_to_server( $s3_data, $size->filename );
		}
	}

	public function compress() {
		if ( $this->settings->get_compressor() === null || ! $this->file_type_allowed() ) {
			return;
		}

		/* Integration tests need to be written before this can be enabled. */
		// if ( $this->settings->has_offload_s3_installed() ) {
		// 	$this->download_missing_image_sizes();
		// }

		$success = 0;
		$failed = 0;

		$compressor = $this->settings->get_compressor();
		$active_tinify_sizes = $this->settings->get_active_tinify_sizes();
		$uncompressed_sizes = $this->filter_image_sizes( 'uncompressed', $active_tinify_sizes );

		foreach ( $uncompressed_sizes as $size_name => $size ) {
			if ( ! $size->is_duplicate() ) {
				$size->add_tiny_meta_start();
				$this->update_tiny_post_meta();
				$resize = $this->settings->get_resize_options( $size_name );
				$preserve = $this->settings->get_preserve_options( $size_name );
				try {
					$response = $compressor->compress_file( $size->filename, $resize, $preserve );
					$size->add_tiny_meta( $response );
					$success++;
				} catch ( Tiny_Exception $e ) {
					$size->add_tiny_meta_error( $e );
					$failed++;
				}
				$this->add_wp_metadata( $size_name, $size );
				$this->update_tiny_post_meta();
			}
		}

		/*
			Other plugins can hook into this action to execute custom logic
			after the image sizes have been compressed, ie. cache flushing.
		*/
		do_action( 'tiny_image_after_compression', $this->id, $success );

		return array(
			'success' => $success,
			'failed' => $failed,
		);
	}

	public function compress_retina( $size_name, $path ) {
		if ( $this->settings->get_compressor() === null || ! $this->file_type_allowed() ) {
			return;
		}

		if ( ! isset( $this->sizes[ $size_name ] ) ) {
			$this->sizes[ $size_name ] = new Tiny_Image_Size( $path );
		}
		$size = $this->sizes[ $size_name ];

		if ( ! $size->has_been_compressed() ) {
			$size->add_tiny_meta_start();
			$this->update_tiny_post_meta();
			$compressor = $this->settings->get_compressor();
			$preserve = $this->settings->get_preserve_options( $size_name );

			try {
				$response = $compressor->compress_file( $path, false, $preserve );
				$size->add_tiny_meta( $response );
			} catch ( Tiny_Exception $e ) {
				$size->add_tiny_meta_error( $e );
			}
			$this->update_tiny_post_meta();
		}
	}

	public function remove_retina_metadata() {
		// Remove metadata from all sizes, as this callback only fires when all
		// retina sizes are deleted.
		foreach ( $this->sizes as $size_name => $size ) {
			if ( self::is_retina( $size_name ) ) {
				unset( $this->sizes[ $size_name ] );
			}
		}
		$this->update_tiny_post_meta();
	}

	public function add_wp_metadata( $size_name, $size ) {
		if ( self::is_original( $size_name ) ) {
			if ( isset( $size->meta['output'] ) ) {
				$output = $size->meta['output'];
				if ( isset( $output['width'] ) && isset( $output['height'] ) ) {
					$this->wp_metadata['width'] = $output['width'];
					$this->wp_metadata['height'] = $output['height'];
					$this->wp_metadata['filesize'] = $output['size'];
				}
			}
		}
	}

	public function update_tiny_post_meta() {
		$tiny_metadata = array();
		foreach ( $this->sizes as $size_name => $size ) {
			$tiny_metadata[ $size_name ] = $size->meta;
		}
		update_post_meta( $this->id, Tiny_Config::META_KEY, $tiny_metadata );
		/*
			This action is being used by WPML:
			https://gist.github.com/srdjan-jcc/5c47685cda4da471dff5757ba3ce5ab1
		*/
		do_action( 'updated_tiny_postmeta', $this->id, Tiny_Config::META_KEY, $tiny_metadata );
	}

	public function get_image_sizes() {
		$original = isset( $this->sizes[ self::ORIGINAL ] )
			? array(
				self::ORIGINAL => $this->sizes[ self::ORIGINAL ],
			)
			: array();
		$compressed = array();
		$uncompressed = array();
		foreach ( $this->sizes as $size_name => $size ) {
			if ( self::is_original( $size_name ) ) {
				continue;
			}

			if ( $size->has_been_compressed() ) {
				$compressed[ $size_name ] = $size;
			} else {
				$uncompressed[ $size_name ] = $size;
			}
		}
		ksort( $compressed );
		ksort( $uncompressed );
		return $original + $compressed + $uncompressed;
	}

	public function get_image_size( $size = self::ORIGINAL, $create = false ) {
		if ( isset( $this->sizes[ $size ] ) ) {
			return $this->sizes[ $size ];
		} elseif ( $create ) {
			return new Tiny_Image_Size();
		} else {
			return null;
		}
	}

	public function filter_image_sizes( $method, $filter_sizes = null ) {
		$selection = array();
		if ( is_null( $filter_sizes ) ) {
			$filter_sizes = array_keys( $this->sizes );
		}
		foreach ( $filter_sizes as $size_name ) {
			if ( ! isset( $this->sizes[ $size_name ] ) ) {
				continue;
			}

			$tiny_image_size = $this->sizes[ $size_name ];
			if ( $tiny_image_size->$method() ) {
				$selection[ $size_name ] = $tiny_image_size;
			}
		}
		return $selection;
	}

	public function get_count( $methods, $count_sizes = null ) {
		$stats = array_fill_keys( $methods, 0 );
		if ( is_null( $count_sizes ) ) {
			$count_sizes = array_keys( $this->sizes );
		}
		foreach ( $count_sizes as $size ) {
			if ( ! isset( $this->sizes[ $size ] ) ) {
				continue;
			}

			foreach ( $methods as $method ) {
				if ( $this->sizes[ $size ]->$method() ) {
					$stats[ $method ]++;
				}
			}
		}
		return $stats;
	}

	public function get_latest_error() {
		$active_tinify_sizes = $this->settings->get_active_tinify_sizes();
		$error_message = null;
		$last_timestamp = null;
		foreach ( $this->sizes as $size_name => $size ) {
			if ( in_array( $size_name, $active_tinify_sizes, true ) ) {
				if ( isset( $size->meta['error'] ) && isset( $size->meta['message'] ) ) {
					if ( null === $last_timestamp || $last_timestamp < $size->meta['timestamp'] ) {
						$last_timestamp = $size->meta['timestamp'];
						$error_message = mb_strimwidth( $size->meta['message'], 0 , 140, '...' );
					}
				}
			}
		}
		return $error_message;
	}

	public function get_savings( $stats ) {
		$before = $stats['initial_total_size'];
		$after = $stats['optimized_total_size'];
		if ( 0 === $before ) {
			$savings = 0;
		} else {
			$savings = ($before - $after) / $before * 100;
		}
		return '' . number_format( $savings, 1 );
	}

	public function get_statistics( $active_sizes, $active_tinify_sizes ) {
		if ( $this->statistics ) {
			error_log( 'Strangely the image statistics are asked for again.' );
			return $this->statistics;
		}

		$this->statistics['initial_total_size'] = 0;
		$this->statistics['optimized_total_size'] = 0;
		$this->statistics['image_sizes_optimized'] = 0;
		$this->statistics['available_unoptimized_sizes'] = 0;

		foreach ( $this->sizes as $size_name => $size ) {
			if ( ! $size->is_duplicate() ) {
				if ( array_key_exists( $size_name, $active_sizes ) ) {
					if ( isset( $size->meta['input'] ) ) {
						$input = $size->meta['input'];
						$this->statistics['initial_total_size'] += intval( $input['size'] );
						if ( isset( $size->meta['output'] ) ) {
							$output = $size->meta['output'];
							if ( $size->modified() ) {
								$this->statistics['optimized_total_size'] += $size->filesize();
								if ( in_array( $size_name, $active_tinify_sizes, true ) ) {
									$this->statistics['available_unoptimized_sizes'] += 1;
								}
							} else {
								$this->statistics['optimized_total_size']
									+= intval( $output['size'] );
								$this->statistics['image_sizes_optimized'] += 1;
							}
						} else {
							$this->statistics['optimized_total_size'] += intval( $input['size'] );
						}
					} elseif ( $size->exists() ) {
						$this->statistics['initial_total_size'] += $size->filesize();
						$this->statistics['optimized_total_size'] += $size->filesize();
						if ( in_array( $size_name, $active_tinify_sizes, true ) ) {
							$this->statistics['available_unoptimized_sizes'] += 1;
						}
					}
				}
			}
		}// End foreach().

		/*
			When an image hasn't yet been optimized but only exists on S3, we still need to
			know the total size of the image sizes for the bulk optimization tool.
			TODO: First write integration tests before enabling this again.

		if (
			0 === $this->statistics['initial_total_size'] &&
			0 === $this->statistics['optimized_total_size'] &&
			$this->settings->has_offload_s3_installed()
		) {
			$s3_data = get_post_meta( $this->id, 'wpos3_filesize_total', true );
			if ( $s3_data ) {
				$this->statistics['initial_total_size'] = $s3_data;
				$this->statistics['optimized_total_size'] = $s3_data;
			}
		}
		*/

		return $this->statistics;
	}

	public static function is_original( $size ) {
		return self::ORIGINAL === $size;
	}

	public static function is_retina( $size ) {
		return strrpos( $size, 'wr2x' ) === strlen( $size ) - strlen( 'wr2x' );
	}
}
