import copy
from typing import Union

from ._LoopParent import _LoopParent


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

    For instance, if the initial container had the logical ties
    ``[A, B, C, D, E, F]`` (where each letter represents one elemnt 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

    Example:
        Calling the object will return a ``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.LoopByList(input_list, window_size=3)
        >>> looper()
        ['A', 'B', 'C']
        >>> looper()
        ['B', 'C', 'D']

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

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

    Example:
        The very first call will output the input 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
        ``processs_on_first_call`` set to ``True``.

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

    Example:
        The instances of ``LoopByList`` 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.LoopByList(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']

    Example:
        This class can take many optional keyword arguments during its
        creation. ``step_size`` dictates the size of each individual step in
        number of elements (default value is ``1``). ``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``). ``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). ``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). Lastly, ``head_position``
        can be used to offset the starting position of the looping window. It
        must be an integer and its default value is ``0``. By default, calling
        the object will first return the original container and subsequent
        calls will process it; set ``processs_on_first_call`` to ``True`` and
        the looping process will be applied on the very first call.

        >>> input_list = ['A', 'B', 'C', 'D', 'E', 'F']
        >>> looper = auxjad.LoopByList(input_list,
        ...                            window_size=3,
        ...                            step_size=1,
        ...                            max_steps=2,
        ...                            repetition_chance=0.25,
        ...                            forward_bias=0.2,
        ...                            head_position=0,
        ...                            processs_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.processs_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.processs_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.processs_on_first_call
        False

    Example:
        Set ``forward_bias`` to ``0.0`` to move backwards instead of forwards
        (default is ``1.0``). The initial ``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.LoopByList(input_list,
        ...                            window_size=2,
        ...                            head_position=2,
        ...                            forward_bias=0.0,
        ...                            )
        >>> looper.output_all()
        ['C', 'D', 'B', 'C', 'A', 'B']

    Example:
        Setingt ``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 ``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.LoopByList(input_list,
        ...                            window_size=2,
        ...                            head_position=4,
        ...                            forward_bias=0.5,
        ...                            )
        >>> looper.output_n(4)
        ['E', 'F', 'D', 'E', 'C', 'D', 'B', 'C']

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

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

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

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

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

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

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

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

    Example:
        To change the size of the looping window after instantiation, use the
        property ``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 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.LoopByList(input_list, window_size=3)
        >>> looper()
        ['A', 'B', 'C']
        >>> looper.window_size = 4
        >>> looper()
        ['B', 'C', 'D', 'E']

    Example:
        Use the ``contents`` property to read as well as overwrite the contents
        of the looper. Notice that the ``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.LoopByList(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]

    Example:
        It should be clear that the list can contain any types of elements:

        >>> input_list = [123, 'foo', (3, 4), 3.14]
        >>> looper = auxjad.LoopByList(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 ``copy.deepcopy`` of
        the window. The same is true to the ``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.LoopByList(input_list, window_size=3)
        >>> staff = abjad.Staff()
        >>> for element in looper.output_all():
        ...     staff.append(element)
        >>> abjad.f(staff)
        \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/image-LoopByList-1.png
    """

    ### CLASS VARIABLES ###

    __slots__ = ()

    ### INITIALISER ###

    def __init__(self,
                 contents: list,
                 *,
                 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,
                 processs_on_first_call: bool = False,
                 ):
        r'Initialises self.'
        self.contents = contents
        super().__init__(head_position,
                         window_size,
                         step_size,
                         max_steps,
                         repetition_chance,
                         forward_bias,
                         processs_on_first_call,
                         )

    ### SPECIAL METHODS ###

    def __repr__(self) -> str:
        r'Returns interpret representation of ``contents``.'
        return str(self._contents)

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

    ### PUBLIC METHODS ###

    def output_all(self) -> list:
        r"""Goes through the whole looping process and outputs a single 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:
        r"""Goes through ``n`` iterations of the looping process and outputs a
        single 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):
        r"""This method takes a slice with ``window_size`` number of elements
        out of the contents starting at the current ``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:
        r'The ``list`` to be sliced and looped.'
        return self._contents

    @contents.setter
    def contents(self,
                 contents: list,
                 ):
        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, 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)[:]
