<?php

namespace Smush\Core\Lazy_Load;

use Smush\Core\Array_Utils;
use Smush\Core\Keyword_Exclusions;
use Smush\Core\LCP\LCP_Transform;
use Smush\Core\Parser\Composite_Element;
use Smush\Core\Parser\Element;
use Smush\Core\Parser\Element_Attribute;
use Smush\Core\Parser\Page;
use Smush\Core\Settings;
use Smush\Core\Transform\Transform;
use Smush\Core\Upload_Dir;
use Smush\Core\Url_Utils;

class Lazy_Load_Transform implements Transform {
	const LAZYLOAD_CLASS = 'lazyload';
	const TEMP_SRC = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==';

	/**
	 * @var Settings
	 */
	private $settings;
	/**
	 * @var array
	 */
	private $lazy_load_options;
	/**
	 * @var array
	 */
	private $excluded_keywords;

	/**
	 * Keyword Exclusions.
	 *
	 * @var Keyword_Exclusions
	 */
	private $keyword_exclusions;

	/**
	 * @var Lazy_Load_Helper
	 */
	private $helper;
	/**
	 * @var Array_Utils
	 */
	private $array_utils;
	private $upload_dir;
	/**
	 * @var Url_Utils
	 */
	private $url_utils;

	public function __construct() {
		$this->settings    = Settings::get_instance();
		$this->helper      = Lazy_Load_Helper::get_instance();
		$this->array_utils = new Array_Utils();
		$this->upload_dir  = new Upload_Dir();
		$this->url_utils   = new Url_Utils();
	}

	public function should_transform() {
		return ! $this->helper->should_skip_lazyload();
	}

	public function transform_page( $page ) {
		$this->transform_image_elements( $page );
		if ( ! $this->helper->is_format_excluded( 'iframe' ) ) {
			$this->transform_iframes( $page );
		}
	}

	/**
	 * @param $page Page
	 *
	 * @return void
	 */
	private function transform_iframes( $page ) {
		foreach ( $page->get_iframe_elements() as $iframe_element ) {
			$this->transform_iframe( $iframe_element );
		}
	}

	/**
	 * @param Element $iframe_element
	 *
	 * @return void
	 */
	private function transform_iframe( Element $iframe_element ) {
		$src_attribute = $iframe_element->get_attribute( 'src' );
		if ( ! $src_attribute ) {
			return;
		}

		$original_src_url       = $src_attribute->get_value();
		$original_iframe_markup = $iframe_element->get_markup();
		if (
			$this->is_element_excluded( $iframe_element )
			|| $this->is_iframe_skipped_through_filter( $original_src_url, $original_iframe_markup )
		) {
			return;
		}

		if ( esc_url_raw( $original_src_url ) !== $original_src_url ) {
			return;
		}

		if ( $this->helper->should_lazy_load_embed_video() ) {
			$lazy_load_video = new Lazy_Load_Video_Embed( $original_src_url, $iframe_element );
			if ( $lazy_load_video->can_lazy_load() ) {
				$lazy_load_video->transform();
				if ( $this->helper->is_noscript_fallback_enabled() ) {
					$iframe_element->set_postfix( "<noscript>$original_iframe_markup</noscript>" );
				}

				return;
			}
		}

		if ( $this->helper->is_native_lazy_loading_enabled() ) {
			if ( ! $this->element_has_native_lazy_load_attribute( $iframe_element ) ) {
				$this->add_native_lazy_loading_attribute( $iframe_element );
			}

			return;
		}

		$this->update_iframe_element_attributes_for_lazy_load( $iframe_element );
	}

	private function update_iframe_element_attributes_for_lazy_load( Element $iframe_element ) {
		$this->remove_native_lazy_loading_attribute( $iframe_element );
		$this->update_element_attributes_for_lazy_load( $iframe_element, array( 'src' ) );
		$iframe_element->add_attribute( new Element_Attribute( 'data-load-mode', '1' ) );
	}

	private function update_element_attributes_for_lazy_load( Element $element, $replace_attributes ) {
		$this->replace_attributes_with_data_attributes( $element, $replace_attributes );
		// We are adding a new src below, the original src is gone because we replaced it.
		$element->add_attribute( new Element_Attribute( 'src', self::TEMP_SRC ) );
		$this->add_lazy_load_class( $element );
	}

	private function element_has_native_lazy_load_attribute( Element $element ) {
		return $element->has_attribute( 'loading' );
	}

	private function is_element_excluded( Element $element ) {
		return $this->is_high_priority_element( $element )
		       || $element->is_lcp()
		       || $this->element_has_excluded_keywords( $element );
	}

	private function element_has_excluded_keywords( Element $element ) {
		$keyword_exclusions = $this->keyword_exclusions();
		if ( ! $keyword_exclusions->has_excluded_keywords() ) {
			return false;
		}

		return $keyword_exclusions->is_markup_excluded( $element->get_markup() )
		       || $keyword_exclusions->is_id_attribute_excluded( $element->get_attribute_value( 'id' ) )
		       || $keyword_exclusions->is_class_attribute_excluded( $element->get_attribute_value( 'class' ) );
	}

	private function is_iframe_skipped_through_filter( $src, $iframe ) {
		return apply_filters( 'smush_skip_iframe_from_lazy_load', false, $src, $iframe );
	}

	private function get_lazy_load_options() {
		if ( ! $this->lazy_load_options ) {
			$setting                 = $this->settings->get_setting( 'wp-smush-lazy_load' );
			$this->lazy_load_options = empty( $setting ) ? array() : $setting;
		}

		return $this->lazy_load_options;
	}

	private function get_excluded_keywords() {
		if ( ! $this->excluded_keywords ) {
			$this->excluded_keywords = $this->prepare_excluded_keywords();
		}
		return $this->excluded_keywords;
	}

	private function prepare_excluded_keywords() {
		$default_exclude_keywords = $this->get_default_excluded_keywords();
		$exclude_keywords         = $this->helper->get_excluded_classes();// @since 3.13.0 used excluded ids, classes as excluded keywords.
		$exclude_keywords         = array_merge(
			$default_exclude_keywords,
			$exclude_keywords
		);

		return apply_filters( 'wp_smush_lazyload_excluded_keywords', array_unique( $exclude_keywords ) );
	}

	private function replace_attributes_with_data_attributes( Element $element, $attribute_names ) {
		foreach ( $attribute_names as $attribute_name ) {
			$this->replace_attribute_with_data_attribute( $element, $attribute_name );
		}
	}

	/**
	 * @param Element $element
	 * @param $original_attribute_name
	 *
	 * @return void
	 */
	private function replace_attribute_with_data_attribute( Element $element, $original_attribute_name ) {
		$attribute = $element->get_attribute( $original_attribute_name );
		if ( $attribute ) {
			$original_value = $attribute->get_value();
			$data_attribute = new Element_Attribute( "data-$original_attribute_name", $original_value );
			$element->replace_attribute( $original_attribute_name, $data_attribute );
		}
	}

	private function get_default_excluded_keywords() {
		return array(
			'data-lazyload=',
			'soliloquy-preload', // Soliloquy slider.
			'no-lazyload', // Internal class to skip images.
			'data-src=',
			'data-no-lazy=',
			'base64,R0lGOD',
			'data-lazy-original=',
			'data-lazy-src=',
			'data-lazysrc=',
			'data-bgposition=',
			'fullurl=',
			'jetpack-lazy-image',
			'lazy-slider-img=',
			'data-srcset=',
			'class="ls-l',
			'class="ls-bg',
			'soliloquy-image',
			'swatch-img',
			'data-height-percentage',
			'data-large_image',
			'avia-bg-style-fixed',
			'data-skip-lazy',
			'skip-lazy',
			'image-compare__',
			'gform_ajax_frame',
			'recaptcha/api/',
			'google_ads_iframe_',
		);
	}

	private function is_high_priority_element( Element $element ) {
		/**
		 * An image should not be lazy-loaded and marked as high priority at the same time.
		 *
		 * @see wp_img_tag_add_loading_optimization_attrs()
		 */
		$fetch_priority = $element->get_attribute_value( 'fetchpriority' );

		return $fetch_priority === 'high';
	}

	/**
	 * @param Element $element
	 *
	 * @return void
	 */
	private function add_native_lazy_loading_attribute( Element $element ) {
		$element->add_attribute( new Element_Attribute( 'loading', 'lazy' ) );
	}

	private function remove_native_lazy_loading_attribute( Element $element ) {
		$native_lazyload_attr = $element->get_attribute( 'loading' );
		if ( ! empty( $native_lazyload_attr ) ) {
			$element->remove_attribute( $native_lazyload_attr );
		}
	}

	private function transform_image_elements( Page $page ) {
		/**
		 * The following is being done in addition to the separate LCP_Transform just to save an extra re-parse in the transformer {@see Transformer::transform_content()}.
		 * TODO: Remove this when re-parsing after every transform is not necessary.
		 */
		if ( $this->settings->is_lcp_preload_enabled() ) {
			$lcp_transform = new LCP_Transform();
			$lcp_transform->transform_page( $page );
		}

		foreach ( $page->get_composite_elements() as $composite_element ) {
			if ( ! $this->is_composite_element_excluded( $composite_element ) ) {
				$this->transform_elements( $composite_element->get_elements() );
			}
		}

		$this->transform_elements( $page->get_elements() );
	}

	private function transform_image_element( Element $element ) {
		if ( $element->get_tag() === 'source' ) {
			$this->maybe_lazy_load_source_element( $element );
		} else {
			$attributes_updated = $this->maybe_lazy_load_image_element( $element );
			if ( ! $attributes_updated ) {
				$this->maybe_lazy_load_background( $element );
			}
		}
	}

	private function maybe_lazy_load_source_element( Element $element ) {
		$srcset_attribute = $element->get_attribute( 'srcset' );
		if ( ! $srcset_attribute || empty( $srcset_attribute->get_image_urls() ) ) {
			return false;
		}

		$srcset_url       = $srcset_attribute->get_single_image_url();
		$srcset_image_url = $srcset_url->get_absolute_url();
		$srcset_extension = $srcset_url->get_ext();
		$original_markup  = $element->get_markup();
		if (
			! $srcset_image_url
			|| ! $this->helper->is_image_extension_supported( $srcset_extension, $srcset_image_url )
			|| $this->is_element_excluded( $element )
			|| $this->is_image_element_skipped_through_filter( $srcset_image_url, $original_markup )
			|| $this->helper->is_native_lazy_loading_enabled()
		) {
			return false;
		}

		$this->remove_native_lazy_loading_attribute( $element );
		$this->replace_attributes_with_data_attributes( $element, array(
			'src',
			'srcset',
			'sizes',
		) );
		return true;
	}

	private function maybe_lazy_load_image_element( Element $element ) {
		$src_attribute = $element->get_attribute( 'src' );
		if ( ! $src_attribute ) {
			return false;
		}
		$src_image_url = ! empty( $src_attribute->get_single_image_url() )
			? $src_attribute->get_single_image_url()->get_absolute_url()
			: $src_attribute->get_value();
		$src_extension = ! empty( $src_attribute->get_single_image_url() )
			? $src_attribute->get_single_image_url()->get_ext()
			: '';

		$original_markup = $element->get_markup();
		if (
			! $src_image_url
			|| ! $this->helper->is_image_extension_supported( $src_extension, $src_image_url )
			|| $this->is_element_excluded( $element )
			|| $this->is_image_element_skipped_through_filter( $src_image_url, $original_markup )
		) {
			return false;
		}

		$is_tag_supported = in_array( $element->get_tag(), $this->get_lazy_load_image_tag_names(), true );
		if ( ! $is_tag_supported ) {
			return false;
		}

		if ( $this->helper->is_native_lazy_loading_enabled() ) {
			if ( ! $this->element_has_native_lazy_load_attribute( $element ) ) {
				$this->add_native_lazy_loading_attribute( $element );
			}
		} else {
			$this->remove_native_lazy_loading_attribute( $element );
			$this->update_element_attributes_for_lazy_load( $element, array(
				'src',
				'srcset',
				'sizes',
			) );

			$this->set_placeholder_width_and_height_in_style_attribute( $element, $src_image_url );

			if ( $element->is_image_element() && $this->helper->is_noscript_fallback_enabled() ) {
				// TODO: Remove the duplicate <noscript> if it already exists before.
				$element->set_postfix( "<noscript>$original_markup</noscript>" );
			}
		}
		return true;
	}

	private function maybe_lazy_load_background( Element $element ) {
		$background_property  = $element->get_background_css_property();
		$background_image_url = $background_property && ! empty( $background_property->get_single_image_url()->get_absolute_url() )
			? $background_property->get_single_image_url()->get_absolute_url()
			: '';
		$background_extension = $background_property && ! empty( $background_property->get_single_image_url()->get_ext() )
			? $background_property->get_single_image_url()->get_ext()
			: '';

		$original_markup = $element->get_markup();
		if (
			! $background_image_url
			|| ! $this->helper->is_image_extension_supported( $background_extension, $background_image_url )
			|| $this->is_element_excluded( $element )
			|| $this->is_image_element_skipped_through_filter( $background_image_url, $original_markup )
			|| $this->helper->is_native_lazy_loading_enabled()
		) {
			return;
		}

		$data_attribute_name = 'data-' . str_replace( 'background', 'bg', $background_property->get_property() );  // data-bg|data-bg-image.
		$element->add_attribute( new Element_Attribute( $data_attribute_name, trim( $background_property->get_value() ) ) );
		$background_property->set_value( 'inherit' );
		$this->add_lazy_load_class( $element );
	}

	private function get_lazy_load_image_tag_names() {
		$image_tag_names = array(
			'img',
		);
		if ( ! $this->helper->is_native_lazy_loading_enabled() ) {
			$image_tag_names[] = 'source';
		}
		return (array) apply_filters( 'wp_smush_lazyload_image_tag_names', $image_tag_names );
	}

	private function is_image_element_skipped_through_filter( $src_url, $markup ) {
		/**
		 * Filter to skip a single image from lazy load.
		 *
		 * @param bool $skip Should skip? Default: false.
		 * @param string $src_url Image url.
		 * @param string $image Image.
		 *
		 * @since 3.3.0 Added $image param.
		 *
		 */
		return apply_filters( 'smush_skip_image_from_lazy_load', false, $src_url, $markup );
	}

	public function transform_image_url( $url ) {
		return $url;
	}

	/**
	 * @param Element $element
	 *
	 * @return void
	 */
	private function add_lazy_load_class( Element $element ) {
		$class_attr = $element->get_attribute_value( 'class' );
		if ( ! empty( $class_attr ) && strpos( $class_attr, self::LAZYLOAD_CLASS ) !== false ) {
			return;
		}

		$new_class_attr = empty( $class_attr )
			? self::LAZYLOAD_CLASS
			: $class_attr . ' ' . self::LAZYLOAD_CLASS;

		$new_class_attr = apply_filters( 'wp_smush_lazy_load_classes', $new_class_attr );

		$element->add_or_update_attribute( new Element_Attribute( 'class', $new_class_attr ) );
	}

	/**
	 * @param Element $element
	 * @param $src_image_url
	 *
	 * @return void
	 */
	private function set_placeholder_width_and_height_in_style_attribute( Element $element, $src_image_url ) {
		// We need explicit values for width and height. First try attribute values.
		$width  = (int) $element->get_attribute_value( 'width' );
		$height = (int) $element->get_attribute_value( 'height' );

		// If attributes are missing, check if the image file name has dimensions in it
		if ( empty( $width ) || empty( $height ) ) {
			list( $width, $height ) = $this->url_utils->guess_dimensions_from_image_url( $src_image_url );
		}

		// If all else fails, use getimagesize for local images
		if ( empty( $width ) || empty( $height ) ) {
			$image_dimensions = $this->get_image_dimensions( $src_image_url );
			if ( ! empty( $image_dimensions ) ) {
				list( $width, $height ) = $image_dimensions;
			}
		}

		if ( $width && $height ) {
			$original_style = $element->get_attribute_value( 'style' );
			$new_style      = "--smush-placeholder-width: {$width}px; --smush-placeholder-aspect-ratio: $width/$height;$original_style";

			$element->add_or_update_attribute( new Element_Attribute( 'style', $new_style ) );
		}
	}

	private function get_image_dimensions( $image_url ) {
		$upload_url = $this->upload_dir->get_upload_url();
		if ( ! str_starts_with( $image_url, $upload_url ) ) {
			return array();
		}

		$upload_path = $this->upload_dir->get_upload_path();
		$image_path  = str_replace( $upload_url, $upload_path, $image_url );
		if ( ! file_exists( $image_path ) ) {
			return array();
		}

		return getimagesize( $image_path );
	}

	/**
	 * @param Composite_Element $composite_element
	 *
	 * @return bool
	 */
	private function is_composite_element_excluded( Composite_Element $composite_element ): bool {
		foreach ( $composite_element->get_elements() as $sub_element ) {
			if ( $this->is_element_excluded( $sub_element ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @param array $elements
	 *
	 * @return void
	 */
	private function transform_elements( array $elements ) {
		foreach ( $elements as $element ) {
			$this->transform_image_element( $element );
		}
	}

	/**
	 * Get Keyword Exclusions.
	 *
	 * @return Keyword_Exclusions
	 */
	private function keyword_exclusions() {
		if ( ! $this->keyword_exclusions ) {
			$this->keyword_exclusions = new Keyword_Exclusions( $this->get_excluded_keywords() );
		}

		return $this->keyword_exclusions;
	}
}
