# Source code for psd_tools.composite.blend

    """
    Blend mode implementations.

    This module implements Photoshop's blend modes for compositing layers. Blend modes
    determine how a layer's colors interact with the layers beneath it. Each function
    implements the mathematical formula for a specific blend mode.

    The blend functions operate on NumPy arrays with normalized float32 values (0.0-1.0)
    representing pixel color channels. They follow Adobe's PDF Blend Mode specification.

    Blend mode categories:

    **Normal modes:**
    - ``normal``: Source replaces backdrop (no blending)

    **Darken modes:**
    - ``darken``: Selects darker of source and backdrop
    - ``multiply``: Multiplies colors (darkens)
    - ``color_burn``: Darkens backdrop to reflect source
    - ``linear_burn``: Similar to multiply but more extreme
    - ``darker_color``: Selects darker color (non-separable)

    **Lighten modes:**
    - ``lighten``: Selects lighter of source and backdrop
    - ``screen``: Inverted multiply (lightens)
    - ``color_dodge``: Brightens backdrop to reflect source
    - ``linear_dodge``: Same as addition (Add blend mode)
    - ``lighter_color``: Selects lighter color (non-separable)

    **Contrast modes:**
    - ``overlay``: Combination of multiply and screen
    - ``soft_light``: Soft version of overlay
    - ``hard_light``: Hard version of overlay
    - ``vivid_light``: Combination of color dodge and burn
    - ``linear_light``: Combination of linear dodge and burn
    - ``pin_light``: Replaces colors based on brightness
    - ``hard_mix``: Posterizes to primary colors

    **Inversion modes:**
    - ``difference``: Absolute difference between colors
    - ``exclusion``: Similar to difference but lower contrast

    **Component modes (non-separable):**
    - ``hue``: Preserves luminosity and saturation, replaces hue
    - ``saturation``: Preserves luminosity and hue, replaces saturation
    - ``color``: Preserves luminosity, replaces hue and saturation
    - ``luminosity``: Preserves hue and saturation, replaces luminosity

    Implementation details:

    - Separable blend modes process each color channel independently
    - Non-separable modes convert to HSL color space first
    - All functions expect normalized float32 arrays (0.0-1.0 range)
    - Division by zero is protected with small epsilon values

    Example usage::

        import numpy as np
        from psd_tools.composite.blend import multiply, screen

        # Create backdrop and source colors (normalized)
        backdrop = np.array([0.5, 0.3, 0.8], dtype=np.float32)
        source = np.array([0.7, 0.6, 0.2], dtype=np.float32)

        # Apply blend mode
        result = multiply(backdrop, source)
        # Result: [0.35, 0.18, 0.16]

    The ``BLEND_FUNC`` dictionary maps :py:class:`~psd_tools.constants.BlendMode`
    enums to their corresponding functions for easy lookup during compositing.
    """

    import functools
    import logging

    import numpy as np

    from psd_tools.constants import BlendMode
    from psd_tools.terminology import Enum

    logger = logging.getLogger(__name__)

    # Separable blend functions
    def normal(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        return Cs

    def multiply(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        return Cb * Cs

    def screen(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        return Cb + Cs - (Cb * Cs)

    def overlay(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        return hard_light(Cs, Cb)

    def darken(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        return np.minimum(Cb, Cs)

    def lighten(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        return np.maximum(Cb, Cs)

    def color_dodge(Cb: np.ndarray, Cs: np.ndarray, s: float = 1.0) -> np.ndarray:
        B = np.zeros_like(Cb, dtype=np.float32)
        B[Cs == 1] = 1
        B[Cb == 0] = 0
        index = (Cs != 1) & (Cb != 0)
        B[index] = np.minimum(1, Cb[index] / (s * (1 - Cs[index] + 1e-9)))
        return B

    def color_burn(Cb: np.ndarray, Cs: np.ndarray, s: float = 1.0) -> np.ndarray:
        B = np.zeros_like(Cb, dtype=np.float32)
        B[Cb == 1] = 1
        index = (Cb != 1) & (Cs != 0)
        B[index] = 1 - np.minimum(1, (1 - Cb[index]) / (s * Cs[index] + 1e-9))
        return B

    def linear_dodge(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        return np.minimum(1, Cb + Cs)

    def linear_burn(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        return np.maximum(0, Cb + Cs - 1)

    def hard_light(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        index = Cs > 0.5
        B = multiply(Cb, 2 * Cs)
        B[index] = screen(Cb, 2 * Cs - 1)[index]
        return B

    def soft_light(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        index = Cs <= 0.25
        index_not = ~index
        D = np.zeros_like(Cb, dtype=np.float32)
        D[index] = ((16 * Cb[index] - 12) * Cb[index] + 4) * Cb[index]
        D[index_not] = np.sqrt(Cb[index_not])

        index = Cs <= 0.5
        index_not = ~index
        B = np.zeros_like(Cb, dtype=np.float32)
        B[index] = Cb[index] - (1 - 2 * Cs[index]) * Cb[index] * (1 - Cb[index])
        B[index_not] = Cb[index_not] + (2 * Cs[index_not] - 1) * (
            D[index_not] - Cb[index_not]
        )
        return B

    def vivid_light(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        """
        Burns or dodges the colors by increasing or decreasing the contrast,
        depending on the blend color. If the blend color (light source) is lighter
        than 50% gray, the image is lightened by decreasing the contrast. If the
        blend color is darker than 50% gray, the image is darkened by increasing
        the contrast.
        """

        Cs2 = Cs * 2
        index = Cs > 0.5
        B = color_burn(Cb, Cs2)
        D = color_dodge(Cb, Cs2 - 1)
        B[index] = D[index]
        return B

    def linear_light(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        """
        Burns or dodges the colors by decreasing or increasing the brightness,
        depending on the blend color. If the blend color (light source) is lighter
        than 50% gray, the image is lightened by increasing the brightness. If the
        blend color is darker than 50% gray, the image is darkened by decreasing
        the brightness.
        """
        index = Cs > 0.5
        B = linear_burn(Cb, 2 * Cs)
        B[index] = linear_dodge(Cb, 2 * Cs - 1)[index]
        return B

    def pin_light(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        """
        Replaces the colors, depending on the blend color. If the blend color
        (light source) is lighter than 50% gray, pixels darker than the blend color
        are replaced, and pixels lighter than the blend color do not change. If the
        blend color is darker than 50% gray, pixels lighter than the blend color
        are replaced, and pixels darker than the blend color do not change. This is
        useful for adding special effects to an image.
        """
        index = Cs > 0.5
        B = darken(Cb, 2 * Cs)
        B[index] = lighten(Cb, 2 * Cs - 1)[index]
        return B

    def difference(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        return np.abs(Cb - Cs)

    def exclusion(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        return Cb + Cs - 2 * Cb * Cs

    def subtract(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        return np.maximum(0, Cb - Cs)

    def hard_mix(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        """
        Adds the red, green and blue channel values of the blend color to the RGB
        values of the base color. If the resulting sum for a channel is 255 or
        greater, it receives a value of 255; if less than 255, a value of 0.
        Therefore, all blended pixels have red, green, and blue channel values of
        either 0 or 255. This changes all pixels to primary additive colors (red,
        green, or blue), white, or black.
        """
        B = np.zeros_like(Cb, dtype=np.float32)
        B[(Cb + 0.999999 * Cs) >= 1] = 1  # There seems a weird numerical issue.
        return B

    def divide(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        """
        Looks at the color information in each channel and divides the blend color
        from the base color.
        """
        B = Cb / (Cs + 1e-9)
        B[B > 1] = 1
        return B

    # Non-separable blending must be in RGB. CMYK should be first converted to RGB,
    # blended, then CMY components should be retrieved from RGB results. K
    # component is K of Cb for hue, saturation, and color blending, and K of Cs for
    # luminosity.

    def non_separable(k: str = "s"):
        """Wrap non-separable blending function for CMYK handling.

        .. note: This implementation is still inaccurate.
        """

        def decorator(func):
            @functools.wraps(func)
            def _blend_fn(Cb, Cs):
                if Cs.shape[2] == 4:
                    K = Cs[:, :, 3:4] if k == "s" else Cb[:, :, 3:4]
                    Cb, Cs = _cmyk2rgb(Cb), _cmyk2rgb(Cs)
                    return np.concatenate((_rgb2cmy(func(Cb, Cs), K), K), axis=2)
                return func(Cb, Cs)

            return _blend_fn

        return decorator

    def _cmyk2rgb(C: np.ndarray) -> np.ndarray:
        return np.stack([(1.0 - C[:, :, i]) * (1.0 - C[:, :, 3]) for i in range(3)], axis=2)

    def _rgb2cmy(C: np.ndarray, K: np.ndarray) -> np.ndarray:
        K = np.repeat(K, 3, axis=2)
        color = np.zeros((C.shape[0], C.shape[1], 3))
        index = K < 1.0
        color[index] = (1.0 - C[index] - K[index]) / (1.0 - K[index] + 1e-9)
        return color

    @non_separable()
    def hue(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        return _set_lum(_set_sat(Cs, _sat(Cb)), _lum(Cb))

    @non_separable()
    def saturation(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        return _set_lum(_set_sat(Cb, _sat(Cs)), _lum(Cb))

    @non_separable()
    def color(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        return _set_lum(Cs, _lum(Cb))

    @non_separable("s")
    def luminosity(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        return _set_lum(Cb, _lum(Cs))

    @non_separable()
    def darker_color(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        index = np.repeat(_lum(Cs) < _lum(Cb), 3, axis=2)
        B = Cb.copy()
        B[index] = Cs[index]
        return B

    @non_separable()
    def lighter_color(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        index = np.repeat(_lum(Cs) > _lum(Cb), 3, axis=2)
        B = Cb.copy()
        B[index] = Cs[index]
        return B

    def dissolve(Cb: np.ndarray, Cs: np.ndarray) -> np.ndarray:
        # TODO: Implement me!
        logger.debug("Dissolve blend is not implemented")
        return normal(Cb, Cs)

    # Helper functions from PDF reference.
    def _lum(C: np.ndarray) -> np.ndarray:
        return 0.3 * C[:, :, 0:1] + 0.59 * C[:, :, 1:2] + 0.11 * C[:, :, 2:3]

    def _set_lum(C: np.ndarray, L: np.ndarray) -> np.ndarray:
        d = L - _lum(C)
        return _clip_color(C + d)

    def _clip_color(C: np.ndarray) -> np.ndarray:
        L = np.repeat(_lum(C), 3, axis=2)
        C_min = np.repeat(np.min(C, axis=2, keepdims=True), 3, axis=2)
        C_max = np.repeat(np.max(C, axis=2, keepdims=True), 3, axis=2)

        index = C_min < 0.0
        L_i = L[index]
        C[index] = L_i + (C[index] - L_i) * L_i / (L_i - C_min[index] + 1e-9)

        index = C_max > 1.0
        L_i = L[index]
        C[index] = L_i + (C[index] - L_i) * (1 - L_i) / (C_max[index] - L_i + 1e-9)

        # For numerical stability.
        C[C < 0.0] = 0
        C[C > 1] = 1
        return C

    def _sat(C):
        return np.max(C, axis=2, keepdims=True) - np.min(C, axis=2, keepdims=True)

    def _set_sat(C: np.ndarray, s: np.ndarray) -> np.ndarray:
        s = np.repeat(s, 3, axis=2)

        C_max = np.repeat(np.max(C, axis=2, keepdims=True), 3, axis=2)
        C_mid = np.repeat(np.median(C, axis=2, keepdims=True), 3, axis=2)
        C_min = np.repeat(np.min(C, axis=2, keepdims=True), 3, axis=2)

        B = np.zeros_like(C, dtype=np.float32)

        index_diff = C_max > C_min
        index_mid = C == C_mid
        index_max = (C == C_max) & ~index_mid
        index_min = C == C_min

        index = index_mid & index_diff
        B[index] = (
            (C_mid[index] - C_min[index]) * s[index] / (C_max[index] - C_min[index] + 1e-9)
        )
        index = index_max & index_diff
        B[index] = s[index]

        B[~index_diff & index_mid] = 0
        B[~index_diff & index_max] = 0

        B[index_min] = 0

        return B

    """Blend function table."""
    BLEND_FUNC = {
        # Layer attributes
        BlendMode.NORMAL: normal,
        BlendMode.MULTIPLY: multiply,
        BlendMode.SCREEN: screen,
        BlendMode.OVERLAY: overlay,
        BlendMode.DARKEN: darken,
        BlendMode.LIGHTEN: lighten,
        BlendMode.COLOR_DODGE: color_dodge,
        BlendMode.COLOR_BURN: color_burn,
        BlendMode.LINEAR_DODGE: linear_dodge,
        BlendMode.LINEAR_BURN: linear_burn,
        BlendMode.HARD_LIGHT: hard_light,
        BlendMode.SOFT_LIGHT: soft_light,
        BlendMode.VIVID_LIGHT: vivid_light,
        BlendMode.LINEAR_LIGHT: linear_light,
        BlendMode.PIN_LIGHT: pin_light,
        BlendMode.HARD_MIX: hard_mix,
        BlendMode.DIVIDE: divide,
        BlendMode.DIFFERENCE: difference,
        BlendMode.EXCLUSION: exclusion,
        BlendMode.SUBTRACT: subtract,
        BlendMode.HUE: hue,
        BlendMode.SATURATION: saturation,
        BlendMode.COLOR: color,
        BlendMode.LUMINOSITY: luminosity,
        BlendMode.DARKER_COLOR: darker_color,
        BlendMode.LIGHTER_COLOR: lighter_color,
        BlendMode.DISSOLVE: dissolve,
        # Descriptor keys
        Enum.Normal: normal,
        Enum.Multiply: multiply,
        Enum.Screen: screen,
        Enum.Overlay: overlay,
        Enum.Darken: darken,
        Enum.Lighten: lighten,
        Enum.ColorDodge: color_dodge,
        Enum.ColorBurn: color_burn,
        b"linearDodge": linear_dodge,
        b"linearBurn": linear_burn,
        Enum.HardLight: hard_light,
        Enum.SoftLight: soft_light,
        b"vividLight": vivid_light,
        b"linearLight": linear_light,
        b"pinLight": pin_light,
        b"hardMix": hard_mix,
        b"blendDivide": divide,
        Enum.Difference: difference,
        Enum.Exclusion: exclusion,
        Enum.Subtract: subtract,
        Enum.Hue: hue,
        Enum.Saturation: saturation,
        Enum.Color: color,
        Enum.Luminosity: luminosity,
        b"darkerColor": darker_color,
        b"ligherColor": lighter_color,
        Enum.Dissolve: dissolve,
    }