"""Data structures configuring Black behavior.

Mostly around Python language feature support per version and Black configuration
chosen by the user.
"""

from dataclasses import dataclass, field
from enum import Enum, auto
from hashlib import sha256
from operator import attrgetter
from typing import Final

from black.const import DEFAULT_LINE_LENGTH


class TargetVersion(Enum):
    PY33 = 3
    PY34 = 4
    PY35 = 5
    PY36 = 6
    PY37 = 7
    PY38 = 8
    PY39 = 9
    PY310 = 10
    PY311 = 11
    PY312 = 12
    PY313 = 13
    PY314 = 14

    def pretty(self) -> str:
        assert self.name[:2] == "PY"
        return f"Python {self.name[2]}.{self.name[3:]}"


class Feature(Enum):
    F_STRINGS = 2
    NUMERIC_UNDERSCORES = 3
    TRAILING_COMMA_IN_CALL = 4
    TRAILING_COMMA_IN_DEF = 5
    # The following two feature-flags are mutually exclusive, and exactly one should be
    # set for every version of python.
    ASYNC_IDENTIFIERS = 6
    ASYNC_KEYWORDS = 7
    ASSIGNMENT_EXPRESSIONS = 8
    POS_ONLY_ARGUMENTS = 9
    RELAXED_DECORATORS = 10
    PATTERN_MATCHING = 11
    UNPACKING_ON_FLOW = 12
    ANN_ASSIGN_EXTENDED_RHS = 13
    EXCEPT_STAR = 14
    VARIADIC_GENERICS = 15
    DEBUG_F_STRINGS = 16
    PARENTHESIZED_CONTEXT_MANAGERS = 17
    TYPE_PARAMS = 18
    # FSTRING_PARSING = 19  # unused
    TYPE_PARAM_DEFAULTS = 20
    UNPARENTHESIZED_EXCEPT_TYPES = 21
    T_STRINGS = 22
    FORCE_OPTIONAL_PARENTHESES = 50

    # __future__ flags
    FUTURE_ANNOTATIONS = 51


FUTURE_FLAG_TO_FEATURE: Final = {
    "annotations": Feature.FUTURE_ANNOTATIONS,
}


VERSION_TO_FEATURES: dict[TargetVersion, set[Feature]] = {
    TargetVersion.PY33: {Feature.ASYNC_IDENTIFIERS},
    TargetVersion.PY34: {Feature.ASYNC_IDENTIFIERS},
    TargetVersion.PY35: {Feature.TRAILING_COMMA_IN_CALL, Feature.ASYNC_IDENTIFIERS},
    TargetVersion.PY36: {
        Feature.F_STRINGS,
        Feature.NUMERIC_UNDERSCORES,
        Feature.TRAILING_COMMA_IN_CALL,
        Feature.TRAILING_COMMA_IN_DEF,
        Feature.ASYNC_IDENTIFIERS,
    },
    TargetVersion.PY37: {
        Feature.F_STRINGS,
        Feature.NUMERIC_UNDERSCORES,
        Feature.TRAILING_COMMA_IN_CALL,
        Feature.TRAILING_COMMA_IN_DEF,
        Feature.ASYNC_KEYWORDS,
        Feature.FUTURE_ANNOTATIONS,
    },
    TargetVersion.PY38: {
        Feature.F_STRINGS,
        Feature.DEBUG_F_STRINGS,
        Feature.NUMERIC_UNDERSCORES,
        Feature.TRAILING_COMMA_IN_CALL,
        Feature.TRAILING_COMMA_IN_DEF,
        Feature.ASYNC_KEYWORDS,
        Feature.FUTURE_ANNOTATIONS,
        Feature.ASSIGNMENT_EXPRESSIONS,
        Feature.POS_ONLY_ARGUMENTS,
        Feature.UNPACKING_ON_FLOW,
        Feature.ANN_ASSIGN_EXTENDED_RHS,
    },
    TargetVersion.PY39: {
        Feature.F_STRINGS,
        Feature.DEBUG_F_STRINGS,
        Feature.NUMERIC_UNDERSCORES,
        Feature.TRAILING_COMMA_IN_CALL,
        Feature.TRAILING_COMMA_IN_DEF,
        Feature.ASYNC_KEYWORDS,
        Feature.FUTURE_ANNOTATIONS,
        Feature.ASSIGNMENT_EXPRESSIONS,
        Feature.RELAXED_DECORATORS,
        Feature.POS_ONLY_ARGUMENTS,
        Feature.UNPACKING_ON_FLOW,
        Feature.ANN_ASSIGN_EXTENDED_RHS,
        Feature.PARENTHESIZED_CONTEXT_MANAGERS,
    },
    TargetVersion.PY310: {
        Feature.F_STRINGS,
        Feature.DEBUG_F_STRINGS,
        Feature.NUMERIC_UNDERSCORES,
        Feature.TRAILING_COMMA_IN_CALL,
        Feature.TRAILING_COMMA_IN_DEF,
        Feature.ASYNC_KEYWORDS,
        Feature.FUTURE_ANNOTATIONS,
        Feature.ASSIGNMENT_EXPRESSIONS,
        Feature.RELAXED_DECORATORS,
        Feature.POS_ONLY_ARGUMENTS,
        Feature.UNPACKING_ON_FLOW,
        Feature.ANN_ASSIGN_EXTENDED_RHS,
        Feature.PARENTHESIZED_CONTEXT_MANAGERS,
        Feature.PATTERN_MATCHING,
    },
    TargetVersion.PY311: {
        Feature.F_STRINGS,
        Feature.DEBUG_F_STRINGS,
        Feature.NUMERIC_UNDERSCORES,
        Feature.TRAILING_COMMA_IN_CALL,
        Feature.TRAILING_COMMA_IN_DEF,
        Feature.ASYNC_KEYWORDS,
        Feature.FUTURE_ANNOTATIONS,
        Feature.ASSIGNMENT_EXPRESSIONS,
        Feature.RELAXED_DECORATORS,
        Feature.POS_ONLY_ARGUMENTS,
        Feature.UNPACKING_ON_FLOW,
        Feature.ANN_ASSIGN_EXTENDED_RHS,
        Feature.PARENTHESIZED_CONTEXT_MANAGERS,
        Feature.PATTERN_MATCHING,
        Feature.EXCEPT_STAR,
        Feature.VARIADIC_GENERICS,
    },
    TargetVersion.PY312: {
        Feature.F_STRINGS,
        Feature.DEBUG_F_STRINGS,
        Feature.NUMERIC_UNDERSCORES,
        Feature.TRAILING_COMMA_IN_CALL,
        Feature.TRAILING_COMMA_IN_DEF,
        Feature.ASYNC_KEYWORDS,
        Feature.FUTURE_ANNOTATIONS,
        Feature.ASSIGNMENT_EXPRESSIONS,
        Feature.RELAXED_DECORATORS,
        Feature.POS_ONLY_ARGUMENTS,
        Feature.UNPACKING_ON_FLOW,
        Feature.ANN_ASSIGN_EXTENDED_RHS,
        Feature.PARENTHESIZED_CONTEXT_MANAGERS,
        Feature.PATTERN_MATCHING,
        Feature.EXCEPT_STAR,
        Feature.VARIADIC_GENERICS,
        Feature.TYPE_PARAMS,
    },
    TargetVersion.PY313: {
        Feature.F_STRINGS,
        Feature.DEBUG_F_STRINGS,
        Feature.NUMERIC_UNDERSCORES,
        Feature.TRAILING_COMMA_IN_CALL,
        Feature.TRAILING_COMMA_IN_DEF,
        Feature.ASYNC_KEYWORDS,
        Feature.FUTURE_ANNOTATIONS,
        Feature.ASSIGNMENT_EXPRESSIONS,
        Feature.RELAXED_DECORATORS,
        Feature.POS_ONLY_ARGUMENTS,
        Feature.UNPACKING_ON_FLOW,
        Feature.ANN_ASSIGN_EXTENDED_RHS,
        Feature.PARENTHESIZED_CONTEXT_MANAGERS,
        Feature.PATTERN_MATCHING,
        Feature.EXCEPT_STAR,
        Feature.VARIADIC_GENERICS,
        Feature.TYPE_PARAMS,
        Feature.TYPE_PARAM_DEFAULTS,
    },
    TargetVersion.PY314: {
        Feature.F_STRINGS,
        Feature.DEBUG_F_STRINGS,
        Feature.NUMERIC_UNDERSCORES,
        Feature.TRAILING_COMMA_IN_CALL,
        Feature.TRAILING_COMMA_IN_DEF,
        Feature.ASYNC_KEYWORDS,
        Feature.FUTURE_ANNOTATIONS,
        Feature.ASSIGNMENT_EXPRESSIONS,
        Feature.RELAXED_DECORATORS,
        Feature.POS_ONLY_ARGUMENTS,
        Feature.UNPACKING_ON_FLOW,
        Feature.ANN_ASSIGN_EXTENDED_RHS,
        Feature.PARENTHESIZED_CONTEXT_MANAGERS,
        Feature.PATTERN_MATCHING,
        Feature.EXCEPT_STAR,
        Feature.VARIADIC_GENERICS,
        Feature.TYPE_PARAMS,
        Feature.TYPE_PARAM_DEFAULTS,
        Feature.UNPARENTHESIZED_EXCEPT_TYPES,
        Feature.T_STRINGS,
    },
}


def supports_feature(target_versions: set[TargetVersion], feature: Feature) -> bool:
    if not target_versions:
        raise ValueError("target_versions must not be empty")

    return all(feature in VERSION_TO_FEATURES[version] for version in target_versions)


class Preview(Enum):
    """Individual preview style features."""

    # NOTE: string_processing requires wrap_long_dict_values_in_parens
    # for https://github.com/psf/black/issues/3117 to be fixed.
    string_processing = auto()
    hug_parens_with_braces_and_square_brackets = auto()
    wrap_long_dict_values_in_parens = auto()
    multiline_string_handling = auto()
    always_one_newline_after_import = auto()
    fix_fmt_skip_in_one_liners = auto()
    standardize_type_comments = auto()
    wrap_comprehension_in = auto()
    # Remove parentheses around multiple exception types in except and
    # except* without as. See PEP 758 for details.
    remove_parens_around_except_types = auto()
    normalize_cr_newlines = auto()
    fix_module_docstring_detection = auto()
    fix_type_expansion_split = auto()


UNSTABLE_FEATURES: set[Preview] = {
    # Many issues, see summary in https://github.com/psf/black/issues/4042
    Preview.string_processing,
    # See issue #4036 (crash), #4098, #4099 (proposed tweaks)
    Preview.hug_parens_with_braces_and_square_brackets,
}


class Deprecated(UserWarning):
    """Visible deprecation warning."""


_MAX_CACHE_KEY_PART_LENGTH: Final = 32


@dataclass
class Mode:
    target_versions: set[TargetVersion] = field(default_factory=set)
    line_length: int = DEFAULT_LINE_LENGTH
    string_normalization: bool = True
    is_pyi: bool = False
    is_ipynb: bool = False
    skip_source_first_line: bool = False
    magic_trailing_comma: bool = True
    python_cell_magics: set[str] = field(default_factory=set)
    preview: bool = False
    unstable: bool = False
    enabled_features: set[Preview] = field(default_factory=set)

    def __contains__(self, feature: Preview) -> bool:
        """
        Provide `Preview.FEATURE in Mode` syntax that mirrors the ``preview`` flag.

        In unstable mode, all features are enabled. In preview mode, all features
        except those in UNSTABLE_FEATURES are enabled. Any features in
        `self.enabled_features` are also enabled.
        """
        if self.unstable:
            return True
        if feature in self.enabled_features:
            return True
        return self.preview and feature not in UNSTABLE_FEATURES

    def get_cache_key(self) -> str:
        if self.target_versions:
            version_str = ",".join(
                str(version.value)
                for version in sorted(self.target_versions, key=attrgetter("value"))
            )
        else:
            version_str = "-"
        if len(version_str) > _MAX_CACHE_KEY_PART_LENGTH:
            version_str = sha256(version_str.encode()).hexdigest()[
                :_MAX_CACHE_KEY_PART_LENGTH
            ]
        features_and_magics = (
            ",".join(sorted(f.name for f in self.enabled_features))
            + "@"
            + ",".join(sorted(self.python_cell_magics))
        )
        if len(features_and_magics) > _MAX_CACHE_KEY_PART_LENGTH:
            features_and_magics = sha256(features_and_magics.encode()).hexdigest()[
                :_MAX_CACHE_KEY_PART_LENGTH
            ]
        parts = [
            version_str,
            str(self.line_length),
            str(int(self.string_normalization)),
            str(int(self.is_pyi)),
            str(int(self.is_ipynb)),
            str(int(self.skip_source_first_line)),
            str(int(self.magic_trailing_comma)),
            str(int(self.preview)),
            str(int(self.unstable)),
            features_and_magics,
        ]
        return ".".join(parts)

    def __hash__(self) -> int:
        return hash((
            frozenset(self.target_versions),
            self.line_length,
            self.string_normalization,
            self.is_pyi,
            self.is_ipynb,
            self.skip_source_first_line,
            self.magic_trailing_comma,
            frozenset(self.python_cell_magics),
            self.preview,
            self.unstable,
            frozenset(self.enabled_features),
        ))
