# GNU Solfege - ear training for GNOME
# Copyright (C) 2000, 2001, 2002, 2003, 2004  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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  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

>>> from musicalpitch import MusicalPitch
>>> a=MusicalPitch.new_from_notename("c'")
>>> b=Interval("m2")
>>> print (a+b).get_octave_notename()
des'
>>> i=Interval("u")
>>> print i.m_dir, i.m_interval, i.m_mod
1 0 0
>>> n = MusicalPitch.new_from_notename("c'")
>>> print n.get_octave_notename()
c'
>>> print (n+i).get_octave_notename()
c'
>>> i=Interval("M2")
>>> print i.get_intvalue()
2
>>> print i.m_dir, i.m_interval, i.m_mod
1 1 0
>>> print (n+i).get_octave_notename()
d'
>>> i.set_from_string("u")
>>> i.get_intvalue()
0
>>> i.set_from_string("au")
>>> i.get_intvalue()
1
>>> i.set_from_string("m2")
>>> i.get_intvalue()
1
>>> i.set_from_string("M2")
>>> i.get_intvalue()
2
>>> i.set_from_string("d2")
>>> i.get_intvalue()
0
>>> i.set_from_string("a2")
>>> i.get_intvalue()
3
>>> i.set_from_string("d4")
>>> i.get_intvalue()
4
>>> i.set_from_string("4")
>>> i.get_intvalue()
5
>>> i.set_from_string("a4")
>>> i.get_intvalue(), i.m_mod, i.m_interval, i.m_octave
(6, 1, 3, 0)
>>> i.set_from_string("d5")
>>> i.get_intvalue()
6
>>> i.set_from_string("5")
>>> i.get_intvalue()
7
>>> i.set_from_string("a5")
>>> i.get_intvalue()
8
>>> i.set_from_string("d6")
>>> i.get_intvalue()
7
>>> i.set_from_string("m6")
>>> i.get_intvalue()
8
>>> i.set_from_string("M6")
>>> i.get_intvalue()
9
>>> i.set_from_string("a6")
>>> i.get_intvalue()
10
>>> i.set_from_string("d7")
>>> i.get_intvalue()
9
>>> i.set_from_string("m7")
>>> i.get_intvalue()
10
>>> i.set_from_string("M7")
>>> i.get_intvalue()
11
>>> i.set_from_string("a7")
>>> i.get_intvalue()
12
"""
if __name__ == "__main__":
    import testsetup

import re

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__
    """
    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 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 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  1
        second  m2 M2
        third   m3 M3
        fourth  4
        fifth   d5 5
        sixth   m6 M6
        seventh m7 M7
        octave  8
        none    m9 M9
        decim   m10 M10
        """
        # up or down
        s = s.strip()
        if s[0] == "-":
            self.m_dir = -1
            s = s[1:]
        else:
            self.m_dir = 1
        n, i = re.match("(m|M|d|a*)(\w*)", s).groups()
        if i == 'u':# u for unisone
            i = 1
        if int(i) <= 7:
            self.m_interval = int(i) - 1
            self.m_octave = 0
        else:
            self.m_interval = int(i) - 8
            self.m_octave = 1
        if self.m_interval in (1, 2, 5, 6):
            self.m_mod = {'d': -2, 'm': -1, 'M': 0, 'a': 1}[n]
        elif self.m_interval in (0, 3, 4):
            self.m_mod = {'d': -1, 'p': 0, '': 0, 'a': 1}[n]
    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 get_short_name(self):
        """
        DON'T USE THIS FUNCTION ANYWHERE!!!!!!!!
        The format for the names will change without notice, so the
        return value of this function is *ONLY* human readable.
        DON'T LET ANY CODE DEPEND ON THE FORMAT OF THE RETURN VALUE
        """
        n = ""
        if self.m_octave == 1 and self.m_interval in (1, 2):
            n = n + {-2:'dim', -1:'min', 0:'Maj', 1:'aug'}[self.m_mod]
            n = n + {1: 'none', 2:'decim'}[self.m_interval]
            if self.m_dir == -1:
                n = n + " down"
            return n
        elif self.m_octave == 1 and self.m_interval == 0:
            n = n + {-1:'dim', 0:'', 1:'aug'}[self.m_mod]
            n = n + " octave"
            if self.m_dir == -1:
                n = n + " down"
            return n
        elif self.m_interval in (1, 2, 5, 6):
            n = n + {-2:'dim', -1:'min', 0:'Maj', 1:'aug'}[self.m_mod]
            n = n + " " + {1:'second', 2:'third', 5:'sixth', 6:'seventh'}[self.m_interval]
        elif self.m_interval in (0, 3, 4, 7):
            n = n + {-1:'dim', 0:'', 1:'aug'}[self.m_mod]
            n = n + " " +{0:'unison', 3:'fourth', 4:'fifth', 7:'octave'}[self.m_interval]
        if self.m_octave > 0:
            n = n + "+%i octave" % self.m_octave
        if self.m_dir == -1:
            n = n + " down"
        return n

def _test():
    import doctest
    import interval
    return doctest.testmod(interval)

if __name__ == "__main__":
    _test()
