#!/usr/bin/env python
"""
Pitch class - a point in harmonic space.

Represented as an array of exponents on prime dimensions.
Example: (0, 1, 0, 0) represents 3/2 (perfect fifth) in CHS_7
"""

from __future__ import annotations
from fractions import Fraction
from math import log
from operator import add
from typing import Iterator

from .dims import DIMS_4, DIMS_5, DIMS_7, DIMS_8


class Pitch:
    def __init__(self, hs_array: tuple[int, ...], dims: tuple[int, ...] | None = None):
        self.hs_array = hs_array
        self.dims = dims if dims is not None else DIMS_7

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

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

    def __repr__(self) -> str:
        return f"Pitch({self.hs_array})"

    def __iter__(self):
        return iter(self.hs_array)

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

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

    def to_fraction(self) -> Fraction:
        """Convert to frequency ratio (e.g., 3/2)."""
        from fractions import Fraction

        numerator = 1
        denominator = 1
        for d in range(len(self.dims)):
            exp = self.hs_array[d]
            if exp >= 0:
                numerator *= self.dims[d] ** exp
            else:
                denominator *= self.dims[d] ** (-exp)
        return Fraction(numerator, denominator)

    def to_cents(self) -> float:
        """Convert to cents (relative to 1/1 = 0 cents)."""
        fr = self.to_fraction()
        return 1200 * log(float(fr), 2)

    def collapse(self) -> Pitch:
        """
        Collapse pitch so frequency ratio is in [1, 2).

        This removes octave information, useful for pitch classes.
        """
        collapsed = list(self.hs_array)
        fr = self.to_fraction()

        if fr < 1:
            while fr < 1:
                fr *= 2
                collapsed[0] += 1
        elif fr >= 2:
            while fr >= 2:
                fr /= 2
                collapsed[0] -= 1

        return Pitch(tuple(collapsed), self.dims)

    def project(self) -> Pitch:
        """Project pitch to [1, 2) range - same as collapse."""
        return self.collapse()

    def transpose(self, trans: Pitch) -> Pitch:
        """Transpose by another pitch (add exponents element-wise)."""
        return Pitch(tuple(map(add, self.hs_array, trans.hs_array)), self.dims)

    def pitch_difference(self, other: Pitch) -> Pitch:
        """Calculate the pitch difference (self - other)."""
        return Pitch(
            tuple(self.hs_array[d] - other.hs_array[d] for d in range(len(self.dims))),
            self.dims,
        )
