"""Course representation of a section of knitting with no parent loops.

This module contains the Course class which represents a horizontal row of loops in a knitting pattern.
"""

from __future__ import annotations

from collections.abc import Iterator, Sequence
from typing import TYPE_CHECKING, Generic, TypeVar, overload

from knit_graphs.Loop import Loop

if TYPE_CHECKING:
    from knit_graphs.Knit_Graph import Knit_Graph

LoopT = TypeVar("LoopT", bound=Loop)


class Course(Sequence[LoopT], Generic[LoopT]):
    """Course object for organizing loops into knitting rows.

    A Course represents a horizontal row of loops in a knitting pattern.
    It maintains an ordered list of loops and provides methods for analyzing the structure and relationships between courses in the knitted fabric.
    """

    def __init__(self, course_number: int, knit_graph: Knit_Graph[LoopT]) -> None:
        """Initialize an empty course associated with a knit graph.

        Args:
            knit_graph (Knit_Graph): The knit graph that this course belongs to.
        """
        self._course_number: int = course_number
        self._knit_graph: Knit_Graph[LoopT] = knit_graph
        self._loops_in_order: list[LoopT] = []
        self._loop_set: set[LoopT] = set()

    @property
    def course_number(self) -> int:
        """
        Returns:
            int: The course number of the course.
        """
        return self._course_number

    @property
    def loops_in_order(self) -> list[LoopT]:
        """
        Returns:
            list[Loop]: The list of loops in this course.
        """
        return self._loops_in_order

    @property
    def loop_ids_in_course(self) -> list[int]:
        """
        Returns:
            list[int]: The loop ids in the course in the order the occur in the course.
        """
        return [l.loop_id for l in self.loops_in_order]

    @property
    def knit_graph(self) -> Knit_Graph[LoopT]:
        """
        Returns:
            Knit_Graph: The knit graph that this course belongs to.
        """
        return self._knit_graph

    def add_loop(self, loop: LoopT, index: int | None = None) -> None:
        """Add a loop to the course at the specified index or at the end.

        Args:
            loop (Loop): The loop to add to this course.
            index (int | None, optional): The index position to insert the loop at. If None, appends to the end.

        Raises:
            ValueError: If the loop is a parent to any loops already in the course.
        """
        for parent_loop in loop.parent_loops:
            if parent_loop in self:
                raise ValueError(f"{loop} has parent {parent_loop}, cannot be added to same course")
        self._loop_set.add(loop)
        if index is None:
            self.loops_in_order.append(loop)
        else:
            self.loops_in_order.insert(index, loop)

    def has_increase(self) -> bool:
        """Check if this course contains any yarn overs that start new wales.

        Returns:
            bool: True if the course has at least one yarn over (loop with no parent loops) to start new wales.
        """
        return any(not loop.has_parent_loops for loop in self)

    def has_decrease(self) -> bool:
        """Check if this course contains any decrease stitches that merge wales.

        Returns:
            bool: True if the course has at least one decrease stitch (loop with multiple parent loops) merging two or more wales.
        """
        return any(len(loop.parent_loops) > 1 for loop in self)

    def in_round_with(self, next_course: Course[LoopT]) -> bool:
        """Check if the next course connects to this course in a circular pattern.

        This method determines if the courses are connected in the round (circular knitting) by checking if the next course starts at the beginning of this course.

        Args:
            next_course (Course): The course that should follow this course in circular knitting.

        Returns:
            bool: True if the next course starts at the beginning of this course, indicating circular knitting.
        """
        next_start = next_course[0]
        i = 1
        while not next_start.has_parent_loops:
            next_start = next_course[i]
            i += 1
        return self[0] in next_start.parent_loops

    def in_row_with(self, next_course: Course[LoopT]) -> bool:
        """Check if the next course connects to this course in a flat/row pattern.

        This method determines if the courses are connected in flat knitting (back and forth) by checking if the next course starts at the end of this course.

        Args:
            next_course (Course): The course that should follow this course in flat knitting.

        Returns:
            bool: True if the next course starts at the end of this course, indicating flat/row knitting.
        """
        next_start: LoopT = next_course[0]
        i = 1
        while not next_start.has_parent_loops:
            next_start = next_course[i]
            i += 1
        return self[-1] in next_start.parent_loops

    def __contains__(self, loop: object) -> bool:
        """Check if a loop is contained in this course.

        Args:
            loop (LoopT): The loop to check for membership in this course.

        Returns:
            bool: True if the loop is in this course, False otherwise.
        """
        return loop in self._loop_set

    @overload
    def __getitem__(self, index: int) -> LoopT: ...

    @overload
    def __getitem__(self, index: slice) -> list[LoopT]: ...

    def __getitem__(self, index: int | slice) -> LoopT | list[LoopT]:
        """Get loop(s) at the specified index or slice.

        Args:
            index (int | slice): The index or slice to retrieve from the course.

        Returns:
            Loop | list[Loop]: The loop at the specified index, or list of loops for a slice.
        """
        return self.loops_in_order[index]

    def __iter__(self) -> Iterator[LoopT]:
        """Iterate over loops in this course in order.

        Returns:
            Iterator[Loop]: An iterator over the loops in this course in their natural order.
        """
        return iter(self.loops_in_order)

    def __len__(self) -> int:
        """Get the number of loops in this course.

        Returns:
            int: The total number of loops in this course.
        """
        return len(self.loops_in_order)

    def __str__(self) -> str:
        """Get string representation of this course.

        Returns:
            str: String representation showing the ordered list of loops.
        """
        return f"Course {self.course_number}: {self.loops_in_order}"

    def __repr__(self) -> str:
        """Get string representation of this course for debugging.

        Returns:
            str: String representation showing the ordered list of loops.
        """
        return str(self)
