"""
Absfuyu: Data Extension
-----------------------
list extension

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


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


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

from absfuyu.core.baseclass import GetClassMembersMixin
from absfuyu.core.decorator import deprecated as wdeprecated
from absfuyu.core.docstring import deprecated, versionadded, versionchanged, pendingremove
from absfuyu.util import set_min_max

# Type
# ---------------------------------------------------------------------------
T = TypeVar("T")
R = TypeVar("R")  # Return type - Can be anything


# Class
# ---------------------------------------------------------------------------
class ListExt(GetClassMembersMixin, list):
    """
    ``list`` extension

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

    # Deprecated
    @wdeprecated(version="5.2.0", reason="Use ListExt.apply(str) instead")
    @pendingremove("7.0.0", reason="Deprecated")
    @deprecated("5.2.0", reason="Use ListExt.apply(str) instead")
    def stringify(self) -> Self:
        """
        Convert all item in ``list`` into string

        Returns
        -------
        Self
            A list with all items with type <str`>


        Example:
        --------
        >>> test = ListExt([1, 1, 1, 2, 2, 3])
        >>> test.stringify()
        ['1', '1', '1', '2', '2', '3']
        """
        return self.__class__(map(str, self))

    @wdeprecated(version="6.8.0", reason="Use ``group_connected_values()`` method")
    @pendingremove("7.0.0", reason="Deprecated")
    @deprecated("6.8.0", reason="Use ``group_connected_values()`` method")
    def group_by_pair_value(self, max_loop: int = 3) -> list[list]:
        """
        Assume each ``list`` in ``list`` is a pair value,
        returns a ``list`` contain all paired value

        Parameters
        ----------
        max_loop : int
            Times to run functions (minimum: ``3``)

        Returns
        -------
        list[list]
            Grouped value


        Example:
        --------
        >>> test = ListExt([[1, 2], [2, 3], [4, 3], [5, 6]])
        >>> test.group_by_pair_value()
        [[1, 2, 3, 4], [5, 6]]

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

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

        iter = self.copy()

        # Init loop
        for _ in range(max(max_loop, 3)):
            temp: dict[Any, list] = {}
            # Make dict{key: all `item` that contains `key`}
            for item in iter:
                for x in item:
                    if temp.get(x, None) is None:
                        temp[x] = [item]
                    else:
                        temp[x].append(item)

            # Flatten dict.values
            temp = {k: list(set(chain(*v))) for k, v in temp.items()}

            iter = list(temp.values())

        return list(x for x, _ in groupby(iter))

    # Info
    def head(self, number_of_items: int = 5) -> list:
        """
        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) -> list:
        """
        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]

    # Misc
    def apply(self, func: Callable[[Any], Any]) -> Self:
        """
        Apply function to each entry

        Parameters
        ----------
        func : Callable
            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)

    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)

    @overload
    def freq(self) -> dict: ...  # type: ignore

    @overload
    def freq(
        self,
        sort: bool = False,
        num_of_first_char: int | None = None,
    ) -> dict: ...

    @overload
    def freq(
        self,
        sort: bool = False,
        num_of_first_char: int | None = None,
        appear_increment: Literal[True] = ...,
    ) -> list[int]: ...

    def freq(
        self,
        sort: bool = False,
        num_of_first_char: int | None = None,
        appear_increment: bool = False,
    ) -> dict | list[int]:
        """
        Find frequency of each item in list

        Parameters
        ----------
        sort : bool, optional
            - ``True``: Sorts the output in ascending order
            - ``False``: No sort (default value)

        num_of_first_char : int | None, optional
            | Number of first character taken into account to sort, by default ``None``
            | Eg: ``num_of_first_char = 1``: first character of each item

        appear_increment : bool, optional
            Returns incremental index list of each item when sort,
            by default ``False``

        Returns
        -------
        dict
            A dict that show frequency

        list[int]
            Incremental index list


        Example:
        --------
        >>> test = ListExt([1, 1, 2, 3, 5, 5])
        >>> test.freq()
        {1: 2, 2: 1, 3: 1, 5: 2}

        >>> test = ListExt([1, 1, 2, 3, 3, 4, 5, 6])
        >>> test.freq(appear_increment=True)
        [2, 3, 5, 6, 7, 8]
        """
        if sort:
            data = self.sorts().copy()
        else:
            data = self.copy()

        if num_of_first_char is None:
            temp = Counter(data)
        else:
            max_char: int = min([len(str(x)) for x in data])
            # logger.debug(f"Max character: {max_char}")
            if num_of_first_char not in range(1, max_char):
                # logger.debug(f"Not in {range(1, max_char)}. Using default value...")
                temp = Counter(data)
            else:
                # logger.debug(f"Freq of first {num_of_first_char} char")
                temp = Counter([str(x)[:num_of_first_char] for x in data])

        try:
            times_appear = dict(sorted(temp.items()))
        except TypeError:
            times_appear = dict(self.__class__(temp.items()).sorts())
        # logger.debug(times_appear)

        if appear_increment:
            times_appear_increment: list[int] = list(accumulate(times_appear.values(), operator.add))
            # logger.debug(times_appear_increment)
            return times_appear_increment  # incremental index list
        else:
            return times_appear  # character frequency

    @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)
        except TypeError:
            return sep.join(self.apply(str))

    @overload
    def numbering(self) -> Self: ...

    @overload
    def numbering(self, start: int | float = 0) -> Self: ...

    @overload
    def numbering(self, *, step: int | float = 1) -> Self: ...

    @overload
    def numbering(self, start: int | float = 0, step: int | float = 1) -> Self: ...

    @versionchanged("5.5.0", "Use itertools.count to wrap")
    def numbering(self, start: int | float = 0, step: int | float = 1) -> Self:
        """
        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))

    @versionadded("5.3.0")  # no test case yet
    def transpose(self, fillvalue: Any | None = None, /) -> Self:
        """
        Transpose a list of iterable.

        Parameters
        ----------
        fillvalue : Any, optional
            A fill value, by default ``None``

        Returns
        -------
        Self | list[list[T]]
            Transposed list.


        Example:
        --------
        >>> ListExt([1, 1, 1, 1]).transpose()
        [(1, 1, 1, 1)]

        >>> ListExt([[1, 1, 1, 1], [1, 1, 1, 1]]).transpose()
        [(1, 1), (1, 1), (1, 1), (1, 1)]

        >>> ListExt([[1, 1, 1, 1], [1, 1, 1, 1], [1]]).transpose()
        [(1, 1, 1), (1, 1, None), (1, 1, None), (1, 1, None)]
        """
        try:
            return self.__class__(zip_longest(*self, fillvalue=fillvalue)).apply(list)
        except TypeError:  # Dimension of 1
            mod_dat = self.apply(lambda x: [x])
            # return self.__class__(zip_longest(*mod_dat, fillvalue=fillvalue)).apply(list)
            return mod_dat

    # Random
    def pick_one(self) -> Any:
        """
        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:
        """
        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:
        random.shuffle(self)
        return self

    # Len
    @versionchanged("5.2.0", reason="Handle more type")
    def len_items(self) -> Self:
        """
        ``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])

    @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()))

    # 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) -> Self:
        """
        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]]
        """
        # Old
        # out = self.sorts().slice_points(self.freq(appear_increment=True))
        # return __class__(out[:-1])

        # New
        temp = groupby(self.sorts())
        return self.__class__([list(g) for _, g in temp])

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

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

    @versionadded("6.8.0")
    def group_by_type(self, *, init_normal_list: bool = False) -> defaultdict[type, list | Self]:
        """
        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

    @versionadded("5.5.0")
    def split_equal(self, n: int, sort: bool = True, reverse: bool = True) -> Self:
        """
        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)

    # Slicing
    def slice_points(self, points: list[int]) -> Self:
        """
        Splits a list into sublists based on specified split points (indices).

        This method divides the original list into multiple sublists. The ``points``
        argument provides the indices at which the list should be split.  The resulting
        list of lists contains the sublists created by these splits. The original
        list is not modified.

        Parameters
        ----------
        points : list
            A list of integer indices representing the points at which to split
            the list. These indices are *exclusive* of the starting sublist
            but *inclusive* of the ending sublist.

        Returns
        -------
        Self | list[list]
            A list of lists, where each inner list is a slice of the original list
            defined by the provided split points.


        Example:
        --------
        >>> test = ListExt([1, 1, 2, 3, 3, 4, 5, 6])
        >>> test.slice_points([2, 5])
        [[1, 1], [2, 3, 3], [4, 5, 6]]
        >>> test.slice_points([0, 1, 2, 3, 4, 5, 6, 7, 8])
        [[], [1], [1], [2], [3], [3], [4], [5], [6]]
        >>> test.slice_points([])
        [[1, 1, 2, 3, 3, 4, 5, 6]]
        """
        points.append(len(self))
        data = self.copy()
        # return [data[points[i]:points[i+1]] for i in range(len(points)-1)]
        return self.__class__(data[i1:i2] for i1, i2 in zip([0] + points[:-1], points))

    @versionadded("5.1.0")
    def split_chunk(self, chunk_size: int, /) -> Self:
        """
        Split list into smaller chunks

        Parameters
        ----------
        chunk_size : int
            Chunk size, minimum: ``1``

        Returns
        -------
        Self | list[list]
            List of chunk


        Example:
        --------
        >>> ListExt([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]).split_chunk(5)
        [[1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [1, 1]]
        """
        slice_points = list(range(0, len(self), max(chunk_size, 1)))[1:]
        return self.slice_points(slice_points)

    @versionadded("5.3.0")  # no test case yet
    def to_column(self, ncols: int, fillvalue: Any | None = None) -> Self:
        """
        Smart convert 1 dimension list to 2 dimension list,
        in which, number of columns = ``ncols``.

        Parameters
        ----------
        ncols : int
            Number of columns

        fillvalue : Any | None, optional
            Fill value, by default ``None``

        Returns
        -------
        Self
            Coulumned list.


        Example:
        --------
        >>> ins = ListExt(range(1, 20))

        >>> # Normal split chunk
        >>> ins.split_chunk(10)
        [
            [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
            [11, 12, 13, 14, 15, 16, 17, 18, 19]
        ]

        >>> # Column split chunk
        >>> ins.to_column(10)
        [
            [1, 3, 5, 7, 9, 11, 13, 15, 17, 19],
            [2, 4, 6, 8, 10, 12, 14, 16, 18, None]
        ]
        """
        num_of_col = max(ncols, 1)
        len_cols = len(self.split_chunk(num_of_col))
        return self.split_chunk(len_cols).transpose(fillvalue)

    @overload
    def wrap_to_column(self, width: int, /) -> Self: ...

    @overload
    def wrap_to_column(
        self,
        width: int,
        /,
        *,
        margin: int = 4,
        sep: str = "",
        fill: str = " ",
        transpose: bool = False,
    ) -> Self: ...

    @versionchanged("5.3.0", reason="New `sep`, `fill`, `transpose` parameters")
    @versionadded("5.2.0")  # no test case yet
    def wrap_to_column(
        self,
        width: int,
        /,
        *,
        margin: int = 4,
        sep: str = "",
        fill: str = " ",
        transpose: bool = False,
    ) -> Self:
        """
        Arrange list[str] items into aligned text columns (for printing)
        with automatic column count calculation.

        Parameters
        ----------
        width : int
            Total available display width (must be >= ``margin``)

        margin : int, optional
            Reserved space for borders/padding, should be an even number, by default ``4``

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

        fill : str, optional
            Fill character for spacing, must have the length of 1, by default ``" "``

        transpose : bool, optional
            Smart transpose the columns, by default ``False``

        Returns
        -------
        Self
            New instance type[list[str]] with formatted column strings.


        Example:
        --------
        >>> items = ListExt(["apple", "banana", "cherry", "date"])
        >>> print(items.wrap_to_column(30))
        ['apple  banana cherry ', 'date   ']

        >>> items.wrap_to_column(15)
        ['apple  ', 'banana ', 'cherry ', 'date   ']
        """

        max_item_length = self.max_item_len() + max(len(sep), 1)
        available_width = max(width, 4) - max(margin, 0)  # Set boundary
        if len(fill) != 1:
            fill = " "

        # Calculate how many columns of text
        column_count = max(1, available_width // max_item_length) if max_item_length > 0 else 1

        # splitted_chunk: list[list[str]] = self.split_chunk(cols)
        # mod_chunk = self.__class__(
        #     [[x.ljust(max_name_len, " ") for x in chunk] for chunk in splitted_chunk]
        # ).apply(lambda x: "".join(x))

        def mod_item(item: list[str]) -> str:
            # Set width for str item and join them together
            return sep.join(x.ljust(max_item_length, fill) for x in item)

        if transpose:
            mod_chunk = self.to_column(column_count, fillvalue="").apply(mod_item)
        else:
            mod_chunk = self.split_chunk(column_count).apply(mod_item)

        return mod_chunk
