#-----------------------------------------------------------------------------
# Copyright (c) Anaconda, Inc., and Bokeh Contributors.
# All rights reserved.
#
# The full license is in the file LICENSE.txt, distributed with this software.
#-----------------------------------------------------------------------------
""" Provide properties for various visual attributes.

"""

#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
from __future__ import annotations

import logging # isort:skip
log = logging.getLogger(__name__)

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

# Standard library imports
import base64
import datetime  # lgtm [py/import-and-import-from]
import re
import tempfile
from io import BytesIO
from pathlib import Path
from typing import Any, BinaryIO, Literal
from urllib.parse import quote

# Bokeh imports
from ...util.serialization import convert_datetime_type
from .. import enums
from .auto import Auto
from .bases import Property
from .container import Seq, Tuple
from .datetime import Datetime, TimeDelta
from .either import Either
from .enum import Enum
from .nullable import Nullable
from .numeric import Float, Int
from .primitive import String
from .string import Regex

#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------

__all__ = (
    'CSSLength',
    'DashPattern',
    'FontSize',
    'HatchPatternType',
    'Image',
    'MinMaxBounds',
    'MarkerType',
)

#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------

class DashPattern(Either):
    """ Accept line dash specifications.

    Express patterns that describe line dashes.  ``DashPattern`` values
    can be specified in a variety of ways:

    * An enum: "solid", "dashed", "dotted", "dotdash", "dashdot"
    * a tuple or list of integers in the `HTML5 Canvas dash specification style`_.
      Note that if the list of integers has an odd number of elements, then
      it is duplicated, and that duplicated list becomes the new dash list.

    To indicate that dashing is turned off (solid lines), specify the empty
    list [].

    .. _HTML5 Canvas dash specification style: https://html.spec.whatwg.org/#dash-list

    """

    _dash_patterns = {
        "solid": [],
        "dashed": [6],
        "dotted": [2,4],
        "dotdash": [2,4,6,4],
        "dashdot": [6,4,2,4],
    }

    def __init__(self, default=[], *, help: str | None = None) -> None:
        types = Enum(enums.DashPattern), Regex(r"^(\d+(\s+\d+)*)?$"), Seq(Int)
        super().__init__(*types, default=default, help=help)

    def __str__(self) -> str:
        return self.__class__.__name__

    def transform(self, value):
        value = super().transform(value)

        if isinstance(value, str):
            try:
                return self._dash_patterns[value]
            except KeyError:
                return [int(x) for x in value.split()]
        else:
            return value

CSS_LENGTH_RE = re.compile(r"^[0-9]+(.[0-9]+)?(%|em|ex|ch|ic|rem|vw|vh|vi|vb|vmin|vmax|cm|mm|q|in|pc|pt|px)$", re.I)

class CSSLength(String):

    def validate(self, value: Any, detail: bool = True) -> None:
        super().validate(value, detail)

        if not (isinstance(value, str) and CSS_LENGTH_RE.match(value)):
            msg = "" if not detail else f"{value!r} is not a valid CSS length"
            raise ValueError(msg)

class FontSize(String):

    def validate(self, value: Any, detail: bool = True) -> None:
        super().validate(value, detail)

        if isinstance(value, str):
            if len(value) == 0:
                msg = "" if not detail else "empty string is not a valid font size value"
                raise ValueError(msg)
            elif not CSS_LENGTH_RE.match(value):
                msg = "" if not detail else f"{value!r} is not a valid font size value"
                raise ValueError(msg)

class HatchPatternType(Either):
    """ Accept built-in fill hatching specifications.

    Accepts either "long" names, e.g. "horizontal-wave" or the single letter
    abbreviations, e.g. "v"

    """

    def __init__(self, default=[], *, help: str | None = None) -> None:
        types = Enum(enums.HatchPattern), Enum(enums.HatchPatternAbbreviation), String
        super().__init__(*types, default=default, help=help)

    def __str__(self) -> str:
        return self.__class__.__name__

class Image(Property[str]):
    """ Accept image file types, e.g. PNG, JPEG, TIFF, etc.

    This property can be configured with:

    * A ``pathlib.Path`` image file path
    * A data URL encoded image string
    * A string filename to be loaded with ``PIL.Image.open``
    * An RGB(A) NumPy array, will be converted to PNG
    * A ``PIL.Image.Image`` object

    In all cases, the image data is serialized as a Base64 encoded string.

    """

    def validate(self, value: Any, detail: bool = True) -> None:
        import numpy as np
        import PIL.Image

        if isinstance(value, str | Path | PIL.Image.Image):
            return

        if isinstance(value, np.ndarray):
            if value.dtype == "uint8" and len(value.shape) == 3 and value.shape[2] in (3, 4):
                return

        msg = "" if not detail else f"invalid value: {value!r}; allowed values are string filenames, PIL.Image.Image instances, or RGB(A) NumPy arrays"
        raise ValueError(msg)

    def transform(self, value: Any) -> str:
        import numpy as np
        import PIL.Image

        if isinstance(value, np.ndarray):
            value = PIL.Image.fromarray(value)

        if isinstance(value, str):
            if value.startswith(("data:", "http://", "https://", "file://")):
                return value

            path = Path(value)
            if path.exists():
                value = path
            else:
                return value

        def data_image(format: str, encoding: Literal["utf8", "base64"], data: str) -> str:
            return f"data:image/{format};{encoding},{data}"

        if isinstance(value, Path) and value.suffix == ".svg":
            return data_image("svg+xml", "utf8", quote(value.read_text()))

        # tempfile doesn't implement IO interface (https://bugs.python.org/issue33762)
        if isinstance(value, Path | BinaryIO | tempfile._TemporaryFileWrapper):
            value = PIL.Image.open(value)

        if isinstance(value, PIL.Image.Image):
            out = BytesIO()
            format = value.format or "PNG"
            value.save(out, format)
            encoded = base64.b64encode(out.getvalue()).decode('ascii')
            return data_image(format.lower(), "base64", encoded)

        raise ValueError(f"Could not transform {value!r}")

class MinMaxBounds(Either):
    """ Accept (min, max) bounds tuples for use with Ranges.

    Bounds are provided as a tuple of ``(min, max)`` so regardless of whether your range is
    increasing or decreasing, the first item should be the minimum value of the range and the
    second item should be the maximum. Setting min > max will result in a ``ValueError``.

    Setting bounds to None will allow your plot to pan/zoom as far as you want. If you only
    want to constrain one end of the plot, you can set min or max to
    ``None`` e.g. ``DataRange1d(bounds=(None, 12))`` """

    def __init__(self, default='auto', *, accept_datetime: bool = False, help: str | None = None) -> None:
        types = (
            Auto,

            Tuple(Float, Float),
            Tuple(Nullable(Float), Float),
            Tuple(Float, Nullable(Float)),

            Tuple(TimeDelta, TimeDelta),
            Tuple(Nullable(TimeDelta), TimeDelta),
            Tuple(TimeDelta, Nullable(TimeDelta)),
        )
        if accept_datetime:
            types = (
                *types,
                Tuple(Datetime, Datetime),
                Tuple(Nullable(Datetime), Datetime),
                Tuple(Datetime, Nullable(Datetime)),
            )
        super().__init__(*types, default=default, help=help)

    def validate(self, value: Any, detail: bool = True) -> None:
        super().validate(value, detail)

        if value[0] is None or value[1] is None:
            return

        value = list(value)

        # make sure the values are timestamps for comparison
        if isinstance(value[0], datetime.datetime):
            value[0] = convert_datetime_type(value[0])

        if isinstance(value[1], datetime.datetime):
            value[1] = convert_datetime_type(value[1])

        if value[0] < value[1]:
            return

        msg = "" if not detail else "Invalid bounds: maximum smaller than minimum. Correct usage: bounds=(min, max)"
        raise ValueError(msg)

class MarkerType(Enum):
    """

    """
    def __init__(self, **kw) -> None:
        super().__init__(enums.MarkerType, **kw)

#-----------------------------------------------------------------------------
# Dev API
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Private API
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------
