<?php 

namespace AdvancedBlockControls;

// Prevent direct access to the file
if (!defined('ABSPATH')) {
    exit;
}

class FontObject {
    public $fontFamilyValue;
    public $fontFamilyStyleList;

    public function __construct($fontFamilyValue, $fontFamilyStyleList = []) {
        $this->fontFamilyValue = $fontFamilyValue;
        $this->fontFamilyStyleList = $fontFamilyStyleList;
    }
}
class CssManager
{
    private $cssString = ""; // Stores the cumulative CSS string to be injected
    private $cssInsertionTrack = array(); // Tracks which block IDs have had their CSS processed
    private $googleFonts = array(); // Google fonts track
    private $utils; // Instance of the Utils class for accessing utility methods
    private $globalData; // Instance of the GlobalData class for variable resolution

    // Constructor initializes hooks and the Utils instance
    public function __construct()
    {
        // Hook into WordPress to prepare CSS when rendering blocks and to inject CSS in the head section
        add_filter('render_block', [$this, 'prepareCss'], 10, 2);
        if (wp_is_block_theme()) {
            // In classic themes there's an issue that css not being fully ready when wp_enqueue_scripts is called, in recent experience only first block css is ready
            // wp_footer runs after all blocks are rendered but as we use wp_enqueue_style in injectCSS this won't override the early css added with wp_enqueue_scripts
            // so we need to make it conditional to avoid this issue, this update has been made specifically to fix an issue with Bricks Builder
            add_action('wp_enqueue_scripts', [$this, 'injectCss']);
        } else {
            // For compatability with some classic themes like: the Astra, Neve, Blocksy and Hello Elementor theme styles
            add_action('wp_footer', [$this, 'injectCss']);
        }

        // Initialize the Utils instance
        $this->utils = new Utils();
        
        // Don't initialize GlobalData here - check when needed in injectCss()
        $this->globalData = null;
    }

    // Apply Target to all selectors in the css string
    private function applyTarget($css, $target = "")
    {
        $cssString = "";
        $finalSelector = "";

        if ($css) {
            $finalSelector .= "%%selector%%";
        }

        if ($target) {
            $finalSelector .= " " . $target;
        }

        if ($css) {
            $cssString = str_replace('%%selector%%', $finalSelector, $css);
        }

        return $cssString;
    }

    private function applySpecifityToSelector($selector,  $blockName, $frontEndSpecifity = "" ) {
        if(!isset($blockName)) {
            return $selector;
        }

        $settings = $this->utils->getSettings();

        $finalSpecifity = "";

        if ($this->utils->isCoreBlock($blockName)) {
            $finalSpecifity = $settings['frontEndSpecifityCore'];
        } else {
            $finalSpecifity = $settings['frontEndSpecifityThirdParty'];
        }

        if(isset($finalSpecifity)) {
            return $finalSpecifity . " " . $selector;
        }

        return $selector;
    }

    private function applyClassName($css, $className, $blockName) {
      // We apply specifity first
      $finalSelector = $this->applySpecifityToSelector("%%selector%%", $blockName, "");

      // Replace %%selector%% with actual className
      $finalSelector = str_replace("%%selector%%", "." . $className, $finalSelector);

      $result = str_replace("%%selector%%",  $finalSelector, $css);

      // We use final selector in the replace
      return  $result;
    }

    // Wraps CSS in a media query
    private function wrapWithMediaQuery($css, $query)
    {
        return "@media only screen and " . $query . " {" . $css . "}";
    }

    // Helper function to apply CSS and CSS editor values to a target or directly to device CSS
    private function applyControlCss($baseCss, $controlData, $target = null, $controlType = null) {
        $controlCss = $baseCss;
        $css = $controlData['css'] ?? '';
        if (!empty($target) && $controlType !== "custom_css") {
            // If there's a target, apply the CSS to the target execluding custom_css control
            $controlCss .= $this->applyTarget($css, $target);
        } else {
            // If there's no target, append CSS directly
            $controlCss .= $css;
        }
        return $controlCss;
    }

    // Helper function to finalize CSS by combining rules, applying class names, and avoiding wrapping in media queries if not needed
    private function finalizeCss($baseCss, $targetsCss, $className,  $blockName, $mediaQuery = null) {
        $finalCss = $baseCss . implode("", $targetsCss); // Combine device-specific CSS with targeted CSS rules
        
        // Check if finalCss is empty or contains only whitespace
        if (!trim($finalCss)) {
            return ""; // Return an empty string if there's no CSS to process
        }

        $finalCss = $this->combineCssRules($finalCss); // Combine CSS rules
        $finalCss = $this->applyClassName($finalCss, $className, $blockName); // Apply final class name with specificity

        // Wrap the CSS in a media query if provided, and finalCss is not empty
        if ($mediaQuery && trim($finalCss)) {
            $finalCss = $this->wrapWithMediaQuery($finalCss, $mediaQuery);
        }

        return $finalCss;
    }

    private function processDeviceControls($deviceControls, $blockName, $className, $mediaQuery = null, $mobileFirstMediaQuery = null) {
        $deviceCss = "";
        $targetsCss = array();
    
        $mobileFirstDeviceCss = "";
        $mobileFirstTargetsCss = array();
         // Reorder deviceControls to move custom_css controls last
        uksort($deviceControls, function($a, $b) {
            $typeA = $this->utils->getControlTypeFromSlug($a);
            $typeB = $this->utils->getControlTypeFromSlug($b);

            if ($typeA === "custom_css" && $typeB !== "custom_css") {
                return 1; // Move custom_css later
            } elseif ($typeA !== "custom_css" && $typeB === "custom_css") {
                return -1; // Keep others before custom_css
            }
            return 0; // Maintain original order otherwise
        });
    
        foreach ($deviceControls as $controlSlug => $controlData) {
            $controlId = $controlData["controlId"] ?? null;
        
            // Extract control type from control slug
            $controlType = $this->utils->getControlTypeFromSlug($controlSlug);
            
            // Check if CSS should be generated for this control
            if ($this->utils->shouldGenerateCssForControl($blockName, $controlId)) {
                
                $target = $this->utils->getControlTarget($blockName, $controlId);
                
                if ($this->utils->isMobileFirstControl($controlType)) {
                   
                    // For mobile-first controls
                    $mobileFirstDeviceCss = $this->applyControlCss($mobileFirstDeviceCss, $controlData, $target);
                   
                    if (!empty($target)) {
                        $mobileFirstTargetsCss[] = $this->applyControlCss("", $controlData, $target);
                    }
                } else {
                    // For other controls
                    $deviceCss = $this->applyControlCss($deviceCss, $controlData, $target, $controlType);
                    if (!empty($target)) {
                        $targetsCss[] = $this->applyControlCss("", $controlData, $target);
                    }
                }
        
                // Track google fonts
                if (isset($controlData['data']['fontFamilyValue'])) {
                    $fontFamilyStyle = $controlData['data']['fontFamilyStyle'] ?? null;
                    $this->googleFonts = $this->addOrUpdateFont($this->googleFonts, $controlData['data']['fontFamilyValue'], $fontFamilyStyle);
                }
            }
        }
    
        // Finalize CSS for desktop-first and mobile-first
        $finalDeviceCss = $this->finalizeCss($deviceCss, $targetsCss, $className, $blockName, $mediaQuery);
        $finalMobileFirstDeviceCss = $this->finalizeCss($mobileFirstDeviceCss, $mobileFirstTargetsCss, $className, $blockName, $mobileFirstMediaQuery);
        
        // Combine desktop-first and mobile-first CSS
        return $finalDeviceCss . $finalMobileFirstDeviceCss;
    }
    

    // Main method that prepares the CSS for each block
    public function prepareCss($block_content, $block) 
    {    
        
        if (isset($block['attrs'][AttributesManager::ATTRIBUTE_KEY])) {

            // Getting attribute data
            $data = json_decode($block['attrs'][AttributesManager::ATTRIBUTE_KEY], true);

            // Check and process only if the block css ID haven't been processed yet
            if (isset($data['id']) && !in_array($data['id'], $this->cssInsertionTrack)) {

                // className
                $className = "abc-style-" . $data['id'];

                // Mark this block css ID as processed to avoid duplication
                $this->cssInsertionTrack[] = $data['id'];

                $settings = $this->utils->getSettings();
                $finalCss = "";

                /*
                    - Mobile First Controls
                */

                // 1. WideScreen
                if ($settings['brpWideScreenEnabled']) {
                    $mobileFirstWideScreenQuery = "(min-width:{$settings['brpWideScreenVal']}{$settings['brpWideScreenUnit']})";
                } else {
                    $mobileFirstWideScreenQuery = null;
                }

                // 2. Desktop
                $mobileFirstDesktopQuery = "(min-width:{$settings['brpTabletVal']}{$settings['brpTabletUnit']})";

                if ($settings['brpLaptopEnabled']) {
                    $mobileFirstDesktopQuery = "(min-width:{$settings['brpLaptopVal']}{$settings['brpLaptopUnit']})";
                } elseif ($settings['brpTabletLandscapeEnabled']) {
                    $mobileFirstDesktopQuery = "(min-width:{$settings['brpTabletLandscapeVal']}{$settings['brpTabletLandscapeUnit']})";
                }

                if ($settings['brpWideScreenEnabled']) {
                    $mobileFirstDesktopQuery .= " and (max-width:{$settings['brpWideScreenVal']}{$settings['brpWideScreenUnit']})";
                }

                // 3. Laptop
                $mobileFirstLaptopQuery = "(min-width:{$settings['brpTabletVal']}{$settings['brpTabletUnit']}) and (max-width:{$settings['brpLaptopVal']}{$settings['brpLaptopUnit']})";

                if ($settings['brpTabletLandscapeEnabled']) {
                    $mobileFirstLaptopQuery = "(min-width:{$settings['brpTabletLandscapeVal']}{$settings['brpTabletLandscapeUnit']}) and (max-width:{$settings['brpLaptopVal']}{$settings['brpLaptopUnit']})";
                }

                // 4. Tablet
                $mobileFirstTabletQuery = "(min-width:{$settings['brpMobileVal']}{$settings['brpMobileUnit']}) and (max-width:{$settings['brpTabletVal']}{$settings['brpTabletUnit']})";

                if ($settings['brpMobileLandscapeEnabled']) {
                    $mobileFirstTabletQuery = "(min-width:{$settings['brpMobileLandscapeVal']}{$settings['brpMobileLandscapeUnit']}) and (max-width:{$settings['brpTabletVal']}{$settings['brpTabletUnit']})";
                }

                // 5. TabletLandscape
                $mobileFirstTabletLandscapeQuery = "(min-width:{$settings['brpTabletVal']}{$settings['brpTabletUnit']}) and (max-width:{$settings['brpTabletLandscapeVal']}{$settings['brpTabletLandscapeUnit']})";

                // 6. MobileLandscape
                $mobileFirstMobileLandscapeQuery = "(min-width:{$settings['brpMobileVal']}{$settings['brpMobileUnit']}) and (max-width:{$settings['brpMobileLandscapeVal']}{$settings['brpMobileLandscapeUnit']})";

                // 7. Mobile
                $mobileFirstMobileQuery = "(max-width:{$settings['brpMobileVal']}{$settings['brpMobileUnit']})";

                // Define media queries based on settings
                $tabletQuery = "(max-width:{$settings['brpTabletVal']}{$settings['brpTabletUnit']})";
                $mobileQuery = "(max-width:{$settings['brpMobileVal']}{$settings['brpMobileUnit']})";

                $wideScreenQuery = "(min-width: {$settings['brpWideScreenVal']}{$settings['brpWideScreenUnit']})";
                $laptopQuery = "(max-width: {$settings['brpLaptopVal']}{$settings['brpLaptopUnit']})";
                $tabletLandscapeQuery = "(max-width: {$settings['brpTabletLandscapeVal']}{$settings['brpTabletLandscapeUnit']})";
                $mobileLandscapeQuery = "(max-width: {$settings['brpMobileLandscapeVal']}{$settings['brpMobileLandscapeUnit']})";

                
                // Process CSS for each device ( ORDER MATTERS )
                if (isset($data['screens'])) {
                    
                    // Desktop
                    if (isset($data['screens']['Desktop'])) {
                        $finalCss .= $this->processDeviceControls($data['screens']['Desktop'], $block['blockName'], $className, "", $mobileFirstDesktopQuery);
                    }

                    // WideScreen
                    if (!empty($settings['brpWideScreenEnabled']) && isset($data['screens']['WideScreen'])) {
                        $finalCss .= $this->processDeviceControls(
                            $data['screens']['WideScreen'],
                            $block['blockName'],
                            $className,
                            $wideScreenQuery,
                            $mobileFirstWideScreenQuery
                        );
                    }

                     // Laptop
                    if (!empty($settings['brpLaptopEnabled']) && isset($data['screens']['Laptop'])) {
                        $finalCss .= $this->processDeviceControls(
                            $data['screens']['Laptop'],
                            $block['blockName'],
                            $className,
                            $laptopQuery,
                            $mobileFirstLaptopQuery
                        );
                    }

                     // TabletLandscape
                     if (!empty($settings['brpTabletLandscapeEnabled']) && isset($data['screens']['TabletLandscape'])) {
                        $finalCss .= $this->processDeviceControls(
                            $data['screens']['TabletLandscape'],
                            $block['blockName'],
                            $className,
                            $tabletLandscapeQuery,
                            $mobileFirstTabletLandscapeQuery
                        );
                    }

                    // Tablet
                    if (isset($data['screens']['Tablet'])) {
                        $finalCss .= $this->processDeviceControls($data['screens']['Tablet'],  $block['blockName'], $className, $tabletQuery, $mobileFirstTabletQuery);
                    }

                    // MobileLandscape
                    if (!empty($settings['brpMobileLandscapeEnabled']) && isset($data['screens']['MobileLandscape'])) {
                        $finalCss .= $this->processDeviceControls(
                            $data['screens']['MobileLandscape'],
                            $block['blockName'],
                            $className,
                            $mobileLandscapeQuery,
                            $mobileFirstMobileLandscapeQuery
                        );
                    }


                    if (isset($data['screens']['Mobile'])) {
                        $finalCss .= $this->processDeviceControls($data['screens']['Mobile'], $block['blockName'], $className, $mobileQuery, $mobileFirstMobileQuery);
                    }

                }
               
                // Apply "forceRules" setting by replacing placeholder
                if ($this->utils->isCoreBlock($block["blockName"]) && $settings['forceRulesCore']) {
                    $finalCss = str_replace("%%important%%", " !important", $finalCss);
                } else if (!$this->utils->isCoreBlock($block["blockName"]) && $settings['forceRulesThirdParty']) {
                    $finalCss = str_replace("%%important%%", " !important", $finalCss);
                } else {
                    $finalCss = str_replace("%%important%%", "", $finalCss);
                }

                // Append the final CSS to the class property for later injection
                $this->cssString .= $finalCss;

            }

            // Modify the block's content to include the generated CSS class (if necessary)
            if ( isset( $data['id'] ) && ! empty( $block_content ) ) {
                $block_content = $this->utils->addStyleClassToBlockContent($block_content, $data['id']);
            }
            
        }
        return $block_content;
    }

    // Injects the accumulated CSS into the page head
    public function injectCss()
    {
       
        // Handle Google Fonts import if there are any fonts to include
        if (count($this->googleFonts) > 0) {
            $googleFontsCss = $this->generateSingleCSSImportGoogleFonts($this->googleFonts);
            // Generate a version hash for caching
            $googleFontsVersion = md5($googleFontsCss);
            // Enqueue the Google Fonts CSS
            wp_register_style('abcon-google-fonts', false, [], $googleFontsVersion );
            wp_enqueue_style('abcon-google-fonts');
            wp_add_inline_style('abcon-google-fonts', $googleFontsCss);
        }

        // Check if there is any CSS to inject
        if (!empty($this->cssString)) {
            // Initialize GlobalData here (both free and pro plugins are loaded by now)
            if ($this->globalData === null && class_exists('AdvancedBlockControls\GlobalData')) {
                $this->globalData = new GlobalData();
            }
            
            // Process CSS: replace variables only if pro plugin is active
            if ($this->globalData !== null) {
                $processedCss = $this->globalData->processVarsInCss($this->cssString);
            } else {
                // Use CSS as-is without variable processing (pro not active)
                $processedCss = $this->cssString;
            }
            
            // Generate a version hash for caching
            $cssVersion = md5($processedCss);

            // Enqueue the processed CSS
            wp_register_style('abcon-styles', false, [], $cssVersion);
            wp_enqueue_style('abcon-styles');
            wp_add_inline_style('abcon-styles', $processedCss);
        }
    }


    /**
     * Adds a new font family or updates an existing one within the global `googleFonts` array.
     *
     * @param array $googleFonts The array of FontObjects to add or update.
     * @param string $fontFamilyValue The name of the font family to add or update.
     * @param string|null $fontFamilyStyle Optional. The style to add to the font family's style list.
     * @return array The updated array of FontObjects.
     */
    private function addOrUpdateFont($googleFonts, $fontFamilyValue, $fontFamilyStyle = null) {
        $found = false;
        foreach ($googleFonts as $font) {
            if ($font->fontFamilyValue === $fontFamilyValue) {
                $found = true;
                if ($fontFamilyStyle !== null && !in_array($fontFamilyStyle, $font->fontFamilyStyleList)) {
                    $font->fontFamilyStyleList[] = $fontFamilyStyle;
                }
                break;
            }
        }

        if (!$found) {
            $newFontObject = new FontObject($fontFamilyValue, $fontFamilyStyle ? [$fontFamilyStyle] : []);
            $googleFonts[] = $newFontObject;
        }

        return $googleFonts;
    }


    /**
     * Generates a single CSS `@import` statement to import all specified fonts and their styles from Google Fonts.
     *
     * @param array $googleFonts An array of FontObjects where each object represents a font family.
     * @return string A string containing a single CSS `@import` statement that includes all specified fonts and their styles.
     */
    private function generateSingleCSSImportGoogleFonts($googleFonts) {
        $allFonts = array_map(function($font) {
            $formattedStyles = array_map(function($style) {
                if (substr($style, -1) === "i") {
                    return substr($style, 0, -1) . "italic";
                } elseif ($style === "regular") {
                    return "400";
                } elseif ($style === "italic") {
                    return "400italic";
                } else {
                    return $style;
                }
            }, $font->fontFamilyStyleList);

            $formattedStyles = implode(",", $formattedStyles);
            $fontFamily = str_replace(' ', '+', $font->fontFamilyValue);
            return $fontFamily . ($formattedStyles ? ':' . $formattedStyles : '');
        }, $googleFonts);

        $allFonts = implode("|", $allFonts);
        return "@import url('https://fonts.googleapis.com/css?family={$allFonts}');";
    }

    private function combineCssRules(string $input): string {
        // Step 1: Protect $var({"uid":"..."}) from regex corruption
        $varPattern = '/\$var\(\{"uid":"[^"]+"\}\)/';
        $vars = [];
        
        $protectedInput = preg_replace_callback($varPattern, function($match) use (&$vars) {
            $placeholder = '__VAR_' . count($vars) . '__';
            $vars[] = $match[0];
            return $placeholder;
        }, $input);

        // Step 2: Parse and combine CSS rules
        $regex = '/(?<!{)(%%selector%%[^{]*){([^}]*)}/';
        $rulesMap = [];
        $unmatchedEnd = "";

        preg_match_all($regex, $protectedInput, $matches, PREG_OFFSET_CAPTURE);
        $lastIndex = 0;

        foreach ($matches[0] as $i => $match) {
            $unmatchedBefore = substr($protectedInput, $lastIndex, $match[1] - $lastIndex);
            $unmatchedEnd .= $unmatchedBefore;
            $lastIndex = $match[1] + strlen($match[0]);

            if ($protectedInput[$match[1] - 1] === "{") {
                continue;
            }

            $selector = trim($matches[1][$i][0]);
            $cssRules = trim($matches[2][$i][0]);

            if (array_key_exists($selector, $rulesMap)) {
                if (strpos($rulesMap[$selector], $cssRules) === false) {
                    $rulesMap[$selector] .= $cssRules;
                }
            } else {
                $rulesMap[$selector] = $cssRules;
            }
        }

        // Step 3: Handle unmatched content
        $unmatchedAfter = substr($protectedInput, $lastIndex);
        $unmatchedEnd .= $unmatchedAfter;

        // Step 4: Reconstruct combined CSS
        $combinedCss = "";
        foreach ($rulesMap as $selector => $rules) {
            $combinedCss .= $selector . "{" . $rules . "}";
        }

        $combinedCss .= $unmatchedEnd;

        // Step 5: Restore original variable strings
        foreach ($vars as $index => $varString) {
            $combinedCss = str_replace('__VAR_' . $index . '__', $varString, $combinedCss);
        }

        return $combinedCss;
    }
}
