"""Basic building blocks"""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.

# %% auto 0
__all__ = ['BASE_NOTES', 'CHROMATIC_NOTES', 'ENHARMONIC_NOTES', 'INTERVALS', 'INTERVAL_NAMES', 'INTERVAL_HALF_STEPS', 'SCALES',
           'Note', 'Chord', 'PolyChord', 'Scale']

# %% ../nbs/00_core.ipynb 3
import io
import numpy as np
from fastcore.all import *
import scipy.io.wavfile as wav
from IPython.display import Audio
from mingus.core import chords, notes, intervals

# %% ../nbs/00_core.ipynb 6
BASE_NOTES = ["C", "D", "E", "F", "G", "A", "B"]
CHROMATIC_NOTES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
ENHARMONIC_NOTES = CHROMATIC_NOTES + ["Db", "Eb", "Fb", "E#", "Gb", "Ab", "Bb", "Cb", "B#"]
INTERVALS = ["1", "b2", "2", "b3", "3", "4", "#4", "5", "b6", "6", "b7", "7"]
INTERVAL_NAMES = {
    "1": "unison",
    "b2": "minor second",
    "2": "major second",
    "b3": "minor third",
    "3": "major third",
    "4": "perfect fourth",
    "#4": "augmented fourth",
    "b5": "diminished fifth",
    "5": "perfect fifth",
    "b6": "minor sixth",
    "6": "major sixth",
    "b7": "minor seventh",
    "7": "major seventh",
}
INTERVAL_HALF_STEPS = {
    "1": 0,
    "b2": 1,
    "2": 2,
    "b3": 3,
    "3": 4,
    "4": 5,
    "#4": 6,
    "b5": 6,
    "5": 7,
    "b6": 8,
    "6": 9,
    "b7": 10,
    "7": 11,
}

SCALES = {"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", "5", "b6", "7"],
          "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"],
          "mixolydian #11": ["1", "2", "3", "#4", "5", "6", "b7"],
          "locrian n2": ["1", "2", "b3", "4", "5", "b6", "7"],
          "altered": ["1", "b2", "b3", "3", "b5", "b6", "b7"],
          "aeolian dominant": ["1", "2", "3", "4", "5", "b6", "b7"],
          "phrygian dominant": ["1", "b2", "3", "4", "5", "b6", "b7"],
          # TODO Harmonic minor/major and modes
          }

# %% ../nbs/00_core.ipynb 10
class Note(BasicRepr):
    def __init__(self, note: str, oct: int = 4):
        assert isinstance(oct, int) and oct > 0, f"Octave must be a positive integer, got oct={oct}."
        store_attr()
        # Transform note to uppercase
        if isinstance(note, str):
            note = note[0].upper() + note[1:]
            assert notes.is_valid_note(note), f"Note '{note}' is not valid"
            self.note = self.postprocess_note(notes.remove_redundant_accidentals(note))

    @staticmethod
    def postprocess_note(note: str):
        """ Get rid of unnecessary accidentals."""
        if note == "B#": note = "C"
        elif note == "E#": note = "F"
        elif note == "Cb": note = "B"
        elif note == "Fb": note = "E"
        elif note.endswith("##"):
            note = BASE_NOTES[BASE_NOTES.index(note[0])+1]
        elif note.endswith("bb"):
            note = BASE_NOTES[BASE_NOTES.index(note[0])-1]
        return str(note)
    
    def __str__(self): return self.note
    def __eq__(self, other): return str(self) == str(other)
    def __ne__(self, other): return not str(self) == str(other) 

# %% ../nbs/00_core.ipynb 16
@patch
def __add__(self:Note, other):
    """Add n semitones to a note."""
    octave_change = (other + CHROMATIC_NOTES.index(str(self))) // 12
    return Note(intervals.from_shorthand(str(self), INTERVALS[(other)%12]), oct=self.oct + octave_change)

# %% ../nbs/00_core.ipynb 22
@patch
def __sub__(self:Note, other):
    octave_change = (other + CHROMATIC_NOTES.index(str(self)) - 1) // 12
    return Note(intervals.from_shorthand(str(self), INTERVALS[(other)%12], False), oct=self.oct - octave_change)

# %% ../nbs/00_core.ipynb 27
@patch
def interval(self:Note, other:Note, short=False):
    return intervals.determine(str(self), str(other), short)

# %% ../nbs/00_core.ipynb 34
@patch
def minor(self:Note): return self - 3

# %% ../nbs/00_core.ipynb 37
@patch
def major(self:Note): return self + 3

# %% ../nbs/00_core.ipynb 44
@patch
def play(self:Note, length=1): return Audio(data=self.get_audio_bytes(length))

# %% ../nbs/00_core.ipynb 53
class Chord(BasicRepr):
    def __init__(self, notes: List[Note]):
        self.notes = [Note(n) if isinstance(n, str) else n for n in notes]
        self.s_notes = [str(n) for n in self.notes]
        self.short_s_notes = [f"{n.note}{n.oct}" for n in self.notes]
        self.first = self.notes[0]
        self.s_first = str(self.first)

    def names(self) -> list[str]: return chords.determine(self.s_notes)
    def name(self): return self.names()[0] if self.names() else "No chord found."

    @classmethod
    def from_short(cls, c: str): return cls(chords.from_shorthand(c)) 

    def __list__(self) -> list[str]: return self.notes
    def __repr__(self): return f"Chord: '{self.name()}'. Notes: {self.short_s_notes}"
    def __add__(self, other): return Chord([n + other for n in self.notes])
    def __sub__(self, other): return Chord([n - other for n in self.notes])


class PolyChord(Chord):
    def __init__(self, chords: list[Chord]):
        self.chords = chords
        super().__init__([note for chord in chords for note in chord.notes])
    def __repr__(self): return f"PolyChord: '{'|'.join([c.name() for c in self.chords])}'. Notes: {self.short_s_notes}"

# %% ../nbs/00_core.ipynb 60
@patch
def __mul__(self:Note, other: Note):
    return Chord([self, other])

@patch
def __mul__(self:Chord, other: Note):
    return Chord(self.notes + [other])

# %% ../nbs/00_core.ipynb 63
@patch
def __mod__(self:Chord, other: Chord):
    return PolyChord([self, other])

# %% ../nbs/00_core.ipynb 66
@patch
def invert(self:Chord, n: int = 1):
    assert n > 0 and n < len(self.s_notes), f"Invalid inversion '{n}' for chord with '{len(self.s_notes)}' notes."
    return Chord(self.notes[n:] + [Note(str(note), oct=note.oct + 1) for note in self.notes[:n]])

@patch
def invert(self:PolyChord, n: int = 1):
    return PolyChord([c.invert(n) for c in self.chords])

# %% ../nbs/00_core.ipynb 70
@patch
def get_audio_array(self:Chord, length=1):
    return np.sum([n.get_audio_array(length) for n in self.notes], axis=0)

@patch
def play(self:Chord, length=1): 
    return Audio(self.get_audio_array(length), rate=44100)

# %% ../nbs/00_core.ipynb 79
class Scale:
    def __init__(self, name: str):
        self.name = name.lower()
        self.intervals = SCALES.get(self.name, "scale not found.")

    def __repr__(self): return f"{self.name.title()}. {self.intervals}"

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

    @classmethod
    def available_scales(cls): return list(SCALES.keys())
    
    @classmethod
    def from_intervals(cls, name: str, intervals: list[str]):
        """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())}'"
        custom_scale = cls(name)
        custom_scale.intervals = intervals
        return custom_scale

# %% ../nbs/00_core.ipynb 92
@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/00_core.ipynb 97
@patch
def get_interval_names(self:Scale, short=False):
    return self.intervals if short else [INTERVAL_NAMES[i] for i in self.intervals]

# %% ../nbs/00_core.ipynb 103
@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/00_core.ipynb 109
@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/00_core.ipynb 114
@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))]
