import copy
from typing import Any, Union

from ._LooperParent import _LooperParent


class ListLooper(_LooperParent):
    r"""Outputs slices of a :obj:`list` using the metaphor of a looping window
    of a constant number of elements. This number is given by the argument
    :attr:`window_size`, which is an :obj:`int` representing how many elements
    are to be included in each slice.

    For instance, if the initial container had the elements
    ``[A, B, C, D, E, F]`` (where each letter represents an element of an
    arbitrary type) and the looping window was size ``3``, the output would be:

    ``[A B C] [B C D] [C D E] [D E F] [E F] [F]``

    This can be better visualised as:

    .. code-block:: none

        A B C
          B C D
            C D E
              D E F
                E F
                  F

    Basic usage:
        Calling the object will return a :obj:`list` generated by the looping
        process. Each call of the object will move the window forwards and
        output the result.

        >>> input_list = ['A', 'B', 'C', 'D', 'E', 'F']
        >>> looper = auxjad.ListLooper(input_list, window_size=3)
        >>> looper()
        ['A', 'B', 'C']
        >>> looper()
        ['B', 'C', 'D']

        The property :attr:`current_window` can be used to access the current
        window without moving the head forwards.

        >>> looper.current_window
        ['B', 'C', 'D']

    :attr:`process_on_first_call`:
        The very first call will output the input :obj:`list` without
        processing it. To disable this behaviour and have the looping window
        move on the very first call, initialise the class with the keyword
        argument :attr:`process_on_first_call` set to ``True``.

        >>> input_list = ['A', 'B', 'C', 'D', 'E', 'F']
        >>> looper = auxjad.ListLooper(input_list,
        ...                            window_size=3,
        ...                            process_on_first_call=True,
        ...                            )
        >>> looper()
        ['B', 'C', 'D']

    Using as iterator:
        The instances of this class can also be used as an iterator, which can
        then be used in a for loop to exhaust all windows.

        >>> input_list = ['A', 'B', 'C', 'D', 'E', 'F']
        >>> looper = auxjad.ListLooper(input_list,
        ...                            window_size=3,
        ...                            )
        >>> for window in looper:
        ...     print(window)
        ['A', 'B', 'C']
        ['B', 'C', 'D']
        ['C', 'D', 'E']
        ['D', 'E', 'F']
        ['E', 'F']
        ['F']

    Arguments and properties:
        This class can take many optional keyword arguments during its
        creation. :attr:`step_size` dictates the size of each individual step
        in number of elements (default value is ``1``). :attr:`max_steps` sets
        the maximum number of steps that the window can advance when the object
        is called, ranging between ``1`` and the input value (default is also
        ``1``). :attr:`repetition_chance` sets the chance of a window result
        repeating itself (that is, the window not moving forwards when called).
        It should range from ``0.0`` to ``1.0`` (default ``0.0``, i.e. no
        repetition). :attr:`forward_bias` sets the chance of the window moving
        forward instead of backwards. It should range from ``0.0`` to ``1.0``
        (default ``1.0``, which means the window can only move forwards. A
        value of ``0.5`` gives 50% chance of moving forwards while a value of
        ``0.0`` will move the window only backwards). :attr:`head_position` can
        be used to offset the starting position of the looping window. It must
        be an :obj:`int` and its default value is ``0``. By default, calling
        the object will first return the original container and subsequent
        calls will process it; set :attr:`process_on_first_call` to ``True``
        and the looping process will be applied on the very first call. Lastly,
        set :attr:`end_with_max_n_elements` to ``True`` to end the process when
        the final window has the maximum number of elements.

        >>> input_list = ['A', 'B', 'C', 'D', 'E', 'F']
        >>> looper = auxjad.ListLooper(input_list,
        ...                            window_size=3,
        ...                            step_size=1,
        ...                            max_steps=2,
        ...                            repetition_chance=0.25,
        ...                            forward_bias=0.2,
        ...                            head_position=0,
        ...                            end_with_max_n_elements=True,
        ...                            process_on_first_call=True,
        ...                            )
        >>> looper.window_size
        3
        >>> looper.step_size
        1
        >>> looper.repetition_chance
        0.25
        >>> looper.forward_bias
        0.2
        >>> looper.max_steps
        2
        >>> looper.head_position
        0
        >>> looper.end_with_max_n_elements
        True
        >>> looper.process_on_first_call
        True

        Use the properties below to change these values after initialisation.

        >>> looper.window_size = 2
        >>> looper.step_size = 2
        >>> looper.max_steps = 3
        >>> looper.repetition_chance = 0.1
        >>> looper.forward_bias = 0.8
        >>> looper.head_position = 2
        >>> looper.end_with_max_n_elements = False
        >>> looper.process_on_first_call = False
        >>> looper.window_size
        2
        >>> looper.step_size
        2
        >>> looper.max_steps
        3
        >>> looper.repetition_chance
        0.1
        >>> looper.forward_bias
        0.8
        >>> looper.head_position
        2
        >>> looper.end_with_max_n_elements
        False
        >>> looper.process_on_first_call
        False

    Setting :attr:`forward_bias` to ``0.0``:
        Set :attr:`forward_bias` to ``0.0`` to move backwards instead of
        forwards (default is ``1.0``). The initial :attr:`head_position` must
        be greater than ``0`` otherwise the contents will already be exhausted
        in the very first call (since it will not be able to move backwards
        from that position).

        >>> input_list = ['A', 'B', 'C', 'D']
        >>> looper = auxjad.ListLooper(input_list,
        ...                            window_size=2,
        ...                            head_position=2,
        ...                            forward_bias=0.0,
        ...                            )
        >>> looper.output_all()
        ['C', 'D', 'B', 'C', 'A', 'B']

    :attr:`forward_bias` between ``0.0`` and ``1.0``:
        Setingt :attr:`forward_bias` to a value in between ``0.0`` and ``1.0``
        will result in random steps being taken forward or backward, according
        to the bias. The initial value of :attr:`head_position` will once gain
        play an important role here, as the contents might be exhausted if the
        looper attempts to move backwards after reaching the head position
        ``0``.

        >>> input_list = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
        >>> looper = auxjad.ListLooper(input_list,
        ...                            window_size=2,
        ...                            head_position=4,
        ...                            forward_bias=0.5,
        ...                            )
        >>> looper.output_n(4)
        ['E', 'F', 'D', 'E', 'C', 'D', 'B', 'C']

    :attr:`max_steps`:
        Setting the keyword argument :attr:`max_steps` to a value larger than
        ``1`` will result in a random number of steps (between ``1`` and
        :attr:`max_steps`) being applied at each call.

        >>> input_list = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
        >>> looper = auxjad.ListLooper(input_list,
        ...                            window_size=2,
        ...                            head_position=2,
        ...                            max_steps=4,
        ...                            )
        >>> looper.output_n(4)
        ['C', 'D', 'D', 'E', 'E', 'F', 'H']

    :func:`len()`:
        The function :func:`len()` can be used to get the total number of
        elements in the container.

        >>> input_list = ['A', 'B', 'C', 'D', 'E', 'F']
        >>> looper = auxjad.ListLooper(input_list, window_size=3)
        >>> len(looper)
        6

    :meth:`output_all`:
        To run through the whole process and output it as a single :obj:`list`,
        from the initial head position until the process outputs the single
        last element, use the method :meth:`output_all`.

        >>> input_list = ['A', 'B', 'C', 'D']
        >>> looper = auxjad.ListLooper(input_list, window_size=3)
        >>> looper.output_all()
        ['A', 'B', 'C', 'B', 'C', 'D', 'C', 'D', 'D']

    :meth:`output_n`:
        To run through just part of the process and output it as a single
        :obj:`list`, starting from the initial head position, use the method
        :meth:`output_n` and pass the number of iterations as argument.

        >>> input_list = ['A', 'B', 'C', 'D']
        >>> looper = auxjad.ListLooper(input_list, window_size=3)
        >>> looper.output_n(2)
        ['A', 'B', 'C', 'B', 'C', 'D']

    :attr:`end_with_max_n_elements`:
        When ``True``, the last bar in the output will contain the maximum
        number of leaves given by :attr:`window_size`. E.g. consider the
        elements ``[A, B, C, D]`` and the looping window was size ``3``;
        setting :attr:`end_with_max_n_elements` to ``True`` will output:

        ``[A B C] [B C D]``

        Setting it to ``False`` (which is this property's default value) will
        produces:

        ``[A B C] [B C D] [C D] [D]``

        Compare the two examples below:

        >>> input_list = ['A', 'B', 'C', 'D']
        >>> looper = auxjad.ListLooper(input_list, window_size=3)
        >>> looper.output_all()
        ['A', 'B', 'C', 'B', 'C', 'D', 'C', 'D', 'D']
        >>> input_list = ['A', 'B', 'C', 'D']
        >>> looper = auxjad.ListLooper(input_list,
        ...                            window_size=3,
        ...                            end_with_max_n_elements=True,
        ...                            )
        >>> looper.output_all()
        ['A', 'B', 'C', 'B', 'C', 'D']

    :attr:`window_size`:
        To change the size of the looping window after instantiation, use the
        property :attr:`window_size`. In the example below, the initial window
        is of size ``3``, and so the first call of the looper object outputs
        the first, second, and third elements of the :obj:`list`. The window
        size is then set to ``4``, and the looper is called again, moving to
        the element in the next position, thus outputting the second, third,
        fourth, and fifth elements.

        >>> input_list = ['A', 'B', 'C', 'D', 'E', 'F']
        >>> looper = auxjad.ListLooper(input_list, window_size=3)
        >>> looper()
        ['A', 'B', 'C']
        >>> looper.window_size = 4
        >>> looper()
        ['B', 'C', 'D', 'E']

    :attr:`contents`:
        Use the :attr:`contents` property to read as well as overwrite the
        contents of the looper. Notice that the :attr:`head_position` will
        remain on its previous value and must be reset to ``0`` if that's
        required.

        >>> input_list = ['A', 'B', 'C', 'D', 'E', 'F']
        >>> looper = auxjad.ListLooper(input_list,
        ...                            window_size=3,
        ...                            )
        >>> looper.contents
        ['A', 'B', 'C', 'D', 'E', 'F']
        >>> looper()
        ['A', 'B', 'C']
        >>> looper()
        ['B', 'C', 'D']
        >>> looper.contents = [0, 1, 2, 3, 4]
        >>> looper.contents
        [0, 1, 2, 3, 4]
        >>> looper()
        [1, 2, 3]
        >>> looper.head_position = 0
        >>> looper()
        [0, 1, 2]

    Types in the input list:
        The input :obj:`list` can contain any types of elements:

        >>> input_list = [123, 'foo', (3, 4), 3.14]
        >>> looper = auxjad.ListLooper(input_list, window_size=3)
        >>> looper()
        [123, 'foo', (3, 4)]

        This also include Abjad's types. Abjad's exclusive membership
        requirement is respected since each call returns a
        :func:`copy.deepcopy` of the window. The same is true to the
        :meth:`output_all` method.

        >>> import abjad
        >>> import copy
        >>> input_list = [
        ...     abjad.Container(r"c'4 d'4 e'4 f'4"),
        ...     abjad.Container(r"fs'1"),
        ...     abjad.Container(r"r2 bf4 c'4"),
        ...     abjad.Container(r"c''2. r4"),
        ... ]
        >>> looper = auxjad.ListLooper(input_list, window_size=3)
        >>> staff = abjad.Staff()
        >>> for element in looper.output_all():
        ...     staff.append(element)
        >>> abjad.show(staff)

        ..  docs::

            \new Staff
            {
                {
                    c'4
                    d'4
                    e'4
                    f'4
                }
                {
                    fs'1
                }
                {
                    r2
                    bf4
                    c'4
                }
                {
                    fs'1
                }
                {
                    r2
                    bf4
                    c'4
                }
                {
                    c''2.
                    r4
                }
                {
                    r2
                    bf4
                    c'4
                }
                {
                    c''2.
                    r4
                }
                {
                    c''2.
                    r4
                }
            }

        ..  figure:: ../_images/ListLooper-kvxaoz53y5f.png
    """

    ### CLASS VARIABLES ###

    __slots__ = "_end_with_max_n_elements"

    ### INITIALISER ###

    def __init__(
        self,
        contents: list[Any],
        *,
        window_size: int,
        step_size: int = 1,
        max_steps: int = 1,
        repetition_chance: float = 0.0,
        forward_bias: float = 1.0,
        head_position: int = 0,
        end_with_max_n_elements: bool = False,
        process_on_first_call: bool = False,
    ) -> None:
        self.contents = contents
        self.end_with_max_n_elements = end_with_max_n_elements
        super().__init__(
            head_position=head_position,
            window_size=window_size,
            step_size=step_size,
            max_steps=max_steps,
            repetition_chance=repetition_chance,
            forward_bias=forward_bias,
            process_on_first_call=process_on_first_call,
        )

    ### SPECIAL METHODS ###

    def __repr__(self) -> str:
        r"""Returns interpreter representation of :attr:`contents`."""
        return str(self._contents)

    def __len__(self) -> int:
        r"""Returns a length of :attr:`contents`."""
        return len(self._contents)

    ### PUBLIC METHODS ###

    def output_all(self) -> list[Any]:
        r"""Goes through the whole looping process and outputs a single
        :obj:`list`. This method replaces the parent's one since the parent's
        method outputs an |abjad.Selection|.
        """
        dummy_container = []
        while True:
            try:
                dummy_container.extend(self.__call__())
            except RuntimeError:
                break
        return dummy_container[:]

    def output_n(self, n: int) -> list[Any]:
        r"""Goes through ``n`` iterations of the looping process and outputs a
        single :obj:`list`. This method replaces the parent's one since the
        parent's method outputs an |abjad.Selection|.
        """
        if not isinstance(n, int):
            raise TypeError("argument must be 'int'")
        if n < 0:
            raise ValueError("argument must be a positive 'int'")
        dummy_container = []
        for _ in range(n):
            dummy_container.extend(self.__call__())
        return dummy_container[:]

    ### PRIVATE METHODS ###

    def _slice_contents(self) -> None:
        r"""This method takes a slice with :attr:`window_size` number of
        elements out of :attr:`contents` starting at the current
        :attr:`head_position`.
        """
        start = self._head_position
        end = self._head_position + self._window_size
        self._current_window = self._contents[start:end]

    ### PUBLIC PROPERTIES ###

    @property
    def contents(self) -> list[Any]:
        r"""The :obj:`list` to be sliced and looped."""
        return self._contents

    @contents.setter
    def contents(
        self,
        contents: list[Any],
    ) -> None:
        if not isinstance(contents, list):
            raise TypeError("'contents' must be 'list")
        self._contents = contents[:]
        self._is_first_window = True

    @property
    def current_window(self) -> Union[list[Any], None]:
        r"""Read-only property, returns the window at the current head
        position.
        """
        if self._current_window is None:
            return self._current_window
        return copy.deepcopy(self._current_window)[:]

    @property
    def end_with_max_n_elements(self) -> bool:
        r"""When ``True``, the last bar in the output will contain the maximum
        number of elements given by :attr:`window_size`. E.g. consider the
        elements ``[A, B, C, D]`` and the looping window was size ``3``;
        setting :attr:`end_with_max_n_elements` to ``True`` will output:

        ``[A B C] [B C D]``

        Setting it to ``False`` (which is this property's default value) will
        produces:

        ``[A B C] [B C D] [C D] [D]``
        """
        return self._end_with_max_n_elements

    @end_with_max_n_elements.setter
    def end_with_max_n_elements(
        self,
        end_with_max_n_elements: bool,
    ) -> None:
        if not isinstance(end_with_max_n_elements, bool):
            raise TypeError("'end_with_max_n_elements' must be 'bool'")
        self._end_with_max_n_elements = end_with_max_n_elements

    ### PRIVATE PROPERTIES ###

    @property
    def _done(self) -> bool:
        r""":obj:`bool` indicating whether the process is done (i.e. whether
        the head position has overtaken the :attr:`contents`'s length).
        """
        if self._end_with_max_n_elements:
            return (
                self._head_position + self._window_size > self.__len__() or self._head_position < 0
            )
        else:
            return self._head_position >= self.__len__() or self._head_position < 0
