<?php

/**
 * This file contains the \QUI\Cache\EventCoordinator
 */

namespace QUI\Cache;

use DOMDocument;
use DOMElement;
use Intervention\Image\Image;
use QUI;
use QUI\Users\User;
use QUI\Utils\System\File;
use Symfony\Component\HttpFoundation\Response;

use function count;
use function define;
use function defined;
use function explode;
use function file_exists;
use function filemtime;
use function glob;
use function headers_sent;
use function header_remove;
use function is_array;
use function is_dir;
use function is_file;
use function ltrim;
use function pathinfo;
use function preg_replace;
use function str_replace;
use function strtolower;
use function usort;

use const PATHINFO_EXTENSION;
use const PATHINFO_FILENAME;

/**
 * Class Events
 * Event handling for the cache
 *
 * @package QUI\Cache
 */
class EventCoordinator
{
    /**
     * @param string $url
     */
    public static function onRequestImageNotFound(string $url): void
    {
        $ext = pathinfo($url, PATHINFO_EXTENSION);

        if ($ext !== 'webp') {
            return;
        }

        $project = explode('/', $url)[2];

        if (!QUI::getProjectManager()::existsProject($project)) {
            return;
        }

        $file = CMS_DIR . $url;
        $parts = pathinfo($file);

        $filenameParts = explode('__', $parts['filename']);
        $filename = $filenameParts[0];
        $hasRequestedSize = str_contains($parts['filename'], '__');

        $filenameDir = str_replace(
            CMS_DIR . 'media/cache/' . $project,
            '',
            $parts['dirname']
        );

        if (!empty($filenameDir)) {
            $filenameDir = $filenameDir . DIRECTORY_SEPARATOR;
        }

        $filenameDir = ltrim($filenameDir, DIRECTORY_SEPARATOR);

        // wanted sizes
        $height = false;
        $width = false;

        if ($hasRequestedSize && isset($filenameParts[1])) {
            $sizeParts = explode('x', $filenameParts[1]);
            $width = $sizeParts[0];

            if (isset($sizeParts[1])) {
                $height = $sizeParts[1];
            }
        }

        // look after the original image
        try {
            $QueryBuilder = QUI::getQueryBuilder();
            $result = $QueryBuilder
                ->select('id', 'file')
                ->from(
                    QUI\Utils\Doctrine::quoteIdentifier(
                        QUI::getDBTableName($project . '_media')
                    )
                )
                ->where(
                    $QueryBuilder->expr()->like(
                        QUI\Utils\Doctrine::quoteIdentifier('file'),
                        ':file'
                    )
                )
                ->setParameter('file', $filenameDir . $filename . '.%')
                ->setMaxResults(1)
                ->executeQuery()
                ->fetchAssociative();
        } catch (\Exception $Exception) {
            QUI\System\Log::addDebug($Exception->getMessage());
            return;
        }

        if (!$result) {
            return;
        }

        $originalFile = CMS_DIR . 'media/sites/' . $project . '/' . $result['file'];
        $originalCache = CMS_DIR . 'media/cache/' . $project . '/' . $result['file'];

        $originalExtension = pathinfo($originalFile, PATHINFO_EXTENSION);

        if (empty($originalExtension)) {
            return;
        }

        if (!file_exists($originalFile)) {
            return;
        }

        // check if cache image with filesize exists
        $cacheFile = str_replace('.webp', '.' . $originalExtension, $file);

        if (file_exists($cacheFile)) {
            $webPFile = Optimizer::convertToWebP($cacheFile);
            self::outputWebP((string)$webPFile);

            return;
        }

        // if original cache doesn't exist, and we need no sizes
        if (!$hasRequestedSize && file_exists($originalCache)) {
            $webPFile = Optimizer::convertToWebP($originalCache);
            self::outputWebP((string)$webPFile);

            return;
        }

        // if original cache doesn't exist, create it
        try {
            $Project = QUI::getProject($project);
            $Media = $Project->getMedia();
            $Image = $Media->get($result['id']);

            if (!$hasRequestedSize) {
                $sizeCacheFile = $Image->createCache();
            } elseif (method_exists($Image, 'createResizeCache')) {
                $sizeCacheFile = $Image->createResizeCache($width, $height);
            } else {
                return;
            }

            $webPFile = Optimizer::convertToWebP($sizeCacheFile);
            self::outputWebP((string)$webPFile);
            return;
        } catch (QUI\Exception $Exception) {
            QUI\System\Log::addDebug($Exception->getMessage());
        }
    }

    /**
     * @param string $webPFile
     */
    public static function outputWebP(string $webPFile): void
    {
        if (file_exists($webPFile)) {
            try {
                QUI\Utils\System\File::fileHeader($webPFile);
            } catch (QUI\Exception $Exception) {
                QUI\System\Log::addDebug($Exception->getMessage());
            }
        }
    }

    /**
     * event : on request
     *
     * @param QUI\Rewrite $Rewrite
     * @param string $url
     */
    public static function onRequest(QUI\Rewrite $Rewrite, string $url): void
    {
        if (!defined('NO_INTERNAL_CACHE')) {
            define('NO_INTERNAL_CACHE', true); // use only website cache, not the quiqqer internal cache
        }

        $config = null;

        try {
            $config = QUI::getPackage('quiqqer/cache')->getConfig();
            $cacheEnabled = (bool)$config?->get('settings', 'cache');
        } catch (QUI\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
            $cacheEnabled = false;
        }

        try {
            $ignoreWebpCheck = (bool)$config?->get('settings', 'ignoreWebPCheck');

            if (
                isset($_SERVER['HTTP_ACCEPT'])
                && $config?->get('settings', 'webp')
            ) {
                // if webp supported, use it
                if (
                    str_contains($_SERVER['HTTP_ACCEPT'], 'image/webp')
                    || isset($_SERVER['HTTP_USER_AGENT']) && str_contains($_SERVER['HTTP_USER_AGENT'], ' Chrome/')
                    || $ignoreWebpCheck
                ) {
                    // webp is supported!
                } else {
                    // webp is not supported!
                    $cacheEnabled = false;

                    // no cache generating for this version
                    QUI::getRewrite()->getSite()?->setAttribute('nocache', true);

                    if (!defined('QUIQQER_CACHE_DISABLE_WEBP')) {
                        define('QUIQQER_CACHE_DISABLE_WEBP', true);
                    }
                }
            }
        } catch (QUI\Exception) {
        }

        if (!$cacheEnabled) {
            return;
        }

        $getParams = $_GET;
        $postParams = $_POST;

        if (isset($getParams['_url'])) {
            unset($getParams['_url']);
        }

        // query strings have no cache
        if (!empty($getParams) || !empty($postParams)) {
            return;
        }

        // logged-in users get no cache
        if (QUI::getUsers()->isAuth(QUI::getUserBySession())) {
            return;
        }

        try {
            $content = QUI\Cache\Handler::init()->getCacheFromRequest();
            $Response = QUI::getGlobalResponse();
            $Response->setContent($content);

            //QUI\Cache\Parser\HTTP2ServerPush::parseCSS($content, $Response);
            //QUI\Cache\Parser\HTTP2ServerPush::parseImages($content, $Response);

            self::normalizeHtmlCacheHitHeaders($Response);

            $Response->send();
            exit;
        } catch (QUI\Exception $Exception) {
            QUI\System\Log::addDebug($Exception->getMessage(), [
                'trace' => $Exception->getTraceAsString()
            ]);
        }
    }

    /**
     * event : on request output
     *
     * @param string $output
     */
    public static function onRequestOutput(string &$output): void
    {
        $getParams = $_GET;
        $postParams = $_POST;

        if (isset($getParams['_url'])) {
            unset($getParams['_url']);
        }

        // query strings have no cache
        if (!empty($getParams) || !empty($postParams)) {
            return;
        }

        // logged-in users get no cache
        if (QUI::getUsers()->isAuth(QUI::getUserBySession())) {
            $output = QUI\Cache\Parser\LazyLoading::getInstance()->parse($output);
            return;
        }

        try {
            if (QUI::getRewrite()->getSite()?->getAttribute('nocache')) {
                return;
            }
        } catch (QUI\Exception $Exception) {
            QUI\System\Log::addDebug($Exception->getMessage());
        }


        $Response = QUI::getGlobalResponse();

        if ($Response->getStatusCode() !== 200) {
            return;
        }

        try {
            if (QUI::getRewrite()->getSite()?->getAttribute('nocache')) {
                return;
            }

            $Package = QUI::getPackage('quiqqer/cache');
            $cacheSetting = $Package->getConfig()?->get('settings', 'cache');
        } catch (QUI\Exception $Exception) {
            QUI\System\Log::writeException($Exception);

            return;
        }

        if (!$cacheSetting) {
            return;
        }

        // check project setting
        try {
            $Project = QUI::getRewrite()->getProject();

            if ((int)$Project?->getConfig('website.nocache')) {
                return;
            }
        } catch (QUI\Exception) {
        }

        try {
            QUI\Cache\Handler::init()->generateCacheFromRequest($output);
        } catch (QUI\Exception $Exception) {
            QUI\System\Log::addDebug($Exception->getMessage(), [
                'trace' => $Exception->getTraceAsString()
            ]);
        } catch (\Exception $Exception) {
            QUI\System\Log::writeDebugException($Exception);
        }
    }

    protected static function normalizeHtmlCacheHitHeaders(Response $Response): void
    {
        $maxAge = Config::getHtmlCacheMaxAgeHeaderValue();

        if ($maxAge < 0) {
            $maxAge = 0;
        }

        $Response->headers->remove('Cache-Control');
        $Response->headers->remove('Pragma');
        $Response->headers->remove('Expires');
        $Response->headers->remove('Set-Cookie');

        $Response->setCache([
            'max_age' => $maxAge,
            'public' => true,
            'must_revalidate' => true
        ]);

        $Response->headers->set(
            'Cache-Control',
            'max-age=' . $maxAge . ', must-revalidate, public',
            true
        );

        // Remove session cookie from cached responses as the response is not unique (see quiqqer/core#1290)
        if (!headers_sent()) {
            header_remove('Set-Cookie');
        }
    }

    /**
     * event : on package config save
     *
     * @param QUI\Package\Package $Package
     */
    public static function onPackageConfigSave(QUI\Package\Package $Package): void
    {
        // Package settings can affect generated website output across modules.
        QUI\Cache\Handler::init()->clearCache();
    }

    /**
     * event : on template get header
     * Extend the header with the require js php bundler
     *
     * @param QUI\Template $Template
     */
    public static function onTemplateGetHeader(QUI\Template $Template): void
    {
        try {
            $Package = QUI::getPackage('quiqqer/cache');
            $cacheSetting = $Package->getConfig()?->get('settings', 'cache');
        } catch (QUI\Exception $Exception) {
            QUI\System\Log::writeException($Exception);

            return;
        }

        $Template->extendHeader(
            "<script>
                window.QUIQQER_CACHE_CACHESETTING = $cacheSetting;
            </script>"
        );
    }

    /**
     * Clear the cache -> onSiteSave ...
     * look at <!-- clear cache --> in events.xml
     */
    public static function clearCache(): void
    {
        if (QUI::getUsers()->isSystemUser(QUI::getUserBySession())) {
            return;
        }

        if (QUI::getUsers()->isNobodyUser(QUI::getUserBySession())) {
            return;
        }

        QUI\Cache\Handler::init()->clearCache();
    }

    /**
     * event : on image create size cache
     *
     * @param QUI\Projects\Media\Item $Image
     * @param Image $Cache
     */
    public static function onMediaCreateSizeCache(
        QUI\Projects\Media\Item $Image,
        Image $Cache
    ): void {
        if (!($Image instanceof QUI\Projects\Media\Image)) {
            return;
        }

        try {
            $Package = QUI::getPackage('quiqqer/cache');
            $optimizeOnResize = $Package->getConfig()?->get('settings', 'optimize_on_resize');
            $useWebP = Handler::init()->useWebP();
        } catch (QUI\Exception $Exception) {
            QUI\System\Log::writeException($Exception);

            return;
        }

        $cacheFile = self::getCreatedSizeCacheFile($Image, $Cache);

        if (empty($cacheFile)) {
            return;
        }

        if ($useWebP) {
            Optimizer::convertToWebP($cacheFile);
        }

        if (empty($optimizeOnResize)) {
            return;
        }

        try {
            $extension = strtolower((string)pathinfo($cacheFile, PATHINFO_EXTENSION));

            switch ($extension) {
                case 'jpg':
                case 'jpeg':
                    Optimizer::optimizeJPG($cacheFile);
                    break;

                case 'png':
                    Optimizer::optimizePNG($cacheFile);
                    break;
            }

            return;
        } catch (QUI\Exception $Exception) {
            QUI\System\Log::addWarning($Exception->getMessage());
        }

        $Cache->save(null, 70);
    }

    /**
     * Resolve the written cache file path for resize events across intervention/image versions.
     */
    protected static function getCreatedSizeCacheFile(
        QUI\Projects\Media\Image $Image,
        Image $Cache
    ): ?string {
        $originPath = $Cache->origin()->filePath();

        if (!empty($originPath) && file_exists($originPath)) {
            return $originPath;
        }

        $Media = $Image->getMedia();
        $cacheDir = CMS_DIR . $Media->getCacheDir();
        $file = $Image->getAttribute('file');
        $parts = pathinfo($file);

        if (empty($parts['filename']) || empty($parts['extension'])) {
            return null;
        }

        $dir = $cacheDir;

        if (!empty($parts['dirname']) && $parts['dirname'] !== '.') {
            $dir .= $parts['dirname'] . DIRECTORY_SEPARATOR;
        }

        if (!is_dir($dir)) {
            return null;
        }

        $pattern = $dir . $parts['filename'] . '__*.' . $parts['extension'];
        $files = glob($pattern);
        $originalSizedFile = $dir . $parts['filename'] . '.' . $parts['extension'];

        if (file_exists($originalSizedFile)) {
            if (!is_array($files)) {
                $files = [];
            }

            $files[] = $originalSizedFile;
        }

        if (!is_array($files) || empty($files)) {
            return null;
        }

        $files = array_filter($files, static fn($path) => is_file($path));

        if (empty($files)) {
            return null;
        }

        usort(
            $files,
            static fn($a, $b) => filemtime($b) <=> filemtime($a)
        );

        return $files[0];
    }

    /**
     * @param array<string, mixed> $previousData
     */
    public static function onMediaReplaceBegin(QUI\Projects\Media $Media, int $id, array $previousData): void
    {
        if (!Handler::init()->useWebP()) {
            return;
        }

        if (empty($previousData['file'])) {
            return;
        }

        try {
            self::deleteWebPCacheFiles(CMS_DIR . $Media->getCacheDir(), (string)$previousData['file']);
        } catch (QUI\Exception) {
        }
    }

    public static function onMediaReplace(QUI\Projects\Media $Media, QUI\Projects\Media\Item $Item): void
    {
        if (!Handler::init()->useWebP()) {
            return;
        }

        if ($Item instanceof QUI\Projects\Media\Image) {
            try {
                self::deleteWebPCacheFiles(CMS_DIR . $Media->getCacheDir(), (string)$Item->getAttribute('file'));
                $Item->deleteCache();
            } catch (QUI\Exception) {
            }
        }
    }

    /**
     * @param QUI\Projects\Media\Item $Item
     */
    public static function onMediaSave(QUI\Projects\Media\Item $Item): void
    {
        if (!Handler::init()->useWebP()) {
            return;
        }

        if (!($Item instanceof QUI\Projects\Media\Image)) {
            return;
        }

        try {
            $Media = $Item->getMedia();
            $cdir = CMS_DIR . $Media->getCacheDir();
            $file = $Item->getAttribute('file');

            self::deleteWebPCacheFiles($cdir, (string)$file);

            // create the webp main cache
            $cacheFile = $Item->createCache();
        } catch (QUI\Exception) {
            return;
        }

        Optimizer::convertToWebP((string)$cacheFile);

        // check if same file exists
        try {
            $Folder = $Item->getParent();
            $filename = $Item->getPathinfo(PATHINFO_FILENAME);

            if (!is_string($filename)) {
                return;
            }

            $children = $Folder->getChildrenByName($filename);

            if (count($children) >= 2) {
                QUI::getMessagesHandler()->addAttention(
                    QUI::getLocale()->get('quiqqer/cache', 'message.attention.webp.duplicate.file')
                );
            }
        } catch (QUI\Exception) {
        }
    }

    protected static function deleteWebPCacheFiles(string $cacheDir, string $file): void
    {
        if (empty($file)) {
            return;
        }

        $cacheFile = $cacheDir . $file;
        $cacheData = pathinfo($cacheFile);

        if (empty($cacheData['dirname']) || empty($cacheData['filename'])) {
            return;
        }

        $webPFiles = [
            $cacheData['dirname'] . DIRECTORY_SEPARATOR . $cacheData['filename'] . '.webp'
        ];

        $resizeWebPFiles = glob(
            $cacheData['dirname'] . DIRECTORY_SEPARATOR . $cacheData['filename'] . '__*.webp'
        );

        if (is_array($resizeWebPFiles)) {
            $webPFiles = array_merge($webPFiles, $resizeWebPFiles);
        }

        foreach ($webPFiles as $webPFile) {
            if (file_exists($webPFile)) {
                File::unlink($webPFile);
            }
        }
    }

    /**
     * @param string $picture
     */
    public static function onMediaCreateImageHtml(string &$picture): void
    {
        if (Handler::init()->useWebP() === false) {
            return;
        }

        if (stripos($picture, '<picture') !== false) {
            libxml_use_internal_errors(true);
            $dom = new DOMDocument();
            $dom->loadHTML('<?xml encoding="utf-8" ?>' . $picture, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
            $pictures = $dom->getElementsByTagName('picture');

            foreach ($pictures as $pic) {
                $sources = [];

                foreach ($pic->childNodes as $node) {
                    if ($node instanceof DOMElement && $node->tagName === 'source') {
                        $sources[] = $node;
                    }
                }

                foreach ($sources as $source) {
                    $srcset = $source->getAttribute('srcset');

                    if (preg_match('/\.(jpe?g|png)(\?.*)?$/i', $srcset)) {
                        $webpSrcset = preg_replace('/\.(jpe?g|png)(\?.*)?$/i', '.webp$2', $srcset);
                        $webpSource = $dom->createElement('source');

                        $webpSource->setAttribute('srcset', (string)$webpSrcset);
                        $webpSource->setAttribute('type', 'image/webp');

                        foreach ($source->attributes as $attr) {
                            if ($attr->name !== 'srcset' && $attr->name !== 'type') {
                                $webpSource->setAttribute($attr->name, $attr->value);
                            }
                        }

                        $pic->insertBefore($webpSource, $source);
                    }
                }

                // <img> im <picture> ebenfalls als <source> ergänzen
                foreach ($pic->childNodes as $node) {
                    if ($node instanceof DOMElement && $node->tagName === 'img') {
                        $imgSrc = $node->getAttribute('src');
                        $imgSrcset = $node->getAttribute('srcset');

                        if (
                            preg_match('/\.(jpe?g|png)(\?.*)?$/i', $imgSrc) ||
                            preg_match('/\.(jpe?g|png)(\?.*)?$/i', $imgSrcset)
                        ) {
                            $webpSource = $dom->createElement('source');

                            // srcset bevorzugen, sonst src
                            if (!empty($imgSrcset)) {
                                $webpSrcset = str_ireplace(['.png', '.jpg', '.jpeg'], '.webp', $imgSrcset);
                                $webpSource->setAttribute('srcset', $webpSrcset);
                            } elseif (!empty($imgSrc)) {
                                $webpSrc = preg_replace('/\.(jpe?g|png)(\?.*)?$/i', '.webp$2', $imgSrc);
                                $webpSource->setAttribute('srcset', (string)$webpSrc);
                            }

                            $webpSource->setAttribute('type', 'image/webp');

                            if ($node->hasAttribute('sizes')) {
                                $webpSource->setAttribute('sizes', $node->getAttribute('sizes'));
                            }

                            if ($node->hasAttribute('media')) {
                                $webpSource->setAttribute('media', $node->getAttribute('media'));
                            }

                            $pic->insertBefore($webpSource, $node);
                        }
                    }
                }
            }

            $result = $dom->saveHTML();
            $result = preg_replace('/^<\?xml.*?\?>/', '', (string)$result);
            $picture = trim((string)$result);
        }
    }

    /**
     * event: on quiqqer translator publish
     */
    public static function quiqqerTranslatorPublish(): void
    {
        self::clearCache();
    }

    /**
     * Don't use webp for mails
     *
     * @return void
     */
    public static function onMailerSendInit(): void
    {
        // this solution is not optimal
        // if a mail is sent during the normal running system, the webp is off for everyone (smarty too)
        if (!defined('QUIQQER_CACHE_DISABLE_WEBP')) {
            define('QUIQQER_CACHE_DISABLE_WEBP', true);
        }
    }

    /**
     * @return void
     */
    public static function onQuiqqerMenuIndependentClear(): void
    {
        self::clearCache();
    }

    public static function onUserLogin(User $User): void
    {
        Handler::setLoggedInCookieIfEnabled();
    }

    public static function onQuiqqerFrontendUsersUserAutoLogin(
        User $User,
        mixed $Registrar
    ): void {
        Handler::setLoggedInCookieIfEnabled();
    }

    public static function onUserLogout(User $User): void
    {
        Handler::removeLoggedInCookie();
    }

    public static function onUpdateEnd(): void
    {
        QUI\Cache\Handler::init()->clearCache();
    }
}
