""" A module to contain the abstract base class that all row generators inherit from. """

import logging
from typing import Dict, List, Tuple, Optional
from abc import ABCMeta, abstractmethod

from wheatley.row_generation.helpers import rounds
from wheatley.aliases import Row, Places
from wheatley.stroke import Stroke, HANDSTROKE
from wheatley.row_generation.helpers import generate_starting_row

class RowGenerator(metaclass=ABCMeta):
    """ Abstract base class for behaviours common to all row generators. """

    logger_name = "ROWGEN"

    def __init__(self, stage: int, start_row: Optional[str] = None) -> None:
        self.stage = stage
        self.custom_start_row = start_row
        self.start_row = generate_starting_row(stage, start_row)
        self.logger = logging.getLogger(self.logger_name)

        self._has_bob = False
        self._has_single = False
        self._index = 0
        self._row = self.start_row

    def reset(self) -> None:
        """ Reset the row generator. """
        self.logger.debug("Reset")

        self._has_bob = False
        self._has_single = False
        self._index = 0
        self._row = self.start_row

    def reset_calls(self) -> None:
        """ Clear the pending call flags. """
        self.logger.debug("Reset calls")
        self._has_bob = False
        self._has_single = False

    def next_row(self, stroke: Stroke) -> Row:
        """
        Generates the next row (with no calls).  `rg.next_row(s)` is exactly equivalent to
        `rg.next_row_and_calls(s)[0]`, and does not call `_gen_row` directly.
        """
        return self.next_row_and_calls(stroke)[0]

    def next_row_and_calls(self, stroke: Stroke) -> Tuple[Row, List[str]]:
        """ Generate the next row, and mutate state accordingly. """
        self._row, calls = self._gen_row_and_calls(self._row, stroke, self._index)

        self._index += 1

        self.logger.debug(self._row)

        return (self._row, calls)

    def set_bob(self) -> None:
        """ Set the flag that a bob has been made. """
        self._has_bob = True

    def set_single(self) -> None:
        """ Set the flag that a single has been made. """
        self._has_single = True

    def rounds(self) -> Row:
        """ Generate rounds of the stage given by this RowGenerator. """
        return rounds(self.stage)

    @abstractmethod
    def _gen_row(self, previous_row: Row, stroke: Stroke, index: int) -> Row:
        pass

    # If this isn't overridden by a child class, then we assume that it won't produce calls, and
    # therefore we simply return no calls on every Row).
    def _gen_row_and_calls(self, previous_row: Row, stroke: Stroke, index: int) -> Tuple[Row, List[str]]:
        return (self._gen_row(previous_row, stroke, index), [])

    def start_stroke(self) -> Stroke: # pylint: disable=no-self-use
        """ Gets the stroke of the first row.  This defaults to HANDSTROKE, but should be overridden by
        other RowGenerators if different start strokes are possible. """
        return HANDSTROKE

    def early_calls(self) -> Dict[int, List[str]]: # pylint: disable=no-self-use
        """
        Returns the calls that should be called **before** going into the changes.  This map
        goes from <number of rows before the first row of method> to <list of calls that should be
        called on that row>.  For example, if we call a 'single' at the start of Original (as in
        https://complib.org/composition/68549), then the `early_calls` should return
        `{2: ["Go Original"], 1: ["Single"]}`.

        If this is not overridden, then it is assumed that the child class doesn't want to call any
        calls, so we return a default empty set
        """
        return {}

    @abstractmethod
    def summary_string(self) -> str:
        """ Returns a short string summarising the RowGenerator.
        This should make grammatical sense when formatted with
        'Wheatley (will ring|is ringing) {summary_string}'.
        """

    def permute(self, row: Row, places: Places) -> Row:
        """ Permute a row by a place notation given by `places`. """
        new_row = list(row)

        i = 1
        if places and places[0] % 2 == 0:
            # Skip 1 for implicit lead when lowest pn is even
            i += 1

        while i < self.stage:
            if i in places:
                i += 1
                continue

            # If not in place, must swap, index is 1 less than place
            new_row[i - 1], new_row[i] = new_row[i], new_row[i - 1]
            i += 2

        return Row(new_row)
