// #region Imports
import { PureEventEmitter } from '@breadstone/mosaik-elements';
import { CssLength, ThemePalette } from '@breadstone/mosaik-themes';
import { GlobalStyleSheet } from '../Dom/StyleSheet';
import { THEME_SERVICE_DEFAULT_CONFIG } from './IThemeService';
// #endregion
/**
 * Service for managing theme CSS variables throughout the application.
 * Provides methods to apply theme properties globally to the document.
 *
 * Uses a singleton pattern - access via `ThemeService.instance`.
 *
 * @example
 * ```typescript
 * // Configure service globally before use (optional)
 * ThemeService.configure({
 *     styleId: 'my-theme-vars',
 *     autoInitialize: true
 * });
 *
 * // Use the singleton instance
 * ThemeService.instance.applyTheme(myTheme);
 *
 * // Or apply individual parts (theme name is derived from stored state)
 * ThemeService.instance.applyScheme(theme.scheme);       // global scope
 * ThemeService.instance.applyPalette(theme.palette);     // global scope
 * ThemeService.instance.applyLayout(theme.layout);       // global scope
 * ThemeService.instance.applyFontFamily('Arial');        // global scope
 *
 * // Subscribe to theme changes
 * ThemeService.instance.themeChanged.subscribe((detail) => {
 *     console.log('Theme changed:', detail.theme);
 * });
 * ```
 *
 * @public
 */
export class ThemeService {
    // #region Fields
    static _instance = null;
    static _config = { ...THEME_SERVICE_DEFAULT_CONFIG };
    _globalStyleSheet;
    _providers;
    _themeStates;
    _themeChanged;
    _schemeChanged;
    _paletteChanged;
    _initialized;
    // #endregion
    // #region Ctor
    /**
     * Constructs a new instance of the `ThemeService` class.
     * Use `ThemeService.instance` to access the singleton instance.
     *
     * @private
     */
    constructor() {
        const config = ThemeService._config;
        this._globalStyleSheet = new GlobalStyleSheet(config.styleId ?? THEME_SERVICE_DEFAULT_CONFIG.styleId);
        this._providers = [];
        this._themeStates = new Map();
        this._themeChanged = new PureEventEmitter();
        this._schemeChanged = new PureEventEmitter();
        this._paletteChanged = new PureEventEmitter();
        this._initialized = false;
        // Auto-initialize if enabled
        if (config.autoInitialize ?? THEME_SERVICE_DEFAULT_CONFIG.autoInitialize) {
            this.initialize();
        }
    }
    // #endregion
    // #region Properties
    /**
     * Returns the singleton instance of the ThemeService.
     * Creates the instance on first access using the current configuration.
     *
     * @public
     * @static
     * @readonly
     */
    static get instance() {
        ThemeService._instance ??= new ThemeService();
        return ThemeService._instance;
    }
    /**
     * Gets whether the service has been initialized.
     *
     * @public
     * @readonly
     */
    get isInitialized() {
        return this._initialized;
    }
    /**
     * Event emitter that fires when the theme changes.
     *
     * @public
     * @readonly
     */
    get themeChanged() {
        return this._themeChanged;
    }
    /**
     * Event emitter that fires when the scheme changes.
     *
     * @public
     * @readonly
     */
    get schemeChanged() {
        return this._schemeChanged;
    }
    /**
     * Event emitter that fires when the palette changes.
     *
     * @public
     * @readonly
     */
    get paletteChanged() {
        return this._paletteChanged;
    }
    // #endregion
    // #region Methods
    /**
     * Configures the theme service globally.
     * Must be called before accessing the singleton instance.
     *
     * @public
     * @static
     * @param config Configuration options for the theme service.
     * @returns The ThemeService class for chaining.
     * @throws Error if called after the instance has already been created.
     *
     * @example
     * ```typescript
     * ThemeService.configure({
     *     styleId: 'my-custom-theme-vars',
     *     autoInitialize: false
     * });
     * ```
     */
    static configure(config) {
        if (ThemeService._instance !== null) {
            throw new Error('ThemeService.configure() must be called before accessing ThemeService.instance. ' +
                'The service has already been instantiated.');
        }
        ThemeService._config = {
            ...THEME_SERVICE_DEFAULT_CONFIG,
            ...config
        };
        return ThemeService;
    }
    /**
     * Resets the singleton instance.
     * Useful for testing or when the service needs to be re-configured.
     *
     * @public
     * @static
     */
    static reset() {
        if (ThemeService._instance) {
            ThemeService._instance.dispose();
            ThemeService._instance = null;
        }
        ThemeService._config = { ...THEME_SERVICE_DEFAULT_CONFIG };
    }
    /**
     * Initializes the service.
     * Must be called before applying any theme properties (unless autoInitialize is enabled).
     *
     * @public
     */
    initialize() {
        if (this._initialized) {
            return;
        }
        this._globalStyleSheet.initialize();
        this._initialized = true;
        const config = ThemeService._config;
        // Apply default theme mode to document
        const themeMode = config.defaultThemeMode ?? THEME_SERVICE_DEFAULT_CONFIG.defaultThemeMode ?? 'light';
        document.documentElement.setAttribute('theme-mode', themeMode);
        // Apply default theme if configured
        if (config.defaultTheme) {
            document.documentElement.setAttribute('theme', config.defaultTheme.name);
            this.applyTheme(config.defaultTheme);
        }
    }
    /**
     * Disposes of the service and cleans up resources.
     *
     * @public
     */
    dispose() {
        this._globalStyleSheet.dispose();
        this._providers.length = 0;
        this._themeStates.clear();
        this._initialized = false;
    }
    /**
     * Registers a theme provider with the service.
     * If a theme state already exists for the provider's name, the provider
     * will be synchronized with that state. Otherwise, if the provider has a theme,
     * it will be used to initialize the state for that name.
     *
     * @public
     * @param provider The theme provider to register.
     */
    registerProvider(provider) {
        if (!this._providers.includes(provider)) {
            this._providers.push(provider);
            const name = provider.name;
            const existingState = this._themeStates.get(name);
            if (existingState) {
                // Sync provider with existing state
                provider.applyTheme(existingState);
            }
            else if (provider.theme) {
                // Initialize state from provider's theme (deep clone)
                this._themeStates.set(name, this.cloneTheme(provider.theme));
            }
        }
    }
    /**
     * Unregisters a theme provider from the service.
     *
     * @public
     * @param provider The theme provider to unregister.
     */
    unregisterProvider(provider) {
        const index = this._providers.indexOf(provider);
        if (index > -1) {
            this._providers.splice(index, 1);
        }
    }
    /**
     * Checks whether a theme exists for the specified name.
     * For the global scope (no name), this also returns true if a default theme is configured.
     *
     * @public
     * @param name Optional name for scoped themes. Use `null` or omit for global scope.
     * @returns `true` if a theme is set for the specified scope, `false` otherwise.
     */
    hasTheme(name) {
        const normalizedName = name === undefined ? null : name;
        // Check state map first
        if (this._themeStates.has(normalizedName)) {
            return true;
        }
        // Check providers for this scope
        const provider = this._providers.find((p) => p.name === normalizedName && p.theme !== null);
        if (provider) {
            return true;
        }
        // For global scope, check default theme
        if (normalizedName === null && ThemeService._config.defaultTheme) {
            return true;
        }
        return false;
    }
    /**
     * Gets the current theme for the specified name.
     * If no name is provided (undefined), returns the theme for the global/default scope (null key).
     *
     * The method uses the following fallback chain:
     * 1. Theme state from `_themeStates` map
     * 2. Theme from the first matching provider
     * 3. Default theme from configuration (global scope only)
     *
     * @public
     * @param name Optional name for scoped themes. Use `null` or omit for global scope.
     * @returns The current theme.
     * @throws Error if no theme is set for the specified scope and no default theme is configured.
     */
    getTheme(name) {
        // Normalize undefined to null for the global scope
        const normalizedName = name === undefined ? null : name;
        // 1. Check state map first
        const state = this._themeStates.get(normalizedName);
        if (state) {
            return state;
        }
        // 2. Check providers for this scope
        const provider = this._providers.find((p) => p.name === normalizedName && p.theme !== null);
        if (provider?.theme) {
            return provider.theme;
        }
        // 3. For global scope, use default theme as fallback
        if (normalizedName === null && ThemeService._config.defaultTheme) {
            return ThemeService._config.defaultTheme;
        }
        throw new Error(`[ThemeService] No theme set for name: ${normalizedName ?? 'global'}.`);
    }
    /**
     * Gets the current theme name from the document element.
     *
     * @public
     * @returns The current theme name or `null` if not set.
     */
    getDocumentTheme() {
        return document.documentElement.getAttribute('theme');
    }
    /**
     * Gets the current theme mode from the document element.
     *
     * @public
     * @returns The current theme mode or `null` if not set.
     */
    getDocumentThemeMode() {
        return document.documentElement.getAttribute('theme-mode');
    }
    /**
     * Applies the complete theme.
     * If name is provided, only providers with that name are notified.
     * If name is not provided (undefined), only providers without a name (null) are notified.
     *
     * @public
     * @param theme The theme to apply.
     * @param name Optional name for scoped application.
     * @param options Optional settings to control notification behavior.
     */
    applyTheme(theme, name, options) {
        this.ensureInitialized();
        // Normalize undefined to null for the global scope
        const normalizedName = name === undefined ? null : name;
        const notifyProviders = options?.notifyProviders ?? true;
        const themeName = theme.name;
        const currentMode = this.getCurrentThemeMode();
        const fontFamily = theme.fontFamily;
        const providers = this.getProvidersForName(normalizedName);
        // Determine global application:
        // - If explicitly set via options, use that value
        // - Otherwise, apply globally for global scope (null) or if any provider has global=true
        const shouldApplyGlobally = options?.applyGlobally ?? (normalizedName === null || providers.some((p) => p.global));
        // Apply scheme
        Object.entries(theme.scheme).forEach(([mode, value]) => {
            Object.entries(value).forEach(([property, cssValue]) => {
                const kebabProperty = this.toKebabCase(property);
                this.setCssVariableWithProviders(`--${themeName}-scheme-${mode}-${kebabProperty}`, cssValue, providers, shouldApplyGlobally);
                if (mode === currentMode) {
                    this.setCssVariableWithProviders(`--${themeName}-scheme-${kebabProperty}`, cssValue, providers, shouldApplyGlobally);
                    this.setCssVariableWithProviders(`--mosaik-scheme-${kebabProperty}`, `var(--${themeName}-scheme-${kebabProperty}, ${cssValue})`, providers, shouldApplyGlobally);
                }
            });
        });
        // Apply palette
        Object.entries(theme.palette).forEach(([mode, modeValues]) => {
            Object.entries(modeValues).forEach(([paletteName, paletteValues]) => {
                if (!ThemePalette.isThemePalette(paletteValues)) {
                    this.setCssVariableWithProviders(`--${themeName}-color-${mode}-${paletteName}`, paletteValues, providers, shouldApplyGlobally);
                    if (mode === currentMode) {
                        this.setCssVariableWithProviders(`--${themeName}-color-${paletteName}`, paletteValues, providers, shouldApplyGlobally);
                        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
                        this.setCssVariableWithProviders(`--mosaik-color-${paletteName}`, `var(--${themeName}-color-${paletteName}, ${paletteValues})`, providers, shouldApplyGlobally);
                    }
                    return;
                }
                Object.entries(paletteValues).forEach(([shade, colorValue]) => {
                    const kebabShade = this.toKebabCase(shade);
                    this.setCssVariableWithProviders(`--${themeName}-color-${mode}-${paletteName}-${kebabShade}`, colorValue, providers, shouldApplyGlobally);
                    if (mode === currentMode) {
                        this.setCssVariableWithProviders(`--${themeName}-color-${paletteName}-${kebabShade}`, colorValue, providers, shouldApplyGlobally);
                        this.setCssVariableWithProviders(`--mosaik-color-${paletteName}-${kebabShade}`, `var(--${themeName}-color-${paletteName}-${kebabShade}, ${colorValue})`, providers, shouldApplyGlobally);
                    }
                });
            });
        });
        // Apply font family
        this.setCssVariableWithProviders(`--${themeName}-font-family`, fontFamily, providers, shouldApplyGlobally);
        this.setCssVariableWithProviders('--mosaik-font-family', `var(--${themeName}-font-family, ${fontFamily})`, providers, shouldApplyGlobally);
        Object.keys(theme.typography).forEach((typographyName) => {
            this.setCssVariableWithProviders(`--${themeName}-typography-${typographyName}-font-family`, fontFamily, providers, shouldApplyGlobally);
            this.setCssVariableWithProviders(`--mosaik-typography-${typographyName}-font-family`, fontFamily, providers, shouldApplyGlobally);
        });
        // Apply typography
        Object.entries(theme.typography).forEach(([typeName, value]) => {
            Object.entries(value).forEach(([property, cssValue]) => {
                const kebabProperty = this.toKebabCase(property);
                this.setCssVariableWithProviders(`--${themeName}-typography-${typeName}-${kebabProperty}`, cssValue, providers, shouldApplyGlobally);
                this.setCssVariableWithProviders(`--mosaik-typography-${typeName}-${kebabProperty}`, `var(--${themeName}-typography-${typeName}-${kebabProperty}, ${cssValue})`, providers, shouldApplyGlobally);
            });
            const shorthand = `${value.fontWeight} ${value.fontSize}/${value.lineHeight} ${fontFamily}`;
            this.setCssVariableWithProviders(`--${themeName}-typography-${typeName}`, shorthand, providers, shouldApplyGlobally);
            this.setCssVariableWithProviders(`--mosaik-typography-${typeName}`, `var(--${themeName}-typography-${typeName}, ${shorthand})`, providers, shouldApplyGlobally);
        });
        // Apply layout
        Object.entries(theme.layout).forEach(([key, value]) => {
            const normalizedValue = this.normalizeCssLength(value);
            const kebabKey = this.toKebabCase(key);
            this.setCssVariableWithProviders(`--${themeName}-layout-${kebabKey}`, normalizedValue, providers, shouldApplyGlobally);
            this.setCssVariableWithProviders(`--mosaik-layout-${kebabKey}`, `var(--${themeName}-layout-${kebabKey}, ${normalizedValue})`, providers, shouldApplyGlobally);
        });
        // Apply elevation
        Object.entries(theme.elevation).forEach(([level, value]) => {
            Object.entries(value).forEach(([property, cssValue]) => {
                const kebabProperty = this.toKebabCase(property);
                this.setCssVariableWithProviders(`--${themeName}-elevation-${level}-${kebabProperty}`, cssValue, providers, shouldApplyGlobally);
                this.setCssVariableWithProviders(`--mosaik-elevation-${level}-${kebabProperty}`, `var(--${themeName}-elevation-${level}-${kebabProperty}, ${cssValue})`, providers, shouldApplyGlobally);
            });
        });
        // Store the theme state (deep clone to prevent mutations)
        this._themeStates.set(normalizedName, this.cloneTheme(theme));
        // Notify providers with matching name (only update their internal state, CSS is already set)
        if (notifyProviders) {
            providers.forEach((provider) => {
                provider.applyTheme(theme);
            });
        }
        this._themeChanged.emit({
            theme,
            name: normalizedName
        });
    }
    /**
     * Applies the scheme (light/dark mode color roles) from the theme.
     * Uses the theme name from the stored state for the specified scope.
     *
     * @public
     * @param scheme The scheme to apply.
     * @param name Optional name for scoped application.
     */
    applyScheme(scheme, name) {
        this.ensureInitialized();
        const normalizedName = name === undefined ? null : name;
        const themeName = this.getThemeNameForScope(normalizedName);
        const currentMode = this.getCurrentThemeMode();
        Object.entries(scheme).forEach(([mode, value]) => {
            Object.entries(value).forEach(([property, cssValue]) => {
                const kebabProperty = this.toKebabCase(property);
                this.setCssVariable(`--${themeName}-scheme-${mode}-${kebabProperty}`, cssValue);
                if (mode === currentMode) {
                    this.setCssVariable(`--${themeName}-scheme-${kebabProperty}`, cssValue);
                    this.setCssVariable(`--mosaik-scheme-${kebabProperty}`, `var(--${themeName}-scheme-${kebabProperty}, ${cssValue})`);
                }
            });
        });
        this._schemeChanged.emit({
            scheme,
            name: normalizedName
        });
    }
    /**
     * Applies the color palette from the theme.
     * Uses the theme name from the stored state for the specified scope.
     *
     * @public
     * @param palette The palette to apply.
     * @param name Optional name for scoped application.
     */
    applyPalette(palette, name) {
        this.ensureInitialized();
        const normalizedName = name === undefined ? null : name;
        const themeName = this.getThemeNameForScope(normalizedName);
        const currentMode = this.getCurrentThemeMode();
        Object.entries(palette).forEach(([mode, modeValues]) => {
            Object.entries(modeValues).forEach(([paletteName, paletteValues]) => {
                if (!ThemePalette.isThemePalette(paletteValues)) {
                    this.setCssVariable(`--${themeName}-color-${mode}-${paletteName}`, paletteValues);
                    if (mode === currentMode) {
                        this.setCssVariable(`--${themeName}-color-${paletteName}`, paletteValues);
                        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
                        this.setCssVariable(`--mosaik-color-${paletteName}`, `var(--${themeName}-color-${paletteName}, ${paletteValues})`);
                    }
                    return;
                }
                Object.entries(paletteValues).forEach(([shade, colorValue]) => {
                    const kebabShade = this.toKebabCase(shade);
                    this.setCssVariable(`--${themeName}-color-${mode}-${paletteName}-${kebabShade}`, colorValue);
                    if (mode === currentMode) {
                        this.setCssVariable(`--${themeName}-color-${paletteName}-${kebabShade}`, colorValue);
                        this.setCssVariable(`--mosaik-color-${paletteName}-${kebabShade}`, `var(--${themeName}-color-${paletteName}-${kebabShade}, ${colorValue})`);
                    }
                });
            });
        });
        this._paletteChanged.emit({
            palette,
            name: normalizedName
        });
    }
    /**
     * Applies the font family from the theme.
     * Uses the theme name and typography keys from the stored state for the specified scope.
     *
     * @public
     * @param fontFamily The font family to apply.
     * @param name Optional name for scoped application.
     */
    applyFontFamily(fontFamily, name) {
        const normalizedName = name === undefined ? null : name;
        const state = this._themeStates.get(normalizedName);
        const themeName = state?.name ?? 'joy';
        const typographyKeys = state ? Object.keys(state.typography) : [];
        this.ensureInitialized();
        // Update the theme state
        if (state) {
            state.fontFamily = fontFamily;
        }
        // legacy variable style
        this.setCssVariable(`--${themeName}-font-family`, fontFamily);
        // new variable style
        this.setCssVariable('--mosaik-font-family', `var(--${themeName}-font-family, ${fontFamily})`);
        typographyKeys.forEach((typographyName) => {
            // legacy variable style
            this.setCssVariable(`--${themeName}-typography-${typographyName}-font-family`, fontFamily);
            // new variable style
            this.setCssVariable(`--mosaik-typography-${typographyName}-font-family`, fontFamily);
        });
    }
    /**
     * Applies the typography settings from the theme.
     * Uses the theme name and font family from the stored state for the specified scope.
     *
     * @public
     * @param typography The typography settings to apply.
     * @param name Optional name for scoped application.
     */
    applyTypography(typography, name) {
        const normalizedName = name === undefined ? null : name;
        const state = this._themeStates.get(normalizedName);
        const themeName = state?.name ?? 'joy';
        const fontFamily = state?.fontFamily ?? '';
        this.ensureInitialized();
        // Update the theme state
        if (state) {
            const currentTypography = state.typography;
            const updatedTypography = Object.assign({}, currentTypography, typography);
            state.typography = updatedTypography;
        }
        Object.entries(typography).forEach(([typoName, value]) => {
            Object.entries(value).forEach(([property, cssValue]) => {
                const kebabProperty = this.toKebabCase(property);
                // legacy variable style
                this.setCssVariable(`--${themeName}-typography-${typoName}-${kebabProperty}`, cssValue);
                // new variable style
                this.setCssVariable(`--mosaik-typography-${typoName}-${kebabProperty}`, `var(--${themeName}-typography-${typoName}-${kebabProperty}, ${cssValue})`);
            });
            const shorthand = `${value.fontWeight} ${value.fontSize}/${value.lineHeight} ${fontFamily}`;
            // legacy variable style
            this.setCssVariable(`--${themeName}-typography-${typoName}`, shorthand);
            // new variable style
            this.setCssVariable(`--mosaik-typography-${typoName}`, `var(--${themeName}-typography-${typoName}, ${shorthand})`);
        });
    }
    /**
     * Applies the layout settings from the theme.
     * Uses the theme name from the stored state for the specified scope.
     *
     * @public
     * @param layout The layout settings to apply.
     * @param name Optional name for scoped application.
     */
    applyLayout(layout, name) {
        const normalizedName = name === undefined ? null : name;
        const themeName = this.getThemeNameForScope(normalizedName);
        this.ensureInitialized();
        // Update the theme state
        const currentState = this._themeStates.get(normalizedName);
        if (currentState) {
            const currentLayout = currentState.layout;
            const updatedLayout = Object.assign({}, currentLayout, layout);
            // Update state
            currentState.layout = updatedLayout;
        }
        Object.entries(layout).forEach(([key, value]) => {
            const normalizedValue = this.normalizeCssLength(value);
            const kebabKey = this.toKebabCase(key);
            // legacy variable style
            this.setCssVariable(`--${themeName}-layout-${kebabKey}`, normalizedValue);
            // new variable style
            this.setCssVariable(`--mosaik-layout-${kebabKey}`, `var(--${themeName}-layout-${kebabKey}, ${normalizedValue})`);
        });
    }
    /**
     * Applies the elevation (shadow) settings from the theme.
     * Uses the theme name from the stored state for the specified scope.
     *
     * @public
     * @param elevation The elevation settings to apply.
     * @param name Optional name for scoped application.
     */
    applyElevation(elevation, name) {
        const normalizedName = name === undefined ? null : name;
        const themeName = this.getThemeNameForScope(normalizedName);
        this.ensureInitialized();
        // Update the theme state
        const currentState = this._themeStates.get(normalizedName);
        if (currentState) {
            const currentElevation = currentState.elevation;
            const updatedElevation = Object.assign({}, currentElevation, elevation);
            currentState.elevation = updatedElevation;
        }
        Object.entries(elevation).forEach(([level, value]) => {
            Object.entries(value).forEach(([property, cssValue]) => {
                const kebabProperty = this.toKebabCase(property);
                // legacy variable style
                this.setCssVariable(`--${themeName}-elevation-${level}-${kebabProperty}`, cssValue);
                // new variable style
                this.setCssVariable(`--mosaik-elevation-${level}-${kebabProperty}`, `var(--${themeName}-elevation-${level}-${kebabProperty}, ${cssValue})`);
            });
        });
    }
    /**
     * Gets the current theme mode from the document.
     *
     * @public
     * @returns The current theme mode ('light' or 'dark').
     */
    getCurrentThemeMode() {
        const mode = document.documentElement.getAttribute('theme-mode');
        return mode === 'dark' ? 'dark' : 'light';
    }
    /**
     * Replaces the scheme with the specified key.
     * This is useful when you want to replace a specific scheme in the theme.
     * If the name is provided, only providers with that name are notified.
     * If name is not provided (undefined), only providers without a name (null) are notified.
     *
     * @public
     * @param mode The theme mode to replace.
     * @param scheme The scheme color roles to replace.
     * @param name Optional name for scoped replacement.
     * @param options Optional settings to control notification behavior.
     */
    replaceScheme(mode, scheme, name, options) {
        this.ensureInitialized();
        // Normalize undefined to null for the global scope
        const normalizedName = name === undefined ? null : name;
        const notifyProviders = options?.notifyProviders ?? true;
        const currentMode = this.getCurrentThemeMode();
        const providers = this.getProvidersForName(normalizedName);
        // Determine global application:
        // - If explicitly set via options, use that value
        // - Otherwise, apply globally for global scope (null) or if any provider has global=true
        const shouldApplyGlobally = options?.applyGlobally ?? (normalizedName === null || providers.some((p) => p.global));
        // Update the theme state
        const currentState = this._themeStates.get(normalizedName);
        if (currentState) {
            const currentScheme = currentState.scheme;
            const currentModeValue = currentScheme[mode];
            const mergedMode = Object.assign({}, currentModeValue, scheme);
            const updatedScheme = Object.assign({}, currentScheme, { [mode]: mergedMode });
            // Update state
            currentState.scheme = updatedScheme;
            // Apply CSS variables
            const themeName = currentState.name;
            Object.entries(updatedScheme).forEach(([schemeMode, value]) => {
                Object.entries(value).forEach(([property, cssValue]) => {
                    const kebabProperty = this.toKebabCase(property);
                    this.setCssVariableWithProviders(`--${themeName}-scheme-${schemeMode}-${kebabProperty}`, cssValue, providers, shouldApplyGlobally);
                    if (schemeMode === currentMode) {
                        this.setCssVariableWithProviders(`--${themeName}-scheme-${kebabProperty}`, cssValue, providers, shouldApplyGlobally);
                        this.setCssVariableWithProviders(`--mosaik-scheme-${kebabProperty}`, `var(--${themeName}-scheme-${kebabProperty}, ${cssValue})`, providers, shouldApplyGlobally);
                    }
                });
            });
        }
        // Notify providers with matching name (only update their internal state)
        if (notifyProviders) {
            providers.forEach((provider) => {
                if (!provider.theme?.scheme) {
                    return;
                }
                const currentScheme = provider.theme.scheme;
                const currentModeValue = currentScheme[mode];
                const mergedMode = Object.assign({}, currentModeValue, scheme);
                const updatedScheme = Object.assign({}, currentScheme, { [mode]: mergedMode });
                provider.applyScheme(updatedScheme);
            });
        }
        this._schemeChanged.emit({
            mode,
            scheme,
            name: normalizedName
        });
    }
    /**
     * Replaces the palette with the specified key.
     * This is useful when you want to replace a specific palette in the theme.
     * If themeId is provided, only providers with that ID are notified.
     * If themeId is not provided (undefined), only providers without an ID (null) are notified.
     *
     * @public
     * @param mode The theme mode to replace.
     * @param semantic The semantic color roles to replace.
     * @param name Optional name for scoped replacement.
     * @param options Optional settings to control notification behavior.
     */
    replacePalette(mode, semantic, name, options) {
        this.ensureInitialized();
        // Normalize undefined to null for the global scope
        const normalizedName = name === undefined ? null : name;
        const notifyProviders = options?.notifyProviders ?? true;
        const currentMode = this.getCurrentThemeMode();
        const providers = this.getProvidersForName(normalizedName);
        // Determine global application:
        // - If explicitly set via options, use that value
        // - Otherwise, apply globally for global scope (null) or if any provider has global=true
        const shouldApplyGlobally = options?.applyGlobally ?? (normalizedName === null || providers.some((p) => p.global));
        // Update the theme state
        const currentState = this._themeStates.get(normalizedName);
        if (currentState) {
            const currentPalette = currentState.palette;
            const currentModeValue = currentPalette[mode];
            const mergedMode = Object.assign({}, currentModeValue, semantic);
            const updatedPalette = Object.assign({}, currentPalette, { [mode]: mergedMode });
            // Update state
            currentState.palette = updatedPalette;
            // Apply CSS variables
            const themeName = currentState.name;
            Object.entries(updatedPalette).forEach(([paletteMode, modeValues]) => {
                Object.entries(modeValues).forEach(([paletteName, paletteValues]) => {
                    if (!ThemePalette.isThemePalette(paletteValues)) {
                        this.setCssVariableWithProviders(`--${themeName}-color-${paletteMode}-${paletteName}`, paletteValues, providers, shouldApplyGlobally);
                        if (paletteMode === currentMode) {
                            this.setCssVariableWithProviders(`--${themeName}-color-${paletteName}`, paletteValues, providers, shouldApplyGlobally);
                            // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
                            this.setCssVariableWithProviders(`--mosaik-color-${paletteName}`, `var(--${themeName}-color-${paletteName}, ${paletteValues})`, providers, shouldApplyGlobally);
                        }
                        return;
                    }
                    Object.entries(paletteValues).forEach(([shade, colorValue]) => {
                        const kebabShade = this.toKebabCase(shade);
                        this.setCssVariableWithProviders(`--${themeName}-color-${paletteMode}-${paletteName}-${kebabShade}`, colorValue, providers, shouldApplyGlobally);
                        if (paletteMode === currentMode) {
                            this.setCssVariableWithProviders(`--${themeName}-color-${paletteName}-${kebabShade}`, colorValue, providers, shouldApplyGlobally);
                            this.setCssVariableWithProviders(`--mosaik-color-${paletteName}-${kebabShade}`, `var(--${themeName}-color-${paletteName}-${kebabShade}, ${colorValue})`, providers, shouldApplyGlobally);
                        }
                    });
                });
            });
        }
        // Notify providers with matching name (only update their internal state)
        if (notifyProviders) {
            providers.forEach((provider) => {
                if (!provider.theme?.palette) {
                    return;
                }
                const currentPalette = provider.theme.palette;
                const currentModeValue = currentPalette[mode];
                const mergedMode = Object.assign({}, currentModeValue, semantic);
                const updatedPalette = Object.assign({}, currentPalette, { [mode]: mergedMode });
                provider.applyPalette(updatedPalette);
            });
        }
        this._paletteChanged.emit({
            mode,
            palette: semantic,
            name: normalizedName
        });
    }
    /**
     * Sets a CSS variable.
     *
     * @private
     * @param property The CSS variable name (e.g., `--my-var`).
     * @param value The value to set.
     * @param options Optional settings for where to apply the variable.
     */
    setCssVariable(property, value, options) {
        const applyGlobally = options?.applyGlobally ?? true;
        const element = options?.element;
        if (element) {
            // eslint-disable-next-line no-restricted-syntax -- Setting CSS variables is the correct approach for theming
            element.style.setProperty(property, value);
        }
        if (applyGlobally) {
            this._globalStyleSheet.setVariable(property, value);
            // eslint-disable-next-line no-restricted-syntax -- Setting CSS variables is the correct approach for theming
            document.documentElement.style.setProperty(property, value);
        }
    }
    /**
     * Sets a CSS variable globally and on all provider elements.
     * For providers with global=false, only sets locally on the element.
     *
     * @private
     * @param property The CSS variable name.
     * @param value The value to set.
     * @param providers The providers to set the variable on.
     * @param applyGlobally Whether to apply the variable globally.
     */
    setCssVariableWithProviders(property, value, providers, applyGlobally) {
        // Apply globally if needed
        if (applyGlobally) {
            this._globalStyleSheet.setVariable(property, value);
            // eslint-disable-next-line no-restricted-syntax -- Setting CSS variables is the correct approach for theming
            document.documentElement.style.setProperty(property, value);
        }
        // Apply to all provider elements (they are HTMLElements)
        providers.forEach((provider) => {
            // eslint-disable-next-line no-restricted-syntax -- Setting CSS variables is the correct approach for theming
            provider.style.setProperty(property, value);
        });
    }
    /**
     * Converts a camelCase string to kebab-case.
     *
     * @private
     * @param value The value to convert.
     * @returns The kebab-case string.
     */
    toKebabCase(value) {
        return value
            .replace(/([a-z])([A-Z])/g, '$1-$2')
            .replace(/\s+/g, '-')
            .toLowerCase();
    }
    /**
     * Normalizes a CssLength value to a px string.
     *
     * @private
     * @param value The value to normalize.
     * @returns The normalized px string.
     */
    normalizeCssLength(value) {
        if (typeof value === 'number') {
            return `${value}px`;
        }
        if (value === 'auto') {
            throw new Error(`[ThemeService] The layout property '${value}' must be a px value.`);
        }
        if (CssLength.extractUnit(value) !== 'px') {
            throw new Error(`[ThemeService] The layout property '${value}' must be a px value.`);
        }
        return value;
    }
    /**
     * Ensures that the service has been initialized.
     *
     * @private
     */
    ensureInitialized() {
        if (!this._initialized) {
            throw new Error('[ThemeService] The service must be initialized before use. Call initialize() first or enable autoInitialize in the configuration.');
        }
    }
    /**
     * Gets all providers that match the specified theme ID.
     * If themeId is null, returns providers without an ID.
     *
     * @private
     * @param themeId The theme ID to filter by.
     * @returns Array of matching providers.
     */
    getProvidersForName(name) {
        return this._providers.filter((provider) => provider.name === name);
    }
    /**
     * Gets the theme name for CSS variable prefixing from the stored state.
     * Falls back to the default theme name from configuration if no state exists for the given scope.
     *
     * @private
     * @param name The scope name (null for global scope).
     * @returns The theme name for CSS variable prefixing.
     */
    getThemeNameForScope(name) {
        return this._themeStates.get(name)?.name ?? ThemeService._config.defaultTheme?.name ?? 'joy';
    }
    /**
     * Creates a deep clone of a theme object.
     *
     * @private
     * @param theme The theme to clone.
     * @returns A deep clone of the theme.
     */
    cloneTheme(theme) {
        return JSON.parse(JSON.stringify(theme));
    }
}
//# sourceMappingURL=ThemeService.js.map