#!/usr/bin/env python
"""
Chord class - a set of pitches forming a connected subgraph in harmonic space.

A chord is a tuple of Pitches. Two chords are equivalent under
transposition if they have the same intervallic structure.
"""

from __future__ import annotations
from typing import Iterator

from .pitch import Pitch


class Chord:
    def __init__(self, pitches: tuple[Pitch, ...], dims: tuple[int, ...] | None = None):
        from .pitch import DIMS_7

        self.dims = dims if dims is not None else DIMS_7
        self._pitches = pitches

    def __hash__(self) -> int:
        return hash(self._pitches)

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Chord):
            return NotImplemented
        return self._pitches == other._pitches

    def __repr__(self) -> str:
        return f"Chord({self._pitches})"

    def __iter__(self) -> Iterator[Pitch]:
        return iter(self._pitches)

    def __len__(self) -> int:
        return len(self._pitches)

    def __getitem__(self, index: int) -> Pitch:
        return self._pitches[index]

    @property
    def pitches(self) -> tuple[Pitch, ...]:
        """Get the pitches as a tuple."""
        return self._pitches

    @property
    def collapsed_pitches(self) -> set[Pitch]:
        """Get all pitches collapsed to pitch class."""
        return set(p.collapse() for p in self._pitches)

    def is_connected(self) -> bool:
        """
        Check if the chord forms a connected subgraph in harmonic space.

        A set is connected if every pitch can be reached from every other
        by stepping through adjacent pitches (differing by ±1 in one dimension).
        """
        if len(self._pitches) <= 1:
            return True

        adj = {p: set() for p in self._pitches}

        for i, p1 in enumerate(self._pitches):
            for p2 in self._pitches[i + 1 :]:
                if self._is_adjacent(p1, p2):
                    adj[p1].add(p2)
                    adj[p2].add(p1)

        visited = {self._pitches[0]}
        queue = [self._pitches[0]]

        while queue:
            current = queue.pop(0)
            for neighbor in adj[current]:
                if neighbor not in visited:
                    visited.add(neighbor)
                    queue.append(neighbor)

        return len(visited) == len(self._pitches)

    def _is_adjacent(self, p1: Pitch, p2: Pitch) -> bool:
        """Check if two pitches are adjacent (differ by ±1 in exactly one dimension).

        For collapsed harmonic space, skip dimension 0 (the octave dimension).
        """
        diff_count = 0
        for d in range(1, len(self.dims)):
            diff = abs(p1[d] - p2[d])
            if diff > 1:
                return False
            if diff == 1:
                diff_count += 1
        return diff_count == 1

    def symmetric_difference_size(self, other: Chord) -> int:
        """Calculate the size of symmetric difference between two chords."""
        set1 = set(p.collapse() for p in self._pitches)
        set2 = set(p.collapse() for p in other._pitches)
        return len(set1.symmetric_difference(set2))

    def size_difference(self, other: Chord) -> int:
        """Calculate the absolute difference in chord sizes."""
        return abs(len(self._pitches) - len(other._pitches))

    def project_all(self) -> list[Pitch]:
        """Project all pitches to [1, 2) range."""
        return [p.project() for p in self._pitches]

    def transpose(self, trans: Pitch) -> Chord:
        """Transpose the entire chord."""
        return Chord(tuple(p.transpose(trans) for p in self._pitches), self.dims)

    def sorted_by_frequency(self) -> list[Pitch]:
        """Sort pitches by frequency (low to high)."""
        return sorted(self._pitches, key=lambda p: p.to_fraction())
