"""Stack of intervals"""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/02_scale.ipynb.

# %% auto 0
__all__ = ['INTERVAL_NAMES', 'INTERVAL_HALF_STEPS', 'SCALES', 'd', 'INV_SCALES_BY_INTERVAL', 'INV_SCALES_BY_NAME', 'Scale',
           'semi_to_intvals']

# %% ../nbs/02_scale.ipynb 3
import numpy as np
import pandas as pd
from fastcore.all import *
from IPython.display import Audio
from itertools import combinations, accumulate
from collections import defaultdict

from . import Note, Chord, Interval

# %% ../nbs/02_scale.ipynb 4
INTERVAL_NAMES = {
    "1": "unison",
    "b2": "minor second",
    "2": "major second",
    "#2": "augmented second",
    "b3": "minor third",
    "3": "major third",
    "#3": "augmented third",
    "b4": "minor fourth",
    "4": "perfect fourth",
    "#4": "augmented fourth",
    "b5": "diminished fifth",
    "5": "perfect fifth",
    "#5": "augmented fifth",
    "b6": "minor sixth",
    "6": "major sixth",
    "#6": "augmented sixth",
    "b7": "minor seventh",
    "7": "major seventh",
    "8": "octave",
    "b9": "minor ninth",
    "9": "major ninth",
    "#9": "augmented ninth",
    "b10": "minor tenth",
    "10": "major tenth",
    "b11": "minor eleventh",
    "11": "major eleventh",
    "#11": "augmented eleventh",
    "b12": "minor twelfth",
    "12": "major twelfth",
    "b13": "minor thirteenth",
    "13": "major thirteenth",
    "#13": "augmented thirteenth"
}
INTERVAL_HALF_STEPS = {
    "1": 0,
    "#1": 1,
    "b2": 1,
    "2": 2,
    "#2": 3,
    "b3": 3,
    "3": 4,
    "b4": 4,
    "4": 5,
    "#4": 6,
    "b5": 6,
    "5": 7,
    "#5": 8,
    "b6": 8,
    "6": 9,
    "#6": 10,
    "b7": 10,
    "7": 11,
    "8": 12,
    "b9": 13,
    "9": 14,
    "#9": 15,
    "10": 16,
    "11": 17,
    "#11": 18,
    "12": 19,
    "b13": 20,
    "13": 21,
    "#13": 22,
    "14": 23,
    "15": 24
}


SCALES = {# Major modes
          "ionian": ["1", "2", "3", "4", "5", "6", "7"],
          "major": ["1", "2", "3", "4", "5", "6", "7"],
          "minor": ["1", "2", "b3", "4", "5", "b6", "b7"],
          "natural minor": ["1", "2", "b3", "4", "5", "b6", "b7"],
          "dorian": ["1", "2", "b3", "4", "5", "6", "b7"],
          "phrygian": ["1", "b2", "b3", "4", "5", "b6", "b7"],
          "lydian": ["1", "2", "3", "#4", "5", "6", "7"],
          "mixolydian": ["1", "2", "3", "4", "5", "6", "b7"],
          "aeolian": ["1", "2", "b3", "4", "5", "b6", "b7"],
          "locrian": ["1", "b2", "b3", "4", "b5", "b6", "b7"],
          # Melodic Minor modes
          "melodic minor": ["1", "2", "b3", "4", "5", "6", "7"],
          "dorian b2": ["1", "b2", "b3", "4", "5", "6", "b7"],
          "lydian augmented": ["1", "2", "3", "#4", "#5", "6", "7"],
          "lydian b7": ["1", "2", "3", "#4", "5", "6", "b7"],
          "lydian dominant": ["1", "2", "3", "#4", "5", "6", "b7"],
          "acoustic": ["1", "2", "3", "#4", "5", "6", "b7"],
          "mixolydian #11": ["1", "2", "3", "#4", "5", "6", "b7"],
          "locrian n2": ["1", "2", "b3", "4", "b5", "b6", "7"],
          "altered": ["1", "b2", "b3", "b4", "b5", "b6", "b7"],
          "altered dominant": ["1", "b2", "b3", "b4", "b5", "b6", "b7"],
          "diminished whole-tone": ["1", "b2", "b3", "b4", "b5", "b6", "b7"],
          "dominant whole-tone": ["1", "b2", "b3", "b4", "b5", "b6", "b7"],
          "aeolian dominant": ["1", "2", "3", "4", "5", "b6", "b7"],
          # Harmonic minor modes
          "harmonic minor": ["1", "2", "b3", "4", "5", "b6", "7"],
          "locrian 6": ["1", "b2", "b3", "4", "b5", "6", "b7"],
          "ionian augmented": ["1", "2", "3", "4", "#5", "6", "7"],
          "dorian #4": ["1", "2", "b3", "#4", "5", "6", "b7"],
          "phrygian major": ["1", "b2", "3", "4", "5", "b6", "b7"],
          "phrygian dominant": ["1", "b2", "3", "4", "5", "b6", "b7"],
          "spanish phrygian": ["1", "b2", "3", "4", "5", "b6", "b7"],
          "spanish major": ["1", "b2", "3", "4", "5", "b6", "b7"],
          "spanish": ["1", "b2", "3", "4", "5", "b6", "b7"],
          "lydian #2": ["1", "#2", "3", "#4", "5", "6", "7"],
          "lydian #9": ["1", "#2", "3", "#4", "5", "6", "7"],
          "altered dominant bb7": ["1", "b2", "b3", "b4", "b5", "b6", "6"],
          "altered 13": ["1", "b2", "b3", "b4", "b5", "b6", "6"],
          # Harmonic major modes
          "harmonic major": ["1", "2", "3", "4", "5", "b6", "7"],
          "ionian b6": ["1", "2", "3", "4", "5", "b6", "7"],
          "dorian b5": ["1", "2", "b3", "4", "b5", "6", "b7"],
          "phrygian b4": ["1", "b2", "b3", "b4", "5", "b6", "b7"],
          "lydian b3": ["1", "2", "b3", "#4", "5", "6", "7"],
          "melodic minor #4": ["1", "2", "b3", "#4", "5", "6", "7"],
          "mixolydian b2": ["1", "b2", "3", "4", "5", "6", "b7"],
          # Lydian augmented already defined in melodic minor modes
          "locrian bb7": ["1", "b2", "b3", "4", "b5", "b6", "6"],
          # Double harmonic major modes
          "double harmonic major": ["1", "b2", "3", "4", "5", "b6", "7"],
          "double harmonic": ["1", "b2", "3", "4", "5", "b6", "7"],
          "lydian #2#6": ["1", "#2", "3", "#4", "5", "#6", "7"],
          "ultraphrygian": ["1", "b2", "b3", "b4", "5", "b6", "6"],
          "hungarian minor": ["1", "2", "b3", "#4", "5", "b6", "7"],
          "oriental": ["1", "b2", "3", "4", "b5", "6", "b7"], # Mixolydian b2 with lowered 5
          "ionian aug#2": ["1", "#2", "3", "4", "#5", "6", "7"],
          "locrian bb3bb7": ["1", "b2", "2", "4", "b5", "b6", "6"],
          # Bebop scales
          "bebop dominant": ["1", "2", "3", "4", "5", "6", "#6", "7"],
          "bebop major": ["1", "2", "3", "4", "5", "b6", "6", "7"],
          "bebop blues": ["1", "2", "b3", "3", "4", "5", "6", "b7"],
          "bebop melodic minor": ["1", "2", "b3", "4", "5", "b6", "6", "7"],
          # Pentatonic scales
          "pentatonic major": ["1", "2", "3", "5", "6"],
          "major pentatonic": ["1", "2", "3", "5", "6"],
          "pentatonic minor": ["1", "b3", "4", "5", "b7"],
          "minor pentatonic": ["1", "b3", "4", "5", "b7"],
          "pentatonic blues": ["1", "b3", "4", "b5", "5", "b7"],
          "blues pentatonic": ["1", "b3", "4", "b5", "5", "b7"],
          "pentatonic neutral": ["1", "2", "4", "5", "b7"],
          "neutral pentatonic": ["1", "2", "4", "5", "b7"],
          "pentatonic rock": ["1", "b3", "4", "#5", "b7"],
          "rock pentatonic": ["1", "b3", "4", "#5", "b7"],
          "jue": ["1", "b3", "4", "#5", "b7"], # Same intervals as rock pentatonic
          "pentatonic scottish": ["1", "2", "4", "5", "6"],
          "scottish pentatonic": ["1", "2", "4", "5", "6"],
          # Blues scales
          "blues major": ["1", "2", "b3", "3", "5", "6"],
          "blues minor": ["1", "b3", "4", "b5", "5", "b7"],
          "blues": ["1", "b3", "4", "b5", "5", "b7"], # Shorthand for Blues Minor
          "blues diminished": ["1", "b2", "b3", "3", "b5", "5", "6", "b7"],
          "aux diminished blues": ["1", "b2", "b3", "3", "b5", "5", "6", "b7"], 
          "half-whole": ["1", "b2", "b3", "3", "b5", "5", "6", "b7"],
          "balinese": ["1", "b2", "b3", "3", "b5", "5", "6", "b7"], 
          # Gypsy scales
          "gypsy major": ["1", "b2", "3", "4", "5", "b6", "7"],
          "gypsy minor": ["1", "2", "b3", "b5", "5", "b6", "7"],
          "spanish gypsy": ["1", "b2", "3", "4", "5", "b6", "b7"], # Same as Phrygian dominant
          "hungarian gypsy": ["1", "2", "b3", "#4", "5", "b6", "b7"], # Hungarian minor with lowered 7
          # Diminished scales
          "diminished": ["1", "2", "b3", "4", "b5", "b6", "6", "7"],
          "tonic diminished": ["1", "2", "b3", "4", "b5", "b6", "6", "7"],
          "whole-half": ["1", "2", "b3", "4", "#4", "#5", "6", "7"],
          "dominant diminished": ["1", "b2", "b3", "3", "#4", "5", "6", "b7"],
          "whole": ["1", "2", "3", "#4", "#5", "#6"],
          "whole-tone": ["1", "2", "3", "#4", "#5", "#6"],
          "aux augmented": ["1", "2", "3", "#4", "#5", "#6"],
          # Maqam
          "bayati shuri": ["1", "2", "b3", "4", "b5", "6", "b7"],
          "hijaz": ["1", "b2", "3", "4", "5", "b6", "b7"],
          "hijaz kar": ["1", "b2", "3", "4", "5", "6", "7"],
          "huzam": ["1", "b2", "b3", "3", "5", "b6", "b7"],
          "nikriz": ["1", "2", "3", "#4", "5", "6", "7"],
          "tunisian": ["1", "2", "3", "#4", "5", "6", "7"],
          "saba": ["1", "2", "b3", "3", "5", "b6", "b7"],
          "sabah": ["1", "2", "b3", "3", "5", "b6", "b7"],
          "suznak": ["1", "2", "3", "4", "5", "#5", "7"],
          "neveseri": ["1", "b2", "b3", "3", "5", "b6", "b7", "7"],
          # Greek
          "pireaus": ["1", "b2", "3", "#4", "5", "#5", "7"],
          "tsinganikos": ["1", "b2", "3", "4", "#4", "6", "#6"],
          # Hindustani
          "marwa": ["1", "b2", "3", "#4", "5", "6", "7"],
          "poorvi": ["1", "b2", "3", "#4", "5", "6", "7"],
          "segah": ["1", "#2", "3", "4", "5", "#5", "7"],
          "todi": ["1", "b2", "b3", "#4", "5", "b6", "7"],
          # Carnatic
          "charukeshi": ["1", "2", "3", "4", "5", "b6", "b7"],
          "dharmaavati": ["1", "2", "b3", "b5", "5", "6", "7"],
          "lataangi": ["1", "2", "3", "#4", "5", "6", "7"],
          "vachaspati": ["1", "2", "3", "#4", "5", "6", "#6"],
          "natakpriya": ["1", "b2", "b3", "4", "5", "6", "b7"],
          "rampriya": ["1", "b2", "3", "#4", "5", "6", "#6"],
          "suryakant": ["1", "b2", "3", "4", "5", "6", "7"],
          # Japanese
          "joshi akikaze": ["1", "2", "b3", "5", "6"],
          "akikaze joshi": ["1", "2", "b3", "5", "6"],
          "joshi hira": ["1", "2", "b3", "5", "b6"],
          "hira joshi": ["1", "2", "b3", "5", "b6"],
          "joshi iwato": ["1", "b2", "4", "b5", "b7"],
          "iwato joshi": ["1", "b2", "4", "b5", "b7"],
          "joshi kokin": ["1", "2", "4", "5", "b6"],
          "kokin joshi": ["1", "2", "4", "5", "b6"],
          "joshi kumoi": ["1", "b2", "4", "5", "b6"],
          "kumoi joshi": ["1", "b2", "4", "5", "b6"],
          "joshi okinawa": ["1", "3", "4", "5", "7"],
          "okinawa joshi": ["1", "3", "4", "5", "7"],
          "sen in": ["1", "b2", "4", "5", "b7"],
          "in sen": ["1", "b2", "4", "5", "b7"],
          # Misc.
          "chromatic": ["1", "b2", "2", "b3", "3", "4", "#4", "5", "#5", "6", "b7", "7"],
          "augmented": ["1", "#2", "3", "5", "#5", "7"],
          "enigmatic ascending": ["1", "b2", "3", "#4", "#5", "#6", "7"],
          "enigmatic descending": ["1", "b2", "3", "4", "b6", "b7", "7"],
          "hungarian major": ["1", "b3", "3", "b5", "5", "6", "b7"],
          "neapolitan major": ["1", "b2", "b3", "4", "5", "6", "7"],
          "neapolitan minor": ["1", "b2", "b3", "4", "5", "b6", "7"],
          "prometheus": ["1", "2", "3", "b5", "6", "b7"],
          "mystic": ["1", "2", "3", "b5", "6", "b7"],
          "prometheus neapolitan": ["1", "b2", "3", "b5", "6", "b7"], # Prometheus with lowered 2
          "spanish 8 tone": ["1", "b2", "b3", "3", "4", "b5", "b6", "b7"],
          }

# Invert scales so alternative names can be identified by intervals or name
d = defaultdict(list)
[d[tuple(v)].append(k) for k,v in SCALES.items()]
INV_SCALES_BY_INTERVAL = dict(d)
INV_SCALES_BY_NAME = {k: d[tuple(v)] for k,v in SCALES.items()}

# %% ../nbs/02_scale.ipynb 7
class Scale:
    def __init__(self, name: str):
        self.name = name.lower()
        self.intervals = SCALES.get(self.name, [])
        self.flats = sum(1 for i in self.intervals if i.startswith("b"))
        self.sharps = sum(1 for i in self.intervals if i.startswith("#"))
        self.naturals = len(self.intervals) - self.flats - self.sharps

    @classmethod
    def available_scales(cls): return list(SCALES.keys())
    
    @classmethod
    def from_intervals(cls, intervals: list[str], name: str = None):
        """Create a custom scale from a list of intervals."""
        for i in intervals:
            assert i in list(INTERVAL_NAMES), f"Interval '{i}' not valid. Available intervals: '{list(INTERVAL_NAMES.keys())}'"
        # Infer name if not given
        custom_scale = cls(INV_SCALES_BY_INTERVAL.get(tuple(intervals), ["unknown"])[0] if not name else name)
        custom_scale.intervals = intervals
        return custom_scale
    
    @property
    def rel_semitones(self):
        return [INTERVAL_HALF_STEPS[interval] for interval in self.intervals]
    
    @property
    def abs_semitones(self):
        rel = self.rel_semitones
        abs = []
        for i, r in enumerate(rel[1:]):
            abs.append(r - rel[i])
        # Last remaining interval
        abs.append(12-sum(abs))
        return abs
    
    def __repr__(self): return f"Scale: {self.name.title()}. Intervals: {self.intervals}"
    def __eq__(self, other): return self.intervals == other.intervals
    def __ne__(self, other): return not self == other
    def __iter__(self) -> list[str]: return iter(self.intervals)
    def __len__(self): return len(self.intervals)

# %% ../nbs/02_scale.ipynb 31
@patch
def get_notes(self:Scale, root, oct=4):
    """Get the notes of a scale from a root note."""
    root = Note(root, oct=oct) if isinstance(root, str) else root
    return [root + int(INTERVAL_HALF_STEPS[i]) for i in self.intervals]

# %% ../nbs/02_scale.ipynb 36
@patch
def get_diatonic_chords(self:Scale, root, min_notes=3):
    assert min_notes > 1, "min_notes must be greater than 1."
    notes = self.get_notes(root)
    return [Chord(combo) for n in range(min_notes, len(notes)+1) for combo in combinations(notes, n)]

# %% ../nbs/02_scale.ipynb 40
@patch
def rel_interval_names(self:Scale, short=False):
        return self.intervals if short else [INTERVAL_NAMES[i] for i in self.intervals[1:]]

@patch
def abs_interval_names(self:Scale, short=False):
    return [getattr(Interval.from_semitones(i), "long" if not short else "short") for i in self.abs_semitones]

# %% ../nbs/02_scale.ipynb 44
@patch
def get_scale_names(self:Scale):
    return INV_SCALES_BY_INTERVAL.get(tuple(self.intervals), [])

# %% ../nbs/02_scale.ipynb 47
@patch
def _shift_abs_semitones(self:Scale, n):
    """ Shift the absolute semitones of a scale by n steps. """
    return self.abs_semitones[n:] + self.abs_semitones[:n]

@patch
def _shift_rel_semitones(self:Scale, n:int):
    """ Shift relative semitones by n steps. """
    return [0] + list(accumulate(self._shift_abs_semitones(n)[:-1]))

# %% ../nbs/02_scale.ipynb 52
def semi_to_intvals(semitones):
    """Convert relative semitone values to interval names."""
    # Major scale is the baseline for accidentals
    maj = [0, 2, 4, 5, 7, 9, 11, 12, 14, 16, 17, 19, 21, 23, 24, 26, 28, 29, 31, 33, 35]
    acc = {-1: 'b', 0: '', 1: '#'}
    intervals = []
    for i, s in enumerate(semitones):
        deg = i + 1
        diff = (s - maj[i]) % 12
        if diff > 6: diff -= 12
        if diff == 2: intervals.append(f'{deg+1}')
        elif diff == -2: intervals.append(f'{deg-1}')
        else: intervals.append(f'{acc.get(diff, "")}{deg}')
    return intervals

# %% ../nbs/02_scale.ipynb 57
@patch
def shift_intvals(self:Scale, n:int) -> list[str]:
    """Shift the intervals of a scale by n steps."""
    return semi_to_intvals(self._shift_rel_semitones(n))

@patch
def get_modes(self:Scale) -> list[Scale]:
    return [Scale.from_intervals(self.shift_intvals(i)) for i in range_of(self)]

# %% ../nbs/02_scale.ipynb 67
@patch
def get_audio_array(self:Scale, root, oct=4, length=0.3):
    notes = self.get_notes(root, oct=oct)
    octave = Note(root, oct=oct+1).get_audio_array(length=length)
    return np.concatenate([n.get_audio_array(length) for n in notes] + [octave])

@patch
def play(self:Scale, root, oct=4, length=0.3): 
    return Audio(self.get_audio_array(root, oct=oct, length=length), rate=44100)

# %% ../nbs/02_scale.ipynb 76
@patch
def get_triads(self:Scale, root):
    """Get all triads in scale starting from root note."""
    notes = self.get_notes(root)
    return [Chord([notes[i], 
                  Note(str(notes[(i+2)%7]), oct=notes[i].oct + (i+2)//7),
                  Note(str(notes[(i+4)%7]), oct=notes[i].oct + (i+4)//7)]) 
            for i in range(len(notes))]

# %% ../nbs/02_scale.ipynb 80
@patch
def play_triads(self:Scale, root):
    """Play all triads in scale starting from root note."""
    return Audio(np.concatenate([c.get_audio_array() for c in self.get_triads(root)]), rate=44100)

# %% ../nbs/02_scale.ipynb 83
@patch
def get_sevenths(self:Scale, root):
    """Get all seventh chords in scale starting from root note."""
    notes = self.get_notes(root)
    return [Chord([notes[i], 
                  Note(str(notes[(i+2)%7]), oct=notes[i].oct + (i+2)//7),
                  Note(str(notes[(i+4)%7]), oct=notes[i].oct + (i+4)//7),
                  Note(str(notes[(i+6)%7]), oct=notes[i].oct + (i+6)//7)]) 
            for i in range(len(notes))]

# %% ../nbs/02_scale.ipynb 86
@patch
def play_sevenths(self:Scale, root):
    """Play all seventh chords in scale starting from root note."""
    return Audio(np.concatenate([c.get_audio_array() for c in self.get_sevenths(root)]), rate=44100)

# %% ../nbs/02_scale.ipynb 90
@patch
def to_frame(self:Scale, root=None):
    d = {
        "Degree": self.intervals,
        "Relative Interval": ["unison"] + self.rel_interval_names(),
        "Mode": [m.name for m in self.get_modes()],
        "Relative Semitones": self.rel_semitones,
        "Absolute Semitones": self.abs_semitones,
    }
    if root:
        d.update({
            "Notes": self.get_notes(root),
            "Triad": [t.name() for t in self.get_triads(root)],
            "Seventh Chord": [s.name() for s in self.get_sevenths(root)],
        })
    return pd.DataFrame(d)
