/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { DEFAULT_INPUT_BACKGROUND_OPACITY, DEFAULT_SELECTION_OPACITY, DEFAULT_BORDER_OPACITY, } from '../constants.js';
import tinygradient from 'tinygradient';
import tinycolor from 'tinycolor2';
// Define the set of Ink's named colors for quick lookup
export const INK_SUPPORTED_NAMES = new Set([
    'black',
    'red',
    'green',
    'yellow',
    'blue',
    'cyan',
    'magenta',
    'white',
    'gray',
    'grey',
    'blackbright',
    'redbright',
    'greenbright',
    'yellowbright',
    'bluebright',
    'cyanbright',
    'magentabright',
    'whitebright',
]);
// Use tinycolor's built-in names map for CSS colors, excluding ones Ink supports
export const CSS_NAME_TO_HEX_MAP = Object.fromEntries(Object.entries(tinycolor.names)
    .filter(([name]) => !INK_SUPPORTED_NAMES.has(name))
    .map(([name, hex]) => [name, `#${hex}`]));
// Mapping for ANSI bright colors that are not in tinycolor's standard CSS names
export const INK_NAME_TO_HEX_MAP = {
    blackbright: '#555555',
    redbright: '#ff5555',
    greenbright: '#55ff55',
    yellowbright: '#ffff55',
    bluebright: '#5555ff',
    magentabright: '#ff55ff',
    cyanbright: '#55ffff',
    whitebright: '#ffffff',
};
/**
 * Calculates the relative luminance of a color.
 * See https://www.w3.org/TR/WCAG20/#relativeluminancedef
 *
 * @param color Color string (hex or Ink-supported name)
 * @returns Luminance value (0-255)
 */
export function getLuminance(color) {
    const resolved = color.toLowerCase();
    const hex = INK_NAME_TO_HEX_MAP[resolved] || resolved;
    const colorObj = tinycolor(hex);
    if (!colorObj.isValid()) {
        return 0;
    }
    // tinycolor returns 0-1, we need 0-255
    return colorObj.getLuminance() * 255;
}
/**
 * Resolves a CSS color value (name or hex) into an Ink-compatible color string.
 * @param colorValue The raw color string (e.g., 'blue', '#ff0000', 'darkkhaki').
 * @returns An Ink-compatible color string (hex or name), or undefined if not resolvable.
 */
export function resolveColor(colorValue) {
    const lowerColor = colorValue.toLowerCase();
    // 1. Check if it's already a hex code and valid
    if (lowerColor.startsWith('#')) {
        if (/^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/.test(colorValue)) {
            return lowerColor;
        }
        else {
            return undefined;
        }
    }
    // Handle hex codes without #
    if (/^[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/.test(colorValue)) {
        return `#${lowerColor}`;
    }
    // 2. Check if it's an Ink supported name (lowercase)
    if (INK_SUPPORTED_NAMES.has(lowerColor)) {
        return lowerColor; // Use Ink name directly
    }
    // 3. Check if it's a known CSS name we can map to hex
    // We can't import CSS_NAME_TO_HEX_MAP here due to circular deps,
    // but we can use tinycolor directly for named colors.
    const colorObj = tinycolor(lowerColor);
    if (colorObj.isValid()) {
        return colorObj.toHexString();
    }
    // 4. Could not resolve
    return undefined;
}
export function interpolateColor(color1, color2, factor) {
    if (factor <= 0 && color1) {
        return color1;
    }
    if (factor >= 1 && color2) {
        return color2;
    }
    if (!color1 || !color2) {
        return '';
    }
    try {
        const gradient = tinygradient(color1, color2);
        const color = gradient.rgbAt(factor);
        return color.toHexString();
    }
    catch (_e) {
        return color1;
    }
}
export function getThemeTypeFromBackgroundColor(backgroundColor) {
    if (!backgroundColor) {
        return undefined;
    }
    const resolvedColor = resolveColor(backgroundColor);
    if (!resolvedColor) {
        return undefined;
    }
    const luminance = getLuminance(resolvedColor);
    return luminance > 128 ? 'light' : 'dark';
}
export const lightTheme = {
    type: 'light',
    Background: '#FFFFFF',
    Foreground: '#000000',
    LightBlue: '#005FAF',
    AccentBlue: '#005FAF',
    AccentPurple: '#5F00FF',
    AccentCyan: '#005F87',
    AccentGreen: '#005F00',
    AccentYellow: '#875F00',
    AccentRed: '#AF0000',
    DiffAdded: '#D7FFD7',
    DiffRemoved: '#FFD7D7',
    Comment: '#008700',
    Gray: '#5F5F5F',
    DarkGray: '#5F5F5F',
    InputBackground: '#E4E4E4',
    MessageBackground: '#FAFAFA',
    FocusBackground: '#D7FFD7',
    GradientColors: ['#4796E4', '#847ACE', '#C3677F'],
};
export const darkTheme = {
    type: 'dark',
    Background: '#000000',
    Foreground: '#FFFFFF',
    LightBlue: '#AFD7D7',
    AccentBlue: '#87AFFF',
    AccentPurple: '#D7AFFF',
    AccentCyan: '#87D7D7',
    AccentGreen: '#D7FFD7',
    AccentYellow: '#FFFFAF',
    AccentRed: '#FF87AF',
    DiffAdded: '#005F00',
    DiffRemoved: '#5F0000',
    Comment: '#AFAFAF',
    Gray: '#AFAFAF',
    DarkGray: '#878787',
    InputBackground: '#5F5F5F',
    MessageBackground: '#5F5F5F',
    FocusBackground: '#005F00',
    GradientColors: ['#4796E4', '#847ACE', '#C3677F'],
};
export const ansiTheme = {
    type: 'ansi',
    Background: 'black',
    Foreground: '',
    LightBlue: 'blue',
    AccentBlue: 'blue',
    AccentPurple: 'magenta',
    AccentCyan: 'cyan',
    AccentGreen: 'green',
    AccentYellow: 'yellow',
    AccentRed: 'red',
    DiffAdded: 'green',
    DiffRemoved: 'red',
    Comment: 'gray',
    Gray: 'gray',
    DarkGray: 'gray',
    InputBackground: 'black',
    MessageBackground: 'black',
    FocusBackground: 'black',
};
export class Theme {
    name;
    type;
    colors;
    /**
     * The default foreground color for text when no specific highlight rule applies.
     * This is an Ink-compatible color string (hex or name).
     */
    defaultColor;
    /**
     * Stores the mapping from highlight.js class names (e.g., 'hljs-keyword')
     * to Ink-compatible color strings (hex or name).
     */
    _colorMap;
    semanticColors;
    /**
     * Creates a new Theme instance.
     * @param name The name of the theme.
     * @param rawMappings The raw CSSProperties mappings from a react-syntax-highlighter theme object.
     */
    constructor(name, type, rawMappings, colors, semanticColors) {
        this.name = name;
        this.type = type;
        this.colors = colors;
        this.semanticColors = semanticColors ?? {
            text: {
                primary: this.colors.Foreground,
                secondary: this.colors.Gray,
                link: this.colors.AccentBlue,
                accent: this.colors.AccentPurple,
                response: this.colors.Foreground,
            },
            background: {
                primary: this.colors.Background,
                message: this.colors.MessageBackground ??
                    interpolateColor(this.colors.Background, this.colors.Gray, DEFAULT_INPUT_BACKGROUND_OPACITY),
                input: this.colors.InputBackground ??
                    interpolateColor(this.colors.Background, this.colors.Gray, DEFAULT_INPUT_BACKGROUND_OPACITY),
                focus: this.colors.FocusBackground ??
                    interpolateColor(this.colors.Background, this.colors.FocusColor ?? this.colors.AccentGreen, DEFAULT_SELECTION_OPACITY),
                diff: {
                    added: this.colors.DiffAdded,
                    removed: this.colors.DiffRemoved,
                },
            },
            border: {
                default: this.colors.DarkGray,
            },
            ui: {
                comment: this.colors.Gray,
                symbol: this.colors.AccentCyan,
                active: this.colors.AccentBlue,
                dark: this.colors.DarkGray,
                focus: this.colors.FocusColor ?? this.colors.AccentGreen,
                gradient: this.colors.GradientColors,
            },
            status: {
                error: this.colors.AccentRed,
                success: this.colors.AccentGreen,
                warning: this.colors.AccentYellow,
            },
        };
        this._colorMap = Object.freeze(this._buildColorMap(rawMappings)); // Build and freeze the map
        // Determine the default foreground color
        const rawDefaultColor = rawMappings['hljs']?.color;
        this.defaultColor =
            (rawDefaultColor ? Theme._resolveColor(rawDefaultColor) : undefined) ??
                ''; // Default to empty string if not found or resolvable
    }
    /**
     * Gets the Ink-compatible color string for a given highlight.js class name.
     * @param hljsClass The highlight.js class name (e.g., 'hljs-keyword', 'hljs-string').
     * @returns The corresponding Ink color string (hex or name) if it exists.
     */
    getInkColor(hljsClass) {
        return this._colorMap[hljsClass];
    }
    /**
     * Resolves a CSS color value (name or hex) into an Ink-compatible color string.
     * @param colorValue The raw color string (e.g., 'blue', '#ff0000', 'darkkhaki').
     * @returns An Ink-compatible color string (hex or name), or undefined if not resolvable.
     */
    static _resolveColor(colorValue) {
        return resolveColor(colorValue);
    }
    /**
     * Builds the internal map from highlight.js class names to Ink-compatible color strings.
     * This method is protected and primarily intended for use by the constructor.
     * @param hljsTheme The raw CSSProperties mappings from a react-syntax-highlighter theme object.
     * @returns An Ink-compatible theme map (Record<string, string>).
     */
    _buildColorMap(hljsTheme) {
        const inkTheme = {};
        for (const key in hljsTheme) {
            // Ensure the key starts with 'hljs-' or is 'hljs' for the base style
            if (!key.startsWith('hljs-') && key !== 'hljs') {
                continue; // Skip keys not related to highlighting classes
            }
            const style = hljsTheme[key];
            if (style?.color) {
                const resolvedColor = Theme._resolveColor(style.color);
                if (resolvedColor !== undefined) {
                    // Use the original key from the hljsTheme (e.g., 'hljs-keyword')
                    inkTheme[key] = resolvedColor;
                }
                // If color is not resolvable, it's omitted from the map,
                // this enables falling back to the default foreground color.
            }
            // We currently only care about the 'color' property for Ink rendering.
            // Other properties like background, fontStyle, etc., are ignored.
        }
        return inkTheme;
    }
}
/**
 * Creates a Theme instance from a custom theme configuration.
 * @param customTheme The custom theme configuration.
 * @returns A new Theme instance.
 */
export function createCustomTheme(customTheme) {
    const colors = {
        type: 'custom',
        Background: customTheme.background?.primary ?? customTheme.Background ?? '',
        Foreground: customTheme.text?.primary ?? customTheme.Foreground ?? '',
        LightBlue: customTheme.text?.link ?? customTheme.LightBlue ?? '',
        AccentBlue: customTheme.text?.link ?? customTheme.AccentBlue ?? '',
        AccentPurple: customTheme.text?.accent ?? customTheme.AccentPurple ?? '',
        AccentCyan: customTheme.text?.link ?? customTheme.AccentCyan ?? '',
        AccentGreen: customTheme.status?.success ?? customTheme.AccentGreen ?? '',
        AccentYellow: customTheme.status?.warning ?? customTheme.AccentYellow ?? '',
        AccentRed: customTheme.status?.error ?? customTheme.AccentRed ?? '',
        DiffAdded: customTheme.background?.diff?.added ?? customTheme.DiffAdded ?? '',
        DiffRemoved: customTheme.background?.diff?.removed ?? customTheme.DiffRemoved ?? '',
        Comment: customTheme.ui?.comment ?? customTheme.Comment ?? '',
        Gray: customTheme.text?.secondary ?? customTheme.Gray ?? '',
        DarkGray: customTheme.DarkGray ??
            interpolateColor(customTheme.background?.primary ?? customTheme.Background ?? '', customTheme.text?.secondary ?? customTheme.Gray ?? '', DEFAULT_BORDER_OPACITY),
        InputBackground: interpolateColor(customTheme.background?.primary ?? customTheme.Background ?? '', customTheme.text?.secondary ?? customTheme.Gray ?? '', DEFAULT_INPUT_BACKGROUND_OPACITY),
        MessageBackground: interpolateColor(customTheme.background?.primary ?? customTheme.Background ?? '', customTheme.text?.secondary ?? customTheme.Gray ?? '', DEFAULT_INPUT_BACKGROUND_OPACITY),
        FocusBackground: interpolateColor(customTheme.background?.primary ?? customTheme.Background ?? '', customTheme.status?.success ?? customTheme.AccentGreen ?? '#3CA84B', // Fallback to a default green if not found
        DEFAULT_SELECTION_OPACITY),
        FocusColor: customTheme.ui?.focus ?? customTheme.AccentGreen,
        GradientColors: customTheme.ui?.gradient ?? customTheme.GradientColors,
    };
    // Generate CSS properties mappings based on the custom theme colors
    const rawMappings = {
        hljs: {
            display: 'block',
            overflowX: 'auto',
            padding: '0.5em',
            background: colors.Background,
            color: colors.Foreground,
        },
        'hljs-keyword': {
            color: colors.AccentBlue,
        },
        'hljs-literal': {
            color: colors.AccentBlue,
        },
        'hljs-symbol': {
            color: colors.AccentBlue,
        },
        'hljs-name': {
            color: colors.AccentBlue,
        },
        'hljs-link': {
            color: colors.AccentBlue,
            textDecoration: 'underline',
        },
        'hljs-built_in': {
            color: colors.AccentCyan,
        },
        'hljs-type': {
            color: colors.AccentCyan,
        },
        'hljs-number': {
            color: colors.AccentGreen,
        },
        'hljs-class': {
            color: colors.AccentGreen,
        },
        'hljs-string': {
            color: colors.AccentYellow,
        },
        'hljs-meta-string': {
            color: colors.AccentYellow,
        },
        'hljs-regexp': {
            color: colors.AccentRed,
        },
        'hljs-template-tag': {
            color: colors.AccentRed,
        },
        'hljs-subst': {
            color: colors.Foreground,
        },
        'hljs-function': {
            color: colors.Foreground,
        },
        'hljs-title': {
            color: colors.Foreground,
        },
        'hljs-params': {
            color: colors.Foreground,
        },
        'hljs-formula': {
            color: colors.Foreground,
        },
        'hljs-comment': {
            color: colors.Comment,
            fontStyle: 'italic',
        },
        'hljs-quote': {
            color: colors.Comment,
            fontStyle: 'italic',
        },
        'hljs-doctag': {
            color: colors.Comment,
        },
        'hljs-meta': {
            color: colors.Gray,
        },
        'hljs-meta-keyword': {
            color: colors.Gray,
        },
        'hljs-tag': {
            color: colors.Gray,
        },
        'hljs-variable': {
            color: colors.AccentPurple,
        },
        'hljs-template-variable': {
            color: colors.AccentPurple,
        },
        'hljs-attr': {
            color: colors.LightBlue,
        },
        'hljs-attribute': {
            color: colors.LightBlue,
        },
        'hljs-builtin-name': {
            color: colors.LightBlue,
        },
        'hljs-section': {
            color: colors.AccentYellow,
        },
        'hljs-emphasis': {
            fontStyle: 'italic',
        },
        'hljs-strong': {
            fontWeight: 'bold',
        },
        'hljs-bullet': {
            color: colors.AccentYellow,
        },
        'hljs-selector-tag': {
            color: colors.AccentYellow,
        },
        'hljs-selector-id': {
            color: colors.AccentYellow,
        },
        'hljs-selector-class': {
            color: colors.AccentYellow,
        },
        'hljs-selector-attr': {
            color: colors.AccentYellow,
        },
        'hljs-selector-pseudo': {
            color: colors.AccentYellow,
        },
        'hljs-addition': {
            backgroundColor: colors.AccentGreen,
            display: 'inline-block',
            width: '100%',
        },
        'hljs-deletion': {
            backgroundColor: colors.AccentRed,
            display: 'inline-block',
            width: '100%',
        },
    };
    const semanticColors = {
        text: {
            primary: customTheme.text?.primary ?? colors.Foreground,
            secondary: customTheme.text?.secondary ?? colors.Gray,
            link: customTheme.text?.link ?? colors.AccentBlue,
            accent: customTheme.text?.accent ?? colors.AccentPurple,
            response: customTheme.text?.response ??
                customTheme.text?.primary ??
                colors.Foreground,
        },
        background: {
            primary: customTheme.background?.primary ?? colors.Background,
            message: colors.MessageBackground,
            input: colors.InputBackground,
            focus: colors.FocusBackground,
            diff: {
                added: customTheme.background?.diff?.added ?? colors.DiffAdded,
                removed: customTheme.background?.diff?.removed ?? colors.DiffRemoved,
            },
        },
        border: {
            default: colors.DarkGray,
        },
        ui: {
            comment: customTheme.ui?.comment ?? colors.Comment,
            symbol: customTheme.ui?.symbol ?? colors.Gray,
            active: customTheme.ui?.active ?? colors.AccentBlue,
            dark: colors.DarkGray,
            focus: colors.FocusColor ?? colors.AccentGreen,
            gradient: customTheme.ui?.gradient ?? colors.GradientColors,
        },
        status: {
            error: customTheme.status?.error ?? colors.AccentRed,
            success: customTheme.status?.success ?? colors.AccentGreen,
            warning: customTheme.status?.warning ?? colors.AccentYellow,
        },
    };
    return new Theme(customTheme.name, 'custom', rawMappings, colors, semanticColors);
}
/**
 * Validates a custom theme configuration.
 * @param customTheme The custom theme to validate.
 * @returns An object with isValid boolean and error message if invalid.
 */
export function validateCustomTheme(customTheme) {
    // Since all fields are optional, we only need to validate the name.
    if (customTheme.name && !isValidThemeName(customTheme.name)) {
        return {
            isValid: false,
            error: `Invalid theme name: ${customTheme.name}`,
        };
    }
    return {
        isValid: true,
    };
}
/**
 * Checks if a theme name is valid.
 * @param name The theme name to validate.
 * @returns True if the theme name is valid.
 */
function isValidThemeName(name) {
    // Theme name should be non-empty and not contain invalid characters
    return name.trim().length > 0 && name.trim().length <= 50;
}
/**
 * Picks a default theme name based on terminal background color.
 * It first tries to find a theme with an exact background color match.
 * If no match is found, it falls back to a light or dark theme based on the
 * luminance of the background color.
 * @param terminalBackground The hex color string of the terminal background.
 * @param availableThemes A list of available themes to search through.
 * @param defaultDarkThemeName The name of the fallback dark theme.
 * @param defaultLightThemeName The name of the fallback light theme.
 * @returns The name of the chosen theme.
 */
export function pickDefaultThemeName(terminalBackground, availableThemes, defaultDarkThemeName, defaultLightThemeName) {
    if (terminalBackground) {
        const lowerTerminalBackground = terminalBackground.toLowerCase();
        for (const theme of availableThemes) {
            if (!theme.colors.Background)
                continue;
            // resolveColor can return undefined
            const themeBg = resolveColor(theme.colors.Background)?.toLowerCase();
            if (themeBg === lowerTerminalBackground) {
                return theme.name;
            }
        }
    }
    const themeType = getThemeTypeFromBackgroundColor(terminalBackground);
    if (themeType === 'light') {
        return defaultLightThemeName;
    }
    return defaultDarkThemeName;
}
//# sourceMappingURL=theme.js.map