"""
Absfuyu: Data Extension
-----------------------
list extension (with generic)

Version: 6.8.0
Date updated: 24/05/2026 (dd/mm/yyyy)
"""

from __future__ import annotations

# Module Package
# ---------------------------------------------------------------------------
__all__ = ["ListExt"]


# Library
# ---------------------------------------------------------------------------
import random
from collections import defaultdict
from collections.abc import Callable, Iterable
from heapq import heapreplace
from itertools import chain, count, groupby
from typing import Any, Literal, Self, cast, overload

from absfuyu.core.baseclass import GetClassMembersMixin
from absfuyu.core.docstring import versionadded, versionchanged
from absfuyu.util import set_min_max


# Class
# ---------------------------------------------------------------------------
class ListExt[T](GetClassMembersMixin, list[T]):
    """
    ``list`` extension (with generic - W.I.P)

    Missing: freq, transpose, slice_points, split_chunk, to_column, wrap_to_column

    >>> # For a list of new methods
    >>> ListExt.show_all_methods()
    """

    # MARK: Info
    def head(self, number_of_items: int = 5, /) -> Self:
        """
        Show first ``number_of_items`` items in ``ListExt``

        Parameters
        ----------
        number_of_items : int
            | Number of items to shows at once
            | (Default: ``5``)

        Returns
        -------
        list
            Filtered list


        Example:
        --------
        >>> ListExt(range(10)).head(2)
        [0, 1]
        """
        number_of_items = int(set_min_max(number_of_items, min_value=0, max_value=len(self)))
        return self[:number_of_items]

    def tail(self, number_of_items: int = 5, /) -> Self:
        """
        Show last ``number_of_items`` items in ``ListExt``

        Parameters
        ----------
        number_of_items : int
            | Number of items to shows at once
            | (Default: ``5``)

        Returns
        -------
        list
            Filtered list


        Example:
        --------
        >>> ListExt(range(10)).tail(2)
        [8, 9]
        """
        number_of_items = int(set_min_max(number_of_items, min_value=0, max_value=len(self)))
        return self[::-1][:number_of_items][::-1]

    # MARK: Misc
    def sorts(self, reverse: bool = False) -> Self:
        """
        Sort all items (with different type) in ``list``

        Parameters
        ----------
        reverse : bool
            - ``True`` then sort in descending order
            - ``False`` then sort in ascending order (default value)

        Returns
        -------
        Self
            A sorted list


        Example:
        --------
        >>> test = ListExt([9, "abc", 3.5, "aaa", 1, 1.4])
        >>> test.sorts()
        [1, 9, 'aaa', 'abc', 1.4, 3.5]
        """
        lst = self.copy()
        type_weights: dict = {}
        for x in lst:
            if type(x) not in type_weights:
                type_weights[type(x)] = len(type_weights)
        # logger.debug(f"Type weight: {type_weights}")

        output = sorted(lst, key=lambda x: (type_weights[type(x)], str(x)), reverse=reverse)

        # logger.debug(output)
        return self.__class__(output)

    @versionchanged("5.2.0", reason="New ``recursive`` parameter")
    def flatten(self, recursive: bool = False) -> Self:
        """
        Flatten the list

        Parameters
        ----------
        recursive : bool
            Recursively flatten the list, by default ``False``

        Returns
        -------
        Self
            Flattened list


        Example:
        --------
        >>> test = ListExt([["test"], ["test", "test"], ["test"]])
        >>> test.flatten()
        ['test', 'test', 'test', 'test']
        """

        def instance_checking(item):
            if isinstance(item, str):
                return [item]
            if isinstance(item, Iterable):
                return item
            return [item]

        # Flatten
        list_of_list = (instance_checking(x) for x in self)
        flattened = self.__class__(chain(*list_of_list))

        # Recursive
        if recursive:

            def _condition(item) -> bool:
                if isinstance(item, str):
                    return False
                return isinstance(item, Iterable)

            while any(flattened.apply(_condition)):
                flattened = flattened.flatten()

        # Return
        return flattened

    @versionadded("5.2.0")
    def join(self, sep: str = " ", /) -> str:
        """
        Join every element in list to str (``str.join()`` wrapper).

        Parameters
        ----------
        sep : str, optional
            Separator between each element, by default ``" "``

        Returns
        -------
        str
            Joined list


        Example:
        --------
        >>> ListExt(["a", "b", "c"]).join()
        a b c

        >>> # Also work with non-str type
        >>> ListExt([1, 2, 3]).join()
        1 2 3
        """
        try:
            return sep.join(self)  # type: ignore
        except TypeError:
            return sep.join(self.apply(str))

    # MARK: Modify
    def apply[R](self, func: Callable[[T], R]) -> ListExt[R]:
        """
        Apply function to each entry

        Parameters
        ----------
        func : Callable[[Any], Any]
            Callable function

        Returns
        -------
        Self
            ListExt


        Example:
        --------
        >>> test = ListExt([1, 2, 3])
        >>> test.apply(str)
        ['1', '2', '3']
        """
        # return self.__class__(map(func, self))
        return self.__class__(func(x) for x in self)  # type: ignore

    @overload
    def numbering(self) -> ListExt[tuple[int, T]]: ...

    @overload
    def numbering(self, start: int | float = 0) -> ListExt[tuple[int, T]]: ...

    @overload
    def numbering(self, *, step: int | float = 1) -> ListExt[tuple[int, T]]: ...

    @overload
    def numbering(self, start: int | float = 0, step: int | float = 1) -> ListExt[tuple[int, T]]: ...

    def numbering(self, start: int | float = 0, step: int | float = 1) -> ListExt[tuple[int, T]]:
        """
        Number the item in list
        (``itertools.count`` wrapper)

        Parameters
        ----------
        start : int | float, optional
            Start from which number, by default ``0``

        step : int | float, optional
            Step, by default ``1``

        Returns
        -------
        Self
            Counted list


        Example:
        --------
        >>> test = ListExt([9, 9, 9])
        >>> test.numbering()
        [(0, 9), (1, 9), (2, 9)]
        """
        nums = count(start=start, step=step)
        # return self.__class__(enumerate(self, start=start))
        return self.__class__(zip(nums, self))

    # MARK: Slice/Split
    def split_equal(self, n: int, sort: bool = True, reverse: bool = True) -> ListExt[list[T]]:
        """
        Try to equally split a list of number into ``n`` equal sum parts.

        **Note:** Element in list must be a number.

        Parameters
        ----------
        n : int
            Split into how many parts. Must be >= 1.

        sort : bool, optional
            Sort the instance before split, by default ``True``

        reverse : bool, optional
            Reverse sort order, by default ``True``

        Returns
        -------
        Self
            Splitted (list[list])


        Example:
        --------
        >>> ListExt(range(1, 11)).split_equal(2)
        [[10, 7, 5, 4, 1], [9, 8, 6, 3, 2]]
        """

        # https://stackoverflow.com/a/61649667
        bins: list[list[int]] = [[0] for _ in range(max(n, 1))]
        if sort:
            # self = self.sorts(reverse=True)
            self = self.__class__(sorted(self, reverse=reverse))
        for x in self:
            least = bins[0]
            least[0] += x
            least.append(x)
            heapreplace(bins, least)
        return self.__class__(x[1:] for x in bins)

    # MARK: Random
    def pick_one(self) -> T:
        """
        Pick one random items from ``list``

        Returns
        -------
        Any
            Random value


        Example:
        --------
        >>> test = ListExt(["foo", "bar"])
        >>> test.pick_one()
        'bar'
        """
        if len(self) != 0:
            out = random.choice(self)
            # logger.debug(out)
            return out
        else:
            # logger.debug("List empty!")
            raise IndexError("List empty!")

    def get_random(self, number_of_items: int = 5, /) -> list[T]:
        """
        Get ``number_of_items`` random items in ``ListExt``

        Parameters
        ----------
        number_of_items : int
            Number random of items, by default ``5``

        Returns
        -------
        list
            Filtered list
        """
        return [self.pick_one() for _ in range(number_of_items)]

    @versionadded("5.10.0")
    def shuffle(self) -> Self:
        """
        Shuffle the list itself

        Returns
        -------
        Self
            Shuffled list
        """
        random.shuffle(self)
        return self

    # MARK: Len
    @versionchanged("5.2.0", reason="Handle more type")
    def len_items(self) -> ListExt[int]:
        """
        ``len()`` for every item in ``self``

        Returns
        -------
        Self
            List of ``len()``'ed value


        Example:
        --------
        >>> test = ListExt(["foo", "bar", "pizza"])
        >>> test.len_items()
        [3, 3, 5]
        """

        def _len(item: Any) -> int:
            try:
                return len(item)
            except TypeError:
                return len(str(item))

        return self.__class__([_len(x) for x in self])  # type: ignore

    @versionchanged("5.2.0", reason="New ``recursive`` parameter")
    def mean_len(self, recursive: bool = False) -> float:
        """
        Average length of every item. Returns zero if failed (empty list, ZeroDivisionError).

        Parameters
        ----------
        recursive : bool
            Recursively find the average length of items in nested lists, by default ``False``

        Returns
        -------
        float
            Average length


        Example:
        --------
        >>> test = ListExt(["foo", "bar", "pizza"])
        >>> test.mean_len()
        3.6666666666666665
        """

        if recursive:
            dat = self.flatten(recursive=recursive)
        else:
            dat = self

        try:
            return sum(dat.len_items()) / len(dat)
        except ZeroDivisionError:
            return 0.0

    @versionadded("5.2.0")
    def max_item_len(self, recursive: bool = False) -> int:
        """
        Find the maximum length of items in the list.

        Parameters
        ----------
        recursive : bool
            Recursively find the maximum length of items in nested lists, by default ``False``

        Returns
        -------
        int
            Maximum length of items


        Example:
        --------
        >>> test = ListExt(["test", "longer_test"])
        >>> test.max_item_len()
        11

        >>> test = ListExt([["short"], ["longer_test"]])
        >>> test.max_item_len(recursive=True)
        11
        """
        if recursive:
            return cast(int, max(self.flatten(recursive=True).len_items()))
        return cast(int, max(self.len_items()))

    # MARK: Group/Unique
    def unique(self) -> Self:
        """
        Remove duplicates

        Returns
        -------
        Self
            Duplicates removed list


        Example:
        --------
        >>> test = ListExt([1, 1, 1, 2, 2, 3])
        >>> test.unique()
        [1, 2, 3]
        """
        return self.__class__(set(self))

    def group_by_unique(self) -> ListExt[list[T]]:
        """
        Group duplicated elements into list

        Returns
        -------
        Self
            Grouped value


        Example:
        --------
        >>> test = ListExt([1, 2, 3, 1, 3, 3, 2])
        >>> test.group_by_unique()
        [[1, 1], [2, 2], [3, 3, 3]]
        """
        temp = groupby(self.sorts())
        return self.__class__([list(g) for _, g in temp])

    @overload
    def group_by_type(self) -> defaultdict[type, ListExt[T]]: ...

    @overload
    def group_by_type(self, *, init_normal_list: Literal[True] = ...) -> defaultdict[type, list[T]]: ...

    @versionadded("6.8.0")
    def group_by_type(self, *, init_normal_list: bool = False) -> defaultdict[type, list[T] | ListExt[T]]:
        """
        Group items in list by type

        Parameters
        ----------
        init_normal_list : bool, optional
            Use ``defaultdict(list)`` instead of ``defaultdict(self.__class__)``, by default ``False``

        Returns
        -------
        defaultdict[type, list[T]]
            Grouped data
        """
        if init_normal_list:
            result = defaultdict(list)
        else:
            result = defaultdict(self.__class__)
        for x in self:
            result[type(x)].append(x)
        return result

    @versionadded("6.8.0")
    def group_connected_values(self, on_error: Literal["raise", "skip", "force"] = "raise") -> list[list]:
        """
        Group overlapping values into connected groups.

        Each item in ``self`` is treated as an iterable of connected values.
        Groups are merged when they share at least one common value.

        Parameters
        ----------
        on_error : Literal["raise", "skip", "force"]
            Behavior when an item is not iterable, by default ``"raise"``

            - ``"raise"``:
            Raise ``TypeError``.

            - ``"skip"``:
            Ignore invalid items.

            - ``"force"``:
            Treat invalid items as a single-value iterable.

        Returns
        -------
        list[list[Any]]
            List of grouped connected values.


        Notes
        -----
        - Order is preserved based on first appearance.
        - Duplicate values inside groups are removed.
        - Inner strings are treated as single values, not character iterables.
        - Behaves similarly to connected-component merging in graph theory.

        Example:
        --------
        Basic pair grouping:

        >>> test = ListExt([[1, 2], [2, 3], [4, 3], [5, 6]])
        >>> test.group_connected_values()
        [[1, 2, 3, 4], [5, 6]]

        Multiple overlapping groups:

        >>> test = ListExt([[8, 3], [4, 6], [6, 3], [5, 2], [7, 2]])
        >>> test.group_connected_values()
        [[8, 3, 4, 6], [5, 2, 7]]

        Mixed value types:

        >>> test = ListExt([["a", 4], ["b", 4], [5, "c"]])
        >>> test.group_connected_values()
        [['a', 4, 'b'], [5, 'c']]

        Inner iterables with more than two values:

        >>> test = ListExt([[1, 2, 3], [2, 3], [4, 3], [5, 6]])
        >>> test.group_connected_values()
        [[1, 2, 3, 4], [5, 6]]

        Force invalid items into single-value groups:

        >>> test = ListExt([[1, 2], 3, [4, 5]])
        >>> test.group_connected_values(on_error="force")
        [[1, 2], [3], [4, 5]]

        Skip invalid items:

        >>> test = ListExt([[1, 2], 3, [2, 4]])
        >>> test.group_connected_values(on_error="skip")
        [[1, 2, 4]]
        """
        groups: list[list] = []

        for values in self:
            try:
                values = list(values)
            except Exception as err:
                if on_error == "force":
                    values = [values]
                elif on_error == "skip":
                    continue
                elif on_error == "raise":
                    raise err

            matched: list[list] = []

            for group in groups:
                if any(v in group for v in values):
                    matched.append(group)

            if not matched:
                groups.append(values.copy())
                continue

            base = matched[0]

            for v in values:
                if v not in base:
                    base.append(v)

            for other in matched[1:]:
                for v in other:
                    if v not in base:
                        base.append(v)

                groups.remove(other)

        return groups


if __name__ == "__main__":
    from rich import print

    test = ListExt([1, 2, 3, 4, 5, "s"])

    a = test.numbering()
    print(a, type(a))

    # MAKE: group by first char
