<?php

namespace Breakdance\BreakdanceOxygen\Selectors;

use Breakdance\Filesystem\Consts;
use function Breakdance\Data\get_global_option;
use function Breakdance\Data\GlobalRevisions\add_new_revision;
use function Breakdance\Data\GlobalRevisions\load_revisions_list;
use function Breakdance\Data\set_global_option;
use function Breakdance\Filesystem\get_file_contents_from_bucket;
use function Breakdance\Filesystem\HelperFunctions\generate_error_msg_from_fs_wp_error;
use function Breakdance\Filesystem\HelperFunctions\is_fs_error;
use function Breakdance\Filesystem\write_file_to_bucket;
use function Breakdance\Render\formatCss;
use function Breakdance\Render\getCss;
use const Breakdance\Data\GlobalRevisions\BREAKDANCE_REVISION_TYPE_OXY_SELECTORS;

if (BREAKDANCE_MODE == 'breakdance') {
    add_filter(
        '__breakdance_oxy_selectors_css_stylesheet_tag',
        function () {
            return '';
        }
    );
}

add_action('breakdance_loaded', function () {
    \Breakdance\AJAX\register_handler(
        'breakdance_save_oxy_selectors',
        '\Breakdance\BreakdanceOxygen\Selectors\saveSelectors',
        'edit',
        false,
        ['args' => ['selectors' => FILTER_UNSAFE_RAW]]
    );
});

/**
 * @param string $data
 * @return void
 */
function saveSelectors($data)
{
    // Naive way to replace empty objects with nulls, to avoid PHP from converting them to arrays
    $data = str_replace("{}", "null", $data);

    /** @var array{selectors: OxygenSelector[], collections: string[]} $selectorsData */
    $selectorsData = json_decode($data, true);
    $selectors = $selectorsData['selectors'];

    /** @var false|OxygenSelector[] $currentCssSelectors */
    $currentCssSelectors = get_global_option('oxy_selectors_json_string');

    set_global_option('oxy_selectors_collections_json_string', $selectorsData['collections']);

    if ($currentCssSelectors !== $selectors) {
        $currentRevisions = load_revisions_list(BREAKDANCE_REVISION_TYPE_OXY_SELECTORS);
        if (!sizeof($currentRevisions) && $currentCssSelectors) {
            add_new_revision($currentCssSelectors, BREAKDANCE_REVISION_TYPE_OXY_SELECTORS);
        }

        set_global_option('oxy_selectors_json_string', $selectors);

        add_new_revision($selectors, BREAKDANCE_REVISION_TYPE_OXY_SELECTORS);
    }

    if ($currentCssSelectors !== $selectors || !getSelectorsCacheStore()) {
        \Breakdance\Render\generateCacheForGlobalSettings();
    }
}

/**
 * @psalm-suppress MixedInferredReturnType
 * @psalm-suppress MixedReturnStatement
 * @return OxygenSelector[]
 */
function getOxySelectors()
{
    static $selectors = false;

    if ($selectors === false) {
        /**
         * @psalm-suppress MixedAssignment
         * @var OxygenSelector[]|false $selectors
         */
        $selectors = get_global_option('oxy_selectors_json_string');
    }

    return $selectors ?: [];
}

/**
 * @return string[]
 */
function getOxySelectorsCollections()
{
    /**
     * @psalm-suppress MixedAssignment
     * @var string[]|null
     */
    $collections = get_global_option('oxy_selectors_collections_json_string');

    // Auto-fill collections from selectors array if empty
    if (!$collections) {
        $selectors = getOxySelectors();

        if (count($selectors) > 0) {
            /** @var string[] $collections */
            $collections = array_values(array_unique(array_column($selectors, 'collection')));
            return $collections;
        }
    }

    return $collections ?: [];
}

/**
 * @return array{
 *     template: string,
 *     selectors: OxygenSelector[],
 *     collections: string[],
 *     propertyPathsToWhitelistInFlatProps: string[]
 * }
 */
function getOxySelectorsDataForBuilder()
{
    return [
        'template' => cssTemplate(),
        'selectors' => getOxySelectors(),
        'collections' => getOxySelectorsCollections(),
        'propertyPathsToWhitelistInFlatProps' => propertyPathsToWhitelistInFlatProps(),
    ];
}

/**
 * @return string
 */
function cssTemplate()
{
    return (string) file_get_contents(__DIR__ . '/selectors.twig');
}

/**
 * @return string[]
 */
function propertyPathsToWhitelistInFlatProps()
{
    return [
        'selector.id',
        'selector.name',
        'selector.type',
        'selector.children[].id',
        'selector.children[].name'
    ];
}

/**
 * @param array<array-key, mixed> $properties
 * @param OxygenVariable[] $variables
 * @return array
 */
function replaceVariablesInProperties($properties, $variables) {
    /** @var mixed $value */
    foreach ($properties as $key => $value) {
        if (is_array($value)) {
            $properties[$key] = replaceVariablesInProperties($value, $variables);
        } elseif (is_string($value) && str_starts_with($value, '{var-')) {
            $id = str_replace(['{var-', '}'], '', $value);
            $result = array_filter(
                $variables,
                fn($item) => $item['id'] === $id
            );

            $variable = reset($result);

            if ($variable) {
                $properties[$key] = "var(--{$variable['cssVariableName']})";
            } else {
                $properties[$key] = null;
            }
        }
    }

    return $properties;
}

/**
 * @param string $input
 * @param OxygenVariable[] $variables
 * @return string
 */
function replaceVariablesInString(string $input, array $variables): string {
    return preg_replace_callback(
        '/\{var-([^\}]+)\}/',
        /**
         * @param array<int, string> $matches
         */
        function (array $matches) use ($variables): string {
            /**
             * @psalm-suppress PossiblyUndefinedIntArrayOffset
             */
            $id = $matches[1];
            /** @var OxygenVariable $variable */
            $variable = current(array_filter($variables, fn($item) => $item['id'] === $id));

            if ($variable) {
                return "var(--{$variable['cssVariableName']})";
            }

            return '';
        },
        $input
    );
}

/**
 * @psalm-suppress LessSpecificReturnStatement
 * @psalm-suppress MoreSpecificReturnType
 * @param OxygenSelector|OxygenNestedSelector $selector
 * @param OxygenVariable[] $variables
 * @return OxygenSelector|OxygenNestedSelector
 */
function parseVariablesInSelector($selector, $variables = []) {
    if (isset($selector['properties'])) {
        $selector['properties'] = replaceVariablesInProperties($selector['properties'], $variables);
    }

    if (isset($selector['children'])) {
        $selector['children'] = array_map(
            fn($child) => parseVariablesInSelector($child, $variables),
            $selector['children']
        );
    }

    return $selector;
}

/**
 * Changes made here should be reflected in replaceAmpersandInSelector in the oxygen/classes/lib/utils.ts file
 * @param OxygenSelector $selector
 * @return OxygenSelector
 */
function replaceAmpersandInSelector($selector) {
    $replacement = $selector['type'] === 'class' ? '.' . $selector['name'] : $selector['name'];

    if (!isset($selector['children'])) {
        return $selector;
    }

    $selector['children'] = array_map(function($child) use ($replacement) {
        // Implicitly add an ampersand to the beginning of the name
        $name = str_contains($child['name'], '&') ? $child['name'] : '& ' . $child['name'];

        return array_merge($child, [
            'name' => str_replace('&', $replacement, $name)
        ]);
    }, $selector['children']);

    return $selector;
}

/**
 * @param OxygenSelector[] $selectors
 * @return string
 */
function cssForSelectors($selectors)
{
    $breakpoints = \Breakdance\Config\Breakpoints\get_breakpoints();
    $cssTemplate = cssTemplate();
    $propertiesToWhiteListInFlatProps = propertyPathsToWhitelistInFlatProps();
    $variables = \Breakdance\Variables\getVariables();

    /** @var OxygenSelector[] $selectors */
    $selectors = array_map(
        fn($selector) => parseVariablesInSelector(
            replaceAmpersandInSelector($selector),
            $variables
        ),
        $selectors
    );

    $cacheStore = \Breakdance\BreakdanceOxygen\Selectors\getSelectorsCacheStore();
    $css = '';

    $updatedCache = [];

    foreach ($selectors as $selector) {
        [$selectorCss, $selectorFonts] = getCssAndFontsForSelector(
            $selector,
            $cssTemplate,
            $propertiesToWhiteListInFlatProps,
            $breakpoints,
            $cacheStore
        );

        $css .= $selectorCss;

        $updatedCache[] = [
            'id' => $selector['id'],
            'hash' => getSelectorHash($selector),
            'css' => $selectorCss,
            'fonts' => $selectorFonts
        ];
    }

    saveSelectorsCacheStore($updatedCache);

    return $css;
}

/**
 * @param TreeNode $node
 * @return array
 */
function getElementOxySelectors($node)
{
    /** @var string[] $ids */
    $ids = $node['data']['properties']['meta']['classes'] ?? [];

    if (empty($ids)) return [];

    $availableClasses = getOxySelectors();
    $classMap = array_column($availableClasses, null, 'id');

    return array_filter(
        array_map(
            fn($oxySelectorId) => $classMap[$oxySelectorId] ?? null,
            $ids
        )
    );
}

/**
 * @param OxygenSelector $selector
 * @return string[]
 */
function getFontsForSelector($selector)
{
    if (!isset($selector['properties'])) {
        return [];
    }

    /** @var string[] $fonts */
    $fonts = [];

    /** @var array<string, mixed>|null $breakpointProperties */
    foreach ($selector['properties'] as $breakpointProperties) {
        if (!is_array($breakpointProperties)) {
            continue;
        }

        /** @psalm-suppress MixedAssignment */
        $fonts[] = $breakpointProperties['typography']['font_family'] ?? '';
    }

    /** @var string[] $result */
    $result = array_values(array_unique(array_filter($fonts)));

    return $result;
}

// Cache Methods

/**
 * @param OxygenSelector $selector
 * @param OxygenSelectorCacheData[] $cacheStore
 * @return bool
 */
function hasSelectorChanged($selector, $cacheStore)
{
    $cachedData = getCachedSelectorData($selector['id'], $cacheStore);

    // If the selector is not cached, it has changed
    if (!$cachedData) return true;

    return $cachedData['hash'] !== getSelectorHash($selector);
}

/**
 * @return OxygenSelectorCacheData[]
 */
function getSelectorsCacheStore()
{
    $contentsOrError = get_file_contents_from_bucket(Consts::BREAKDANCE_FS_BUCKET_CSS, 'oxy-selectors.json');

    if ($contentsOrError && !is_fs_error($contentsOrError)) {
        /** @var OxygenSelectorCacheData[]|null $decodedJson */
        $decodedJson = json_decode($contentsOrError, true);

        if ($decodedJson) {
            return $decodedJson;
        }
    }

    return [];
}

/**
 * @param string $selectorId
 * @param OxygenSelectorCacheData[] $cacheStore
 * @return OxygenSelectorCacheData|null
 */
function getCachedSelectorData($selectorId, $cacheStore)
{
    $cachedData = array_filter(
        $cacheStore,
        fn($item) => $item['id'] === $selectorId
    );

    /** @var OxygenSelectorCacheData|null */
    $output = $cachedData ? reset($cachedData) : null;
    return $output;
}

/**
 * @param OxygenSelectorCacheData[] $cacheStore
 * @return string
 * @throws \Exception
 */
function saveSelectorsCacheStore($cacheStore)
{
    $basename = 'oxy-selectors.json';
    $writeErrorOrFilename = write_file_to_bucket(Consts::BREAKDANCE_FS_BUCKET_CSS, $basename, json_encode($cacheStore));

    if (!is_fs_error($writeErrorOrFilename)) {
        /** @var string */
        return $writeErrorOrFilename;
    } else {
        /** @var \WP_Error $writeErrorOrFilename */
        throw new \Exception(generate_error_msg_from_fs_wp_error($writeErrorOrFilename));
    }
}

/**
 * @param OxygenSelector $selector
 * @return string
 */
function getSelectorHash($selector)
{
    // Re-encode the JSON in a canonicalized form
    $canonicalJson = json_encode(
        $selector,
        JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
    );

    return hash("sha256", $canonicalJson);
}

/**
 * @param OxygenSelector $selector
 * @param string $cssTemplate
 * @param string[] $propertiesToWhiteListInFlatProps
 * @param Breakpoint[] $breakpoints
 * @param OxygenSelectorCacheData[] $cacheStore
 * @return array{0: string, 1: string[]}
 */
function getCssAndFontsForSelector(
    $selector,
    $cssTemplate,
    $propertiesToWhiteListInFlatProps,
    $breakpoints,
    $cacheStore
)
{
    $hasChanged = hasSelectorChanged($selector, $cacheStore);

    if (!$hasChanged) {
        $cachedData = getCachedSelectorData($selector['id'], $cacheStore);

        if ($cachedData) {
            // Process the fonts from the cached data as they are only
            // processed when the selector is generated via Twig rendering
            if (isset($cachedData['fonts'])) {
                foreach ($cachedData['fonts'] as $font) {
                    \Breakdance\Fonts\process_font($font);
                }
            }

            return [$cachedData['css'], $cachedData['fonts'] ?? []];
        }
    }

    $css = formatCss(
        getCss(
            $cssTemplate,
            ['selector' => $selector],
            $breakpoints,
            [],
            $propertiesToWhiteListInFlatProps,
        )
    );

    return [$css, getFontsForSelector($selector)];
}
