from typing import Dict, Generator, Iterable, List, Optional, Tuple, List

class Instruction:
    def __init__(self, code):
        self.code = code

    def __eq__(self, other):
        return type(other) is type(self) and other.code == self.code

    def __ne__(self, other):
        return not self.__eq__(other)

    def __repr__(self):
        return f"<{type(self).__name__} {self.code}>"

class Stitches(Instruction):
    """Represents a stitch repeated a fixed number of times.
    
    Example: k3 (knit 3)
    """
    def __init__(self, code, count):
        super().__init__(f"{code}{count}")
        self.code = code
        self.count = count

class MarkerAction(Instruction):
    """Represents an action performed on a marker."""
    def __init__(self, action, name=""):
        super().__init__(f"{action}{name}")
        self.action = action
        self.name = name


class InstructionGroup:
    def __init__(self):
        self.items : List[Instruction] = list()

    def add_item(self, instruction : Instruction):
        self.items.append(instruction)

    def __repr__(self):
        return f"<{type(self).__name__} ({len(self.items)})>"

    def __eq__(self, other):
        if type(other) != type(self):
            return False
        if len(self.items) != len(other.items):
            return False
        for a, b in zip(self.items, other.items):
            if a != b:
                return False
        return True


class Comment:
    """Represents a comment placed anywhere in the pattern."""
    def __init__(self, text):
        self.text = text

    def __repr__(self):
        return f"<Comment: {self.text}>"

class Break:
    pass


class BoundedStitchRepeat(InstructionGroup, Instruction):
    """Represents a stitch repeated until
    some condition is met (e.g. the next marker is reached).

    Example: kte-3 (knit to 3 stitches before end)
    """
    def __init__(self, code, stitches_left, count, log_count):
        Instruction.__init__(self, code)
        InstructionGroup.__init__(self)
        self.stitches_left = stitches_left
        self.count = count
        self.log_count = log_count

    def __eq__(self, other):
        if not Instruction.__eq__(self, other):
            return False
        return self.count == other.count

    def __repr__(self):
        return f"<BoundedStitchRepeat {self.code}>"


class NamedInstructionGroup(InstructionGroup):
    """Represents a named sequence of instructions.
    """
    def __init__(self, name : str = ""):
        super().__init__()
        self.name = name

class RepeatedInstructionGroup(InstructionGroup):
    """Represents a sequence of stitches that is repeated
    a fixed number of times or until some condition is met.
    """
    def __init__(self, original_group : InstructionGroup):
        super().__init__()
        self.original_group = original_group
        self.repeat_type = None # marker|end|count
        self.stitches_left = 0
        self.repeat_count = None


class Row(InstructionGroup):
    def __init__(self, row_number, continued : bool = False):
        super().__init__()
        self.row_number : int = row_number
        self.continued : bool = continued
        self.tag = ""
        self.increase_count : Optional[int]= None
        self.log_increase_count = True

    def __repr__(self):
        return f"<Row {self.row_number}>"

    def __eq__(self, other):
        if type(other) != Row:
            return False
        return InstructionGroup.__eq__(self, other)


class RowGroup:
    """Represents a group of rows located sequentially in the pattern."""
    def __init__(self):
        self.items : List[Row|Break] = list()

    def __repr__(self):
        row_numbers = [row.row_number for row in self.rows]
        return f"<{type(self).__name__} {min(row_numbers)}-{max(row_numbers)}>"

    @property
    def rows(self):
        rows = list()
        for item in self.items:
            if isinstance(item, Row):
                rows.append(item)
        return rows


class RepeatedRow(RowGroup):
    def __init__(self, rows : List[Row]):
        super().__init__()
        self.items.extend(rows)

    @property
    def from_row(self):
        return self.rows[0].row_number

    @property
    def to_row(self):
        return self.rows[-1].row_number
    
    @property
    def row_items(self):
        return self.rows[0].items
    
class RepeatedRowGroup(RowGroup):
    """Represents a group of rows that are a repetition of
    a previously worked group of rows."""
    def __init__(self, original_row_group : RowGroup):
        super().__init__()
        self.original_row_group = original_row_group


class Section:
    """Represents a longer part of the pattern, like a sleeve or a yoke.

    :param title: The title of the section - may be left blank.
    """
    def __init__(self, title : str = ""):
        self.title = title
        self.items : List[Comment|Row|RowGroup|Break] = list()

    def __repr__(self):
        return f"<Section: {self.title or 'untitled'}>"

    def get_all_items(self) -> Generator[Comment|Row|Break, None, None]:
        """
        Returns all the rows in the subsection, in order.
        """
        for item in self.items:
            if isinstance(item, RowGroup):
                for subitem in item.items:
                    yield subitem
            else:
                yield item

    def get_diagram(self):
        line = list()
        lines = list()
        for item in self.get_all_items():
            if isinstance(item, Row):
                line.append(item)
            else:
                if line:
                    lines.append(line)
                line = list()
        if line:
            lines.append(line)
        return lines

    def group_rows(self, rows : List[Row]):
        if len(rows) == 1:
            return rows[0]
        return RepeatedRow(rows)

    def finalize(self):
        result = list()
        if len(self.items) < 1:
            return
        current_run = [self.items[0]]
        for item in self.items[1:]:
            if item == current_run[0]:
                current_run.append(item)
            else:
                result.append(self.group_rows(current_run))
                current_run = [item]
        result.append(self.group_rows(current_run))
        self.items = result


class Pattern:
    """Represents a written knitting pattern.
    
    :param title: The title of the pattern
    """
    def __init__(self, title : str = ""):
        self.title = title
        self.items : List[Section|Comment] = list()

    def get_all_items(self) -> Generator[Row|Comment|Break, None, None]:
        for item in self.items:
            if isinstance(item, Section):
                for subitem in item.get_all_items():
                    yield subitem

    def __repr__(self):
        return f"<Pattern: {self.title or 'untitled'}>"

