# GNU Solfege - ear training for GNOME
# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005, 2006  Tom Cato Amundsen
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 51 Franklin ST, Fifth Floor, Boston, MA  02110-1301  USA

"""
The Interval class is a class that is used to do math with
intervals and musical pitches. You use this class if you
need to know the difference between a augmented second and
a minor third. If you don't require this you can probably
ise the simpler function mpdutils.int_to_intervalname
"""

import unittest
import re
import _exceptions

# We have to list all possible interval names here so that xgettext
# can find them and add them to the .pot file for translation. We
# cannot translate the interval quality and interval number separately
# because we don't know how to concat them on other languages.
if 0:
    _("perfect unison")
    _("diminished unison")
    _("augmented unison")
    #
    _("diminished second")
    _("minor second")
    _("major second")
    _("augmented second")
    #
    _("diminished third")
    _("minor third")
    _("major third")
    _("augmented third")
    #
    _("diminished fourth")
    _("perfect fourth")
    _("augmented fourth")
    #
    _("diminished fifth")
    _("perfect fifth")
    _("augmented fifth")
    #
    _("diminished sixth")
    _("minor sixth")
    _("major sixth")
    _("augmented sixth")
    #
    _("diminished seventh")
    _("minor seventh")
    _("major seventh")
    _("augmented seventh")
    #
    _("diminished octave")
    _("perfect octave")
    _("augmented octave")
    # translators: Only translate the word after interval| , and don't include
    # interval| in the translated string. So for Norwegians, translate
    # "interval|diminished" to "forminsket". Do similar for all strings
    # that are preceded with "interval|"
    _("interval|diminished")
    _("interval|pure")
    _("interval|augmented")
    _("interval|minor")
    _("interval|major")
    _("interval|doubly-diminished")
    _("interval|doubly-augmented")


class InvalidIntervalnameException(_exceptions.MpdException):
    def __init__(self, n):
        self.m_intervalname = n
    def __str__(self):
        return _("Invalid interval name: %s") % self.m_intervalname


class Interval:
    """
    The interval is internally a interval less than octave
    pluss n octaves. The data variables:
      m_dir
      m_octave
      m_interval
      m_mod
    should NOT be touched by anyone, except MusicalPitch.__add__
    """
    __nn_to_interval_quality = {
        'p': 'perfect',
        'dd': 'doubly-diminished',
        'd': 'diminished',
        'm': 'minor',
        'M': 'major',
        'a': 'augmented',
        'aa': 'doubly-augmented',
        }
    def __init__(self, iname=None):
        self.m_dir = 1 # value as to be 1 or -1 for initialised obj
        self.m_octave = 0 # 0, 1, 2, 3 etc
        self.m_interval = 0 # 0:unison, 1:seond, ... 6: septim
        # unison:              dim   perfect   aug
        # second:  -2:dim -1:minor 0:major   1:aug
        # third:      dim    minor   major     aug
        # fourth:              dim   perfect   aug
        # fifth:               dim   perfect   aug
        # sixth:      dim    minor   major     aug
        # seventh:    dim    minor   major     aug
        self.m_mod = 0
        if iname:
            self.set_from_string(iname)
    def nn_to_translated_quality(nn):
        """
        Return a translated string of the short 1 or 2 char string
        in nn. Possible values for nn are: dd, d, m, M, a, aa, p
        """
        return _i("interval|%s" % Interval.__nn_to_interval_quality[nn])
    nn_to_translated_quality = staticmethod(nn_to_translated_quality)
    def number_name(steps):
        try:
            return {
                1: "unison",
                2: "second",
                3: "third",
                4: "fourth",
                5: "fifth",
                6: "sixth",
                7: "seventh",
                8: "octave",
                9: "ninth",
                10: "decim"}[steps]
        except KeyError:
            return "%ith" % steps
    number_name = staticmethod(number_name)
    def errorcheck(self):
        assert 0 <= self.m_interval <= 6
        assert -2 <= self.m_mod <= 1 # should be increased to -3 <= x <= 2
        assert self.m_octave >= 0
        assert self.m_dir in (-1, 1)
    def _set(self, direction, interval, mod, octave):
        self.m_dir = direction
        self.m_interval = interval
        self.m_mod = mod
        self.m_octave = octave
        if __debug__:
            self.errorcheck()
        return self
    def new_from_int(i):
        assert isinstance(i, int)
        new_int = Interval()
        new_int.set_from_int(i)
        return new_int
    new_from_int = staticmethod(new_from_int)
    def set_from_int(self, i):
        """It returns self to allow chaining: set_from_int(4).pretty_name()
        """
        if i < 0:
            self.m_dir = -1
        else:
            self.m_dir = 1
        self.m_octave = abs(i) / 12
        self.m_mod, self.m_interval = (
               (0, 0),          # unison
               (-1, 1), (0, 1), # second
               (-1, 2), (0, 2), # third
               (0, 3),          # fourth
               (-1, 4),         # dim 5, tritonus
               (0, 4),          # fifth
               (-1, 5), (0, 5), # sixth
               (-1, 6), (0, 6))[abs(i) % 12] # seventh
        return self
    def set_from_string(self, s):
        """
        unison  p1
        second  m2 M2
        third   m3 M3
        fourth  p4
        fifth   d5 5
        sixth   m6 M6
        seventh m7 M7
        octave  p8
        none    m9 M9
        decim   m10 M10
        """
        # up or down
        s_orig = s[:]
        s = s.strip()
        if s[0] == "-":
            self.m_dir = -1
            s = s[1:]
        else:
            self.m_dir = 1
        m = re.match("(m|M|d|a|p)(\d+)", s)
        if not m:
            raise InvalidIntervalnameException(s_orig)
        n, i = m.groups()
        i = int(i)
        if i <= 7:
            self.m_octave = 0
        else:
            self.m_octave = (i - 1) // 7
        self.m_interval = i - 1 - self.m_octave * 7
        if self.m_interval in (1, 2, 5, 6):
            try:
                self.m_mod = {'d': -2, 'm': -1, 'M': 0, 'a': 1}[n]
            except:
                raise InvalidIntervalnameException(s_orig)
        elif self.m_interval in (0, 3, 4):
            try:
                self.m_mod = {'d': -1, 'p': 0, '': 0, 'a': 1}[n]
            except:
                raise InvalidIntervalnameException(s_orig)
    def get_intvalue(self):
        if __debug__:
            self.errorcheck()
        return ([0, 2, 4, 5, 7, 9, 11][self.m_interval] + self.m_octave * 12 + self.m_mod) * self.m_dir
    def __str__(self):
        if __debug__:
            self.errorcheck()
        s = "(Interval %i %imod %io" % (self.m_interval, self.m_mod, self.m_octave)
        if self.m_dir == -1:
            s = s + " down)"
        else:
            s = s + " up)"
        return s
    def __repr__(self):
        if self.m_interval in (0, 3, 4):
            mod = {-2: 'dd', -1: 'd', 0: 'p', 1: 'a', 2: 'aa'}
            return "%s%s" % ({-2: 'dd', -1: 'd', 0: 'p', 1: 'a', 2: 'aa'}[self.m_mod], self.m_interval + 1 + self.m_octave * 7)
        return "%s%s" % ({-2: 'd', -1: 'm', 0: 'M', 1: 'a'}[self.m_mod],  (self.m_interval + 1 + self.m_octave * 7))
    def __eq__(self, b):
        return self.m_dir == b.m_dir and self.m_octave == b.m_octave \
            and self.m_mod == b.m_mod and self.m_interval == b.m_interval
    def get_number_name(self):
        """
        Return the translated general name of the interval, like second, third
        etc. (major, minor etc.)
        """
        return _(self.number_name(self.steps()))
    def get_quality_short(self):
        """
        Return a short string telling the quality.
        This is a non-translated short string mostly used
        internally in the program.
        """
        if self.m_interval in (0, 3, 4):
            return {-2: "dd",
                    -1: "d",
                     0: "p",
                     1: "a",
                     2: "aa"}[self.m_mod]
        else:
            assert self.m_interval in (1, 2, 5, 6)
            return {-2: "d",
                    -1: "m",
                     0: "M",
                     1: "a"}[self.m_mod]
    def get_quality(self):
        """
        Return the non-translated interval quality.
        """
        return self.__nn_to_interval_quality[self.get_quality_short()]
    def get_cname(self):
        """
        Return the full untranslated interval name, both the number and quality.
        """
        return "%s %s" % (self.get_quality(), self.number_name(self.steps()))
    def get_name(self):
        """
        Return the full translated intervalname, both the number and quality.
        """
        return _(self.get_cname())
    def steps(self):
        return self.m_octave * 7 + self.m_interval + 1


class TestInterval(unittest.TestCase):
    def test_constructor(self):
        for s, a, b, c, i, steps in (
                ('p1', 0, 0, 0, 0, 1),
                ('d1', 0, 0, -1, -1, 1),
                ('a1', 0, 0, 1, 1, 1),
                ('p4', 3, 0, 0, 5, 4),
                ('p5', 4, 0, 0, 7, 5),
                ('p8', 0, 1, 0, 12, 8),
                ('d8', 0, 1, -1, 11, 8),
                ('a8', 0, 1, 1, 13, 8),
                ('p11', 3, 1, 0, 17, 11), # octave + fourth
                ('p12', 4, 1, 0, 19, 12), # octave + fifth
                ('p15', 0, 2, 0, 24, 15), # 2*octave
                ('d15', 0, 2, -1, 23, 15), # 2*octave
                ('a15', 0, 2, 1, 25, 15), # 2*octave
                ('m2', 1, 0, -1, 1, 2),
                ('M2', 1, 0, 0, 2, 2),
                ('M3', 2, 0, 0, 4, 3),
                ('M6', 5, 0, 0, 9, 6),
                ('M9', 1, 1, 0, 14, 9),
                ('m10', 2, 1, -1, 15, 10), # decim
                ('M13', 5, 1, 0, 21, 13), # octave + sixth
                ('m14', 6, 1, -1, 22, 14), # octave + seventh
                ('M16', 1, 2, 0, 26, 16), # 2*octave + second
                ):
            n = Interval(s)
            self.assertEquals((n.m_interval, n.m_octave, n.m_mod), (a, b, c))
            self.assertEquals(repr(n), s)
            self.assertEquals(i, n.get_intvalue())
            self.assertEquals(steps, n.steps())
    def test_addition(self):
        from musicalpitch import MusicalPitch
        a = MusicalPitch.new_from_notename("c'")
        b = Interval("m2")
        self.assertEquals((a+b).get_octave_notename(), "des'")

if __name__ == "__main__":
    import testsetup
    unittest.main()
