#
# Copyright (c), 2016-2026, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present
# distribution, or http://opensource.org/licenses/MIT.
#
# @author Davide Brunato <brunato@sissa.it>
#
import re
from sys import maxunicode
from collections import Counter
from collections.abc import Callable, Iterator, MutableSet
from itertools import chain
from typing import AbstractSet, Any, Optional, Union

from elementpath.helpers import LazyPattern
from .codepoints import RegexError
from .unicode_subsets import UnicodeSubset, lazy_subset, unicode_subset, unicode_category

I_SHORTCUT_REPLACE = (
    ":A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF"
    "\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD"
)

C_SHORTCUT_REPLACE = (
    "-.0-9:A-Z_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-"
    "\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD"
)


@lazy_subset
def c_shortcut() -> UnicodeSubset:
    return UnicodeSubset(C_SHORTCUT_REPLACE)


@lazy_subset
def i_shortcut() -> UnicodeSubset:
    return UnicodeSubset(I_SHORTCUT_REPLACE)


@lazy_subset
def s_shortcut() -> UnicodeSubset:
    return UnicodeSubset(' \t\n\r')


@lazy_subset
def d_shortcut() -> UnicodeSubset:
    return unicode_category('Nd')


@lazy_subset
def w_shortcut() -> UnicodeSubset:
    return UnicodeSubset(chain.from_iterable(unicode_category(x) for x in 'LMNS'))


# Single and Multi character escapes
CHARACTER_ESCAPES: dict[str, Union[str, Callable[[], UnicodeSubset]]] = {
    # Single-character escapes
    '\\n': '\n',
    '\\r': '\r',
    '\\t': '\t',
    '\\|': '|',
    '\\.': '.',
    '\\-': '-',
    '\\^': '^',
    '\\?': '?',
    '\\*': '*',
    '\\+': '+',
    '\\{': '{',
    '\\}': '}',
    '\\(': '(',
    '\\)': ')',
    '\\[': '[',
    '\\]': ']',
    '\\\\': '\\',

    # Multi-character escapes
    '\\s': s_shortcut,
    '\\S': s_shortcut,
    '\\d': d_shortcut,
    '\\D': d_shortcut,
    '\\i': i_shortcut,
    '\\I': i_shortcut,
    '\\c': c_shortcut,
    '\\C': c_shortcut,
    '\\w': w_shortcut,
    '\\W': w_shortcut,
}

re_char_set = LazyPattern(r'(\\[nrt|.\-^?*+{}()\]sSdDiIcCwW]|\\[pP]{[a-zA-Z\-0-9]+})')


def get_charset_parts(charset: str) -> list[str]:
    parts: list[str] = []
    prev = ''
    for part in re_char_set.split(charset):
        if not part:
            continue
        elif part.endswith('-') and part != '\\-':
            prev = part
        elif part.startswith('-') and parts:
            parts[-1] += prev + part
            prev = ''
        else:
            parts.append(prev + part)
            prev = ''
    else:
        if prev:
            parts.append(prev)

    return parts


class CharacterClass(MutableSet[int]):
    """
    A set class to represent XML Schema/XQuery/XPath regex character class.

    :param charset: a string with formatted character set.
    :param xsd_version: the reference XSD version for syntax variants. Defaults to '1.0'.
    TODO: implement __ior__, __iand__, __ixor__ operators for a full mutable set class.
    """
    _re_char_set = re.compile(r'(\\[nrt|.\-^?*+{}()\]sSdDiIcCwW]|\\[pP]{[a-zA-Z\-0-9]+})')
    _re_unicode_ref = re.compile(r'\\([pP]){([\w-]+)}')

    __slots__ = 'xsd_version', 'positive', 'negative'

    def __init__(self, charset: Optional[str] = None, xsd_version: str = '1.0') -> None:
        self.xsd_version = xsd_version
        self.positive = UnicodeSubset()
        self.negative = UnicodeSubset()
        if charset:
            self.add(charset)

    def __repr__(self) -> str:
        return '%s(%s)' % (self.__class__.__name__, str(self))

    def __str__(self) -> str:
        if not self.negative:
            return '[%s]' % str(self.positive)
        elif not self.positive:
            return '[^%s]' % str(self.negative)
        else:
            return '[%s%s]' % (
                str(UnicodeSubset(self.negative.complement())), str(self.positive)
            )

    def __copy__(self) -> 'CharacterClass':
        obj = CharacterClass(xsd_version=self.xsd_version)
        obj.positive.update(self.positive)
        obj.negative.update(self.negative)
        return self

    def __contains__(self, item: object) -> bool:
        if isinstance(item, str):
            item = ord(item)
        elif not isinstance(item, int):
            return False

        if self.negative:
            return item not in self.negative or item in self.positive
        return item in self.positive

    def __iter__(self) -> Iterator[int]:
        if self.negative:
            return (
                cp for cp in range(maxunicode + 1)
                if cp in self.positive or cp not in self.negative
            )
        return iter(sorted(self.positive))  # type: ignore[arg-type]

    def __len__(self) -> int:
        if self.negative:
            not_in_positive = Counter(x not in self.positive for x in self.negative)[True]
            return maxunicode + 1 - not_in_positive
        return len(self.positive)

    def __isub__(self, other: AbstractSet[Any]) -> 'CharacterClass':
        if isinstance(other, CharacterClass):
            if self.negative:
                if other.negative:
                    self.positive |= (other.negative - self.negative)
                    self.negative.clear()
                self.negative |= other.positive
            elif other.negative:
                self.positive &= other.negative
            self.positive -= other.positive
            return self
        return NotImplemented

    def __sub__(self, other: AbstractSet[Any]) -> 'CharacterClass':
        obj = self.__copy__()
        return obj.__isub__(other)

    def add(self, charset: Union[int, str]) -> None:
        if isinstance(charset, int):
            charset = chr(charset)

        for part in get_charset_parts(charset):
            if part in CHARACTER_ESCAPES:
                value = CHARACTER_ESCAPES[part]
                if isinstance(value, str):
                    self.positive.update(value)
                elif part[-1].islower():
                    self.positive |= value()
                else:
                    self.negative |= value()
            elif part.startswith('\\p') or part.startswith('\\P'):
                if self._re_unicode_ref.search(part) is None:
                    raise RegexError("wrong Unicode block specification %r" % part)

                try:
                    subset = unicode_subset(part[3:-1])
                except RegexError:
                    # XSD 1.1 supports Is prefix to match Unicode blocks
                    if not self.xsd_version or not part[3:].startswith('Is'):
                        raise
                    self.positive |= UnicodeSubset([(0, maxunicode + 1)])
                else:
                    if part.startswith('\\p'):
                        self.positive |= subset
                    else:
                        self.negative |= subset
            else:
                self.positive.update(part)

    def discard(self, charset: Union[int, str]) -> None:
        if isinstance(charset, int):
            charset = chr(charset)

        for part in get_charset_parts(charset):
            if part in CHARACTER_ESCAPES:
                value = CHARACTER_ESCAPES[part]
                if isinstance(value, str):
                    self.positive.difference_update(value)
                    if self.negative:
                        self.negative.update(value)
                elif part[-1].islower():
                    self.positive -= value()
                    if self.negative:
                        self.negative |= value()
                else:
                    self.positive &= value()
                    self.negative.clear()

            elif part.startswith('\\p') or part.startswith('\\P'):
                if self._re_unicode_ref.search(part) is None:
                    raise RegexError("wrong Unicode block specification %r" % part)

                try:
                    subset = unicode_subset(part[3:-1])
                except RegexError:
                    # XSD 1.1 supports Is prefix to match Unicode blocks
                    if not self.xsd_version or not part[3:].startswith('Is'):
                        raise
                    self.positive -= UnicodeSubset([(0, maxunicode + 1)])
                else:
                    if part.startswith('\\p'):
                        self.positive -= subset
                    else:
                        self.negative -= subset
            else:
                self.positive.difference_update(part)

    def clear(self) -> None:
        self.positive.clear()
        self.negative.clear()

    def complement(self) -> None:
        if self.positive or self.negative:
            self.positive, self.negative = self.negative, self.positive
        else:
            self.positive.codepoints = [(0, maxunicode + 1)]
