####################################################################################################
#
# Musica - A Music Theory Package for Python
# Copyright (C) 2017 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################
"""This module defines temperaments.
"""
####################################################################################################
__all__ = [
    'ET12',
    'EqualTemperament',
    'TemperamentAccidentalStep',
    'TemperamentNaturalStep',
    'TemperamentStep',
    'UsualEqualTemperament',
    ]
####################################################################################################
from ..Locale.Note import translate_et12_note
from ..Math.MusicTheory import ET12Tuning
from .NoteTools import flatten_note, sharpen_note
from .PitchStandard import A440
from .Quality import IntervalQualities
####################################################################################################
[docs]class EqualTemperament:
    """Class to define an Equal Temperament.
    Step is an alias for semitone.
    """
    ##############################################
[docs]    def __init__(self, number_of_steps, pitch_standard):
        self._number_of_steps = number_of_steps
        self._pitch_standard = pitch_standard
        self._fundamental = self._compute_fundamental() 
    ##############################################
    @property
    def number_of_steps(self):
        return self._number_of_steps
    @property
    def pitch_standard(self):
        return self._pitch_standard
    @property
    def fundamental(self):
        """Return the frequency of C0"""
        return self._fundamental
    ##############################################
[docs]    def is_valid_step_number(self, number):
        return 0 <= number < self._number_of_steps 
    ##############################################
[docs]    def is_last_step_number(self, number):
        return (self._number_of_steps -1) == number 
    ##############################################
[docs]    def fold_step_number(self, number, octave=False):
        step_number = number % self._number_of_steps
        if octave:
            return step_number, number // self._number_of_steps
        else:
            return step_number 
    ##############################################
[docs]    def _compute_scale(self, octave, step_number):
        if step_number < 0:
            raise ValueError("Invalid step number {}".format(step_number))
        octave_factor = 2 ** octave
        interval_factor = 2 ** (step_number / self._number_of_steps)
        return octave_factor * interval_factor 
    ##############################################
[docs]    def _compute_fundamental(self):
        denominator = self._compute_scale(self._pitch_standard.octave,
                                          self._pitch_standard.step_number)
        return self._pitch_standard.frequency / denominator 
    ##############################################
[docs]    def frequency(self, octave, step_number):
        """Return the frequency for an octave using the scientific pitch notation and an step number
        ranging from 0 to 11.
        For A440 (La3), use octave 4 and step number 9.
        """
        return self._fundamental * self._compute_scale(octave, step_number)  
####################################################################################################
[docs]class TemperamentStep:
    ##############################################
[docs]    def __init__(self, temperament, step_number, quality=None):
        self._temperament = temperament
        self._step_number = step_number
        self._quality = quality
        self._prev_natural = None
        self._next_natural = None
        self._prev_step = None
        self._next_step = None 
    ##############################################
    @property
    def step_number(self):
        return self._step_number
    @property
    def quality(self):
        return self._quality
    @property
    def prev_natural(self): # Fixme: previous ?
        return self._prev_natural
    @property
    def next_natural(self):
        return self._next_natural
    @property
    def prev_step(self):
        return self._prev_step
    @property
    def next_step(self):
        return self._next_step
    ##############################################
[docs]    def __int__(self):
        return self._step_number 
[docs]    def __lt__(self, other):
        return self._step_number < int(other) 
    ##############################################
[docs]    def translate(self, language):
        return self._temperament.translator(self._step_number)[language]  
####################################################################################################
[docs]class TemperamentNaturalStep(TemperamentStep):
    ##############################################
[docs]    def __init__(self, temperament, step_number, degree, name, quality):
        super().__init__(temperament, step_number, quality)
        self._name = name
        self._degree = degree 
    ##############################################
    @property
    def is_natural(self):
        return True
    @property
    def is_accidental(self):
        return False
    ##############################################
    @property
    def name(self):
        return self._name
    @property
    def degree(self):
        return self._degree
    ##############################################
[docs]    def __repr__(self):
        return '{0.__class__.__name__} {0._step_number} {0._name}'.format(self)  
####################################################################################################
[docs]class TemperamentAccidentalStep(TemperamentStep):
    ##############################################
[docs]    def __init__(self, temperament, step_number):
        super().__init__(temperament, step_number, quality=IntervalQualities.minor) 
    ##############################################
[docs]    def __repr__(self):
        return '{0.__class__.__name__} {0.step_number}'.format(self) 
    ##############################################
    @property
    def is_natural(self):
        return False
    @property
    def is_accidental(self):
        return True
    ##############################################
    @property
    def sharpen_name(self):
        return sharpen_note(self._prev_natural.name)
    @property
    def flatten_name(self):
        return flatten_note(self._next_natural.name) 
####################################################################################################
[docs]class UsualEqualTemperament(EqualTemperament):
    """Base class factory to build for example a twelve-tone equal temperament.
    """
    ##############################################
[docs]    def __init__(self, math_implementation, pitch_standard, natural_steps, translator):
        super().__init__(math_implementation.number_of_steps, pitch_standard)
        self._math_implementation = math_implementation
        self._translator = translator
        perfect_steps = math_implementation.perfect_steps
        major_scale = math_implementation.major_scale
        self._natural_steps = []
        for degree, ztuple in enumerate(zip(major_scale, natural_steps)):
            pitch, name = ztuple
            if pitch in perfect_steps:
                quality = IntervalQualities.perfect
            else:
                quality = IntervalQualities.major
            step = TemperamentNaturalStep(self, pitch.step_number, degree, name, quality)
            self._natural_steps.append(step)
        # complete
        self._number_of_natural_steps = len(self._natural_steps)
        for step in self._natural_steps:
            i_prev = (step.degree - 1) % self._number_of_natural_steps
            i_next = (step.degree + 1) % self._number_of_natural_steps
            step._prev_natural = self._natural_steps[i_prev]
            step._next_natural = self._natural_steps[i_next]
        # Note names
        self._natural_step_names = natural_steps
        # List of natural step number
        self._natural_step_numbers = major_scale
        # Map note name -> step
        self._name_to_step = {step.name:step for step in self._natural_steps}
        # Map step number -> step
        self._steps = [None]*self._number_of_steps
        for step in self._natural_steps:
            self._steps[step.step_number] = step
        # Add accidentals and link steps
        for i in range(self._number_of_steps):
            step = self._steps[i]
            if step is None:
                step = TemperamentAccidentalStep(self, i)
                self._steps[i] = step
                # prev and next are natural steps
                i_prev = self.fold_step_number(i - 1)
                i_next = self.fold_step_number(i + 1)
                step._prev_natural = self._steps[i_prev]
                step._next_natural = self._steps[i_next]
                for name in (step.flatten_name, step.sharpen_name):
                    self._name_to_step[name] = step
        # Link steps
        for i in range(self._number_of_steps):
            step = self._steps[i]
            i_prev = self.fold_step_number(i - 1)
            i_next = self.fold_step_number(i + 1)
            step._prev_step = self._steps[i_prev]
            step._next_step = self._steps[i_next] 
    ##############################################
[docs]    def __iter__(self):
        return iter(self._steps) 
    ##############################################
[docs]    def iter_on_naturals(self):
        for step in self._steps:
            if step.is_natural:
                yield step 
    ##############################################
    @property
    def natural_step_names(self):
        return self._natural_step_names
    @property
    def number_of_natural_steps(self):
        return self._number_of_natural_steps
    ##############################################
[docs]    def fold_natural_step_number(self, number, octave=False):
        step_number = number % self._number_of_natural_steps
        if octave:
            return step_number, number // self._number_of_natural_steps
        else:
            return step_number 
    ##############################################
[docs]    def is_natural_step_number(self, number):
        return number in self._natural_step_numbers 
    ##############################################
[docs]    def translator(self, *args, **kwargs):
        return self._translator(*args, **kwargs) 
    ##############################################
[docs]    def is_valid_step_name(self, name):
        return name in self._natural_step_names 
    ##############################################
[docs]    def __getitem__(self, step_number):
        return self.by_step_number(step_number) 
    ##############################################
[docs]    def by_step_number(self, step_number):
        return self._steps[step_number] 
    ##############################################
[docs]    def by_name(self, name):
        return self._name_to_step[name] 
    ##############################################
[docs]    def by_degree(self, degree):
        return self._natural_steps[degree] 
    ##############################################
[docs]    def name_to_number(self, name):
        return self.by_name(name).step_number  
####################################################################################################
#: Twelve-tone equal temperament, also known as 12 equal temperament, 12-TET, or 12-ET
ET12 = UsualEqualTemperament(
    math_implementation=ET12Tuning,
    pitch_standard=A440,
    natural_steps=( # sorted by step number
        'C', # 1 Do
        'D', # 2 Rè
        'E', # 3 Mi
        'F', # 4 Fa
        'G', # 5 Sol
        'A', # 6 La
        'B', # 7 Si
    ),
    translator=translate_et12_note,
)