<?php

/**
 * This file contains QUI\Cache\Parser\LazyLoading
 */

namespace QUI\Cache\Parser;

use QUI;
use QUI\Utils\StringHelper as StringUtils;

use function htmlspecialchars;
use function preg_match;
use function preg_match_all;
use function preg_quote;
use function preg_replace_callback;
use function stripos;
use function strpos;
use function substr;

/**
 * Class LazyLoading
 *
 * @package QUI\Cache
 */
class LazyLoading extends QUI\Utils\Singleton
{
    protected ?string $autoPriorityImage = null;

    protected bool $autoPriorityImageApplied = false;

    /**
     * Add lazy loading part to HTML
     *
     * @param string $content
     * @return string
     */
    public function parse(string $content): string
    {
        $this->autoPriorityImage = $this->findAutoPriorityImage($content);
        $this->autoPriorityImageApplied = false;

        return (string)preg_replace_callback(
            '#<img([^>]*)>#i',
            [&$this, 'images'],
            $content
        );
    }

    /**
     * @param array<string> $output
     * @return string
     */
    public function images(array $output): string
    {
        $img = $output[0];
        //$imgData = $output[1];
        $attributes = StringUtils::getHTMLAttributes($img);

        if (!isset($attributes['src'])) {
            return $img;
        }

        if ($this->isLazyLoadingDisabled($attributes)) {
            return $img;
        }

        if ($this->isPriorityImage($attributes) || $this->isAutoPriorityImage($img, $attributes)) {
            $attributes['loading'] = 'eager';
            $attributes['fetchpriority'] = 'high';

            return $this->render($attributes);
        }

        if (isset($attributes['loading']) && $attributes['loading'] === 'lazy' && isset($attributes['data-src'])) {
            return $img;
        }

        if (
            str_contains($attributes['src'], '.svg')
            || str_contains($attributes['src'], 'data:')
        ) {
            return $img;
        }

        if (empty($attributes['loading'])) {
            $attributes['loading'] = 'lazy';

            if (!isset($attributes['class'])) {
                $attributes['class'] = 'lazyload';
            } elseif (!str_contains($attributes['class'], 'lazyload')) {
                $attributes['class'] = $attributes['class'] . ' lazyload';
            }
        }

        return $this->render($attributes);
    }

    /**
     * @param array<string, string> $attributes
     */
    protected function isAutoPriorityImage(string $img, array $attributes): bool
    {
        if ($this->autoPriorityImageApplied || $this->autoPriorityImage === null) {
            return false;
        }

        if ($img !== $this->autoPriorityImage) {
            return false;
        }

        if (!$this->isEligibleAutoPriorityImage($attributes)) {
            return false;
        }

        $this->autoPriorityImageApplied = true;

        return true;
    }

    protected function findAutoPriorityImage(string $content): ?string
    {
        preg_match_all('#<img\b[^>]*>#i', $content, $matches, PREG_SET_ORDER);

        $fallback = null;

        foreach ($matches as $match) {
            $img = $match[0];
            $attributes = StringUtils::getHTMLAttributes($img);

            if (!$this->isEligibleAutoPriorityImage($attributes)) {
                continue;
            }

            if ($this->isLikelyAboveTheFoldImage($content, $img)) {
                return $img;
            }

            if ($fallback === null) {
                $fallback = $img;
            }
        }

        return $fallback;
    }

    protected function isLikelyAboveTheFoldImage(string $content, string $img): bool
    {
        $position = strpos($content, $img);

        if ($position === false) {
            return false;
        }

        $before = substr($content, 0, $position);
        $window = substr($before, -2500);
        $structuralNeedles = [
            '<main',
            '<header'
        ];

        foreach ($structuralNeedles as $needle) {
            if (stripos($window, $needle) !== false) {
                return true;
            }
        }

        $attributeNeedles = [
            'hero',
            'page-header',
            'pageHeader',
            'wallpaper',
            'banner',
            'stage',
            'above-the-fold',
            'lcp'
        ];

        foreach ($attributeNeedles as $needle) {
            $quotedNeedle = preg_quote($needle, '#');

            if (preg_match('#(?:class|id)=["\'][^"\']*' . $quotedNeedle . '#i', $window)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param array<string, string> $attributes
     */
    protected function isEligibleAutoPriorityImage(array $attributes): bool
    {
        if (!isset($attributes['src'])) {
            return false;
        }

        if ($this->isLazyLoadingDisabled($attributes) || $this->isPriorityImage($attributes)) {
            return false;
        }

        if (isset($attributes['loading']) && $attributes['loading'] === 'lazy' && isset($attributes['data-src'])) {
            return false;
        }

        return !str_contains($attributes['src'], '.svg') && !str_contains($attributes['src'], 'data:');
    }

    /**
     * @param array<string, string> $attributes
     */
    protected function isLazyLoadingDisabled(array $attributes): bool
    {
        if (isset($attributes['data-no-cache']) && $attributes['data-no-cache'] === '1') {
            return true;
        }

        if (isset($attributes['data-cache-lazy']) && $attributes['data-cache-lazy'] === '0') {
            return true;
        }

        if (isset($attributes['loading']) && $attributes['loading'] === 'eager') {
            return true;
        }

        return isset($attributes['fetchpriority']) && $attributes['fetchpriority'] === 'high';
    }

    /**
     * @param array<string, string> $attributes
     */
    protected function isPriorityImage(array $attributes): bool
    {
        if (isset($attributes['data-cache-preload']) && $attributes['data-cache-preload'] === '1') {
            return true;
        }

        return isset($attributes['data-cache-priority']) && $attributes['data-cache-priority'] === 'high';
    }

    /**
     * @param array<string, string> $attributes
     * @return string
     */
    protected function render(array $attributes): string
    {
        // image string
        $img = '<img ';

        foreach ($attributes as $key => $value) {
            $img .= htmlspecialchars($key) . '="' . htmlspecialchars($value) . '" ';
        }

        $img .= '/>';

        return $img;
    }
}
