<?php
/**
 * @package         CDN for Joomla!
 * @version         7.3.6PRO
 * 
 * @author          Peter van Westen <info@regularlabs.com>
 * @link            https://regularlabs.com
 * @copyright       Copyright © 2025 Regular Labs All Rights Reserved
 * @license         GNU General Public License version 2 or later
 */

namespace RegularLabs\Plugin\System\CDNForJoomla;

defined('_JEXEC') or die;

use Joomla\CMS\Uri\Uri as JUri;
use RegularLabs\Library\Protect as RL_Protect;
use RegularLabs\Library\RegEx as RL_RegEx;

class Replace
{
    static $set = null;

    public static function replace(&$string)
    {
        if (is_array($string))
        {
            self::replaceInList($string);

            return;
        }

        if ( ! is_string($string) || $string == '')
        {
            return;
        }

        $sets = Params::getSets();

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

        Protect::_($string);

        foreach ($sets as $set)
        {
            self::replaceBySet($string, $set);
        }

        RL_Protect::unprotect($string);
    }

    private static function addQueryToFile($file, $query = [], $hash = '')
    {
        $file = trim($file);

        if ( ! empty($query))
        {
            $file .= '?' . implode('&', $query);
        }

        if ( ! empty($hash))
        {
            $file .= '#' . $hash;
        }

        return $file;
    }

    private static function fileExists($file)
    {
        return file_exists(JPATH_SITE . '/' . $file);
    }

    private static function fileIsIgnored($file)
    {
        foreach (self::$set->ignorefiles as $ignore)
        {
            if ($ignore && (str_contains($file, $ignore) || str_contains(htmlentities($file), $ignore)))
            {
                return true;
            }
        }

        return false;
    }

    private static function getCdnUrl($file)
    {
        $cdns = self::$set->cdns;

        if (count($cdns) > 1)
        {
            // Make sure a file is always served from the same cdn server to leverage browser caching
            $cdns = [$cdns[hexdec(substr(hash('md2', $file), -4)) % count($cdns)]];
        }

        return self::$set->protocol . $cdns[0];
    }

    private static function getFileParts($file)
    {
        $file  = trim($file);
        $query = [];
        $hash  = '';

        if ( ! $file)
        {
            return [$file, $query, $hash];
        }

        if (str_contains($file, '#'))
        {
            [$file, $hash] = explode('#', $file, 2);
        }

        if (str_contains($file, '?'))
        {
            [$file, $query] = explode('?', $file, 2);
            $query = explode('&', $query);
        }

        return [$file, $query, $hash];
    }

    private static function getNewFilePath($root, $file)
    {
        RL_RegEx::match('^(\s*)', $file, $match);
        $leading_whitespace = $match[1];

        $file = RL_RegEx::replace('^' . RL_RegEx::quote(JUri::root()), '', trim($file));

        [$file, $query, $hash] = self::getFileParts($file);

        if ( ! $file || self::fileIsIgnored($file))
        {
            return false;
        }

        if (
            self::$set->enable_versioning
            && self::fileExists($root . $file)
            && self::includeVersioningFile($file)
            && file_exists(JPATH_SITE . '/' . $root . $file)
        )
        {
            $query[] = filemtime(JPATH_SITE . '/' . $root . $file);
        }

        return $leading_whitespace . self::getCdnUrl($file)
            . '/' . self::addQueryToFile($file, $query, $hash);
    }

    private static function includeVersioningFile($file)
    {
        foreach (self::$set->versioning_filetypes as $filetype)
        {
            if (str_ends_with($file, $filetype))
            {
                return true;
            }
        }

        return false;
    }

    private static function replaceBySearch(&$string, $search)
    {
        RL_RegEx::matchAll($search, $string, $matches);

        if (empty($matches))
        {
            return false;
        }

        $changed = false;

        foreach ($matches as $match)
        {
            if ( ! self::replaceBySearchMatch($string, $match))
            {
                continue;
            }

            $changed = true;
        }

        return $changed;
    }

    private static function replaceBySearchList(&$string, $searches)
    {
        $changed = 0;

        foreach ($searches as $word => $search)
        {
            if ( ! is_numeric($word) && ! str_contains($string, $word))
            {
                continue;
            }

            $changed = self::replaceBySearch($string, $search);
        }

        return $changed;
    }

    private static function replaceBySearchMatch(&$string, $match)
    {
        if (str_contains($match['prefix'], 'srcset'))
        {
            return self::replaceBySearchSrcset($string, $match);
        }

        $file = self::getNewFilePath($match['root'], $match['url']);

        if ( ! $file)
        {
            return false;
        }

        $string = str_replace(
            $match[0],
            $match['prefix'] . $file . $match['end'],
            $string
        );

        return true;
    }

    private static function replaceBySearchSrcset(&$string, $match)
    {
        $new_files = $match['root'] . $match['url'];
        $files     = explode(',', $new_files);

        $regex = '^(?<prefix> *)' . self::$set->url_regex . '(?<suffix>( .*)?)$';

        foreach ($files as $file)
        {
            //$regex = Params::getUrlRegex();
            RL_RegEx::match($regex, $file, $url_match);

            $new_file = self::getNewFilePath($url_match['root'], $url_match['url']);

            if ( ! $new_file)
            {
                continue;
            }

            $new_file = $url_match['prefix'] . $new_file . $url_match['suffix'];

            $new_files = str_replace(
                $file,
                $new_file,
                $new_files
            );
        }

        if ($match['url'] == $new_files)
        {
            return false;
        }

        $string = str_replace(
            $match[0],
            $match['prefix'] . $new_files . $match['end'],
            $string
        );

        return true;
    }

    private static function replaceBySet(&$string, $set)
    {
        self::$set = $set;

        self::replaceBySearchList($string, self::$set->searches);

        if ( ! empty(self::$set->enable_in_scripts) && str_contains($string, '<script'))
        {
            self::replaceInJavascript($string);
        }
    }

    private static function replaceInJavascript(&$string)
    {
        $regex = '<script(?:\s+(language|type)\s*=[^>]*)?>.*?</script>';

        RL_RegEx::matchAll($regex, $string, $parts);

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

        foreach ($parts as $part)
        {
            self::replaceInJavascriptStringPart($string, $part);
        }
    }

    private static function replaceInJavascriptStringPart(&$string, $part)
    {
        $new_string = $part[0];

        if ( ! self::replaceBySearchList($new_string, self::$set->js_searches))
        {
            return;
        }

        $string = str_replace($part[0], $new_string, $string);
    }

    private static function replaceInList(&$array)
    {
        foreach ($array as &$val)
        {
            self::replace($val);
        }
    }
}
