# This file is part of python-ly, https://pypi.python.org/pypi/python-ly
#
# Copyright (c) 2008 - 2015 by Wilbert Berendsen
#
# 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
# See http://www.gnu.org/licenses/ for more information.

"""
Pitch manipulation.
"""

from __future__ import unicode_literals

import re
from fractions import Fraction

import ly.lex.lilypond


pitchInfo = {
    'nederlands': (
        ('c', 'd', 'e', 'f', 'g', 'a', 'b'),
        ('eses', 'eseh', 'es', 'eh', '', 'ih', 'is', 'isih', 'isis'),
        (('ees', 'es'), ('aes', 'as'))
    ),
    'english': (
        ('c', 'd', 'e', 'f', 'g', 'a', 'b'),
        ('ff', 'tqf', 'f', 'qf', '', 'qs', 's', 'tqs', 'ss'),
    ),
    'deutsch': (
        ('c', 'd', 'e', 'f', 'g', 'a', 'h'),
        ('eses', 'eseh', 'es', 'eh', '', 'ih', 'is', 'isih', 'isis'),
        (('ases', 'asas'), ('ees', 'es'), ('aes', 'as'), ('heses', 'heses'), ('hes', 'b'))
    ),
    'svenska': (
        ('c', 'd', 'e', 'f', 'g', 'a', 'h'),
        ('essess', '', 'ess', '', '', '', 'iss', '', 'ississ'),
        (('ees', 'es'), ('aes', 'as'), ('hessess', 'hessess'), ('hess', 'b'))
    ),
    'italiano': (
        ('do', 're', 'mi', 'fa', 'sol', 'la', 'si'),
        ('bb', 'bsb', 'b', 'sb', '', 'sd', 'd', 'dsd', 'dd')
    ),
    'espanol': (
        ('do', 're', 'mi', 'fa', 'sol', 'la', 'si'),
        ('bb', '', 'b', '', '', '', 's', '', 'ss')
    ),
    'portugues': (
        ('do', 're', 'mi', 'fa', 'sol', 'la', 'si'),
        ('bb', 'btqt', 'b', 'bqt', '', 'sqt', 's', 'stqt', 'ss')
    ),
    'vlaams': (
        ('do', 're', 'mi', 'fa', 'sol', 'la', 'si'),
        ('bb', '', 'b', '', '', '', 'k', '', 'kk')
    ),
}
pitchInfo['norsk'] = pitchInfo['deutsch']
pitchInfo['suomi'] = pitchInfo['deutsch']
pitchInfo['catalan'] = pitchInfo['italiano']


class PitchNameNotAvailable(Exception):
    """Exception raised when there is no name for a pitch.
    
    Can occur when translating pitch names, if the target language e.g.
    does not have quarter-tone names.
    
    """
    def __init__(self, language):
        super(PitchNameNotAvailable, self).__init__()
        self.language = language


class Pitch(object):
    """A pitch with note, alter and octave attributes.
    
    Attributes may be manipulated directly.
    
    """
    def __init__(self, note=0, alter=0, octave=0, accidental="", octavecheck=None):
        self.note = note                # base note (c, d, e, f, g, a, b)
                                        # as integer (0 to 6)
        self.alter = alter              # # = .5; b = -.5; natural = 0
        self.octave = octave            # '' = 2; ,, = -2
        self.accidental = accidental    # "", "?" or "!"
        self.octavecheck = octavecheck  # a number is an octave check
    
    def __repr__(self):
        return '<Pitch {0}>'.format(self.output())
    
    def output(self, language="nederlands"):
        """Returns our string representation."""
        res = []
        res.append(pitchWriter(language)(self.note, self.alter))
        res.append(octaveToString(self.octave))
        res.append(self.accidental)
        if self.octavecheck is not None:
            res.append('=')
            res.append(octaveToString(self.octavecheck))
        return ''.join(res)
        
    @classmethod
    def c1(cls):
        """Returns a pitch c'."""
        return cls(octave=1)

    @classmethod
    def c0(cls):
        """Returns a pitch c."""
        return cls()

    @classmethod
    def f0(cls):
        """Return a pitch f."""
        return cls(3)

    def copy(self):
        """Returns a new instance with our attributes."""
        return self.__class__(self.note, self.alter, self.octave)
        
    def makeAbsolute(self, lastPitch):
        """Makes ourselves absolute, i.e. sets our octave from lastPitch."""
        self.octave += lastPitch.octave - (self.note - lastPitch.note + 3) // 7
        
    def makeRelative(self, lastPitch):
        """Makes ourselves relative, i.e. changes our octave from lastPitch."""
        self.octave -= lastPitch.octave - (self.note - lastPitch.note + 3) // 7


class PitchWriter(object):
    language = "unknown"
    def __init__(self, names, accs, replacements=()):
        self.names = names
        self.accs = accs
        self.replacements = replacements

    def __call__(self, note, alter = 0):
        """
        Returns a string representing the pitch in our language.
        Raises PitchNameNotAvailable if the requested pitch
        has an alteration that is not available in the current language.
        """
        pitch = self.names[note]
        if alter:
            acc = self.accs[int(alter * 4 + 4)]
            if not acc:
                raise PitchNameNotAvailable(self.language)
            pitch += acc
        for s, r in self.replacements:
            if pitch.startswith(s):
                pitch = r + pitch[len(s):]
                break
        return pitch


class PitchReader(object):
    def __init__(self, names, accs, replacements=()):
        self.names = list(names)
        self.accs = list(accs)
        self.replacements = replacements
        self.rx = re.compile("({0})({1})?$".format("|".join(names),
            "|".join(acc for acc in accs if acc)))

    def __call__(self, text):
        for s, r in self.replacements:
            if text.startswith(r):
                text = s + text[len(r):]
        for dummy in 1, 2:
            m = self.rx.match(text)
            if m:
                note = self.names.index(m.group(1))
                if m.group(2):
                    alter = Fraction(self.accs.index(m.group(2)) - 4, 4)
                else:
                    alter = 0
                return note, alter
            # HACK: were we using (rarely used) long english syntax?
            text = text.replace('flat', 'f').replace('sharp', 's')
        return False
            
            
def octaveToString(octave):
    """Converts numeric octave to a string with apostrophes or commas.
    
    0 -> "" ; 1 -> "'" ; -1 -> "," ; etc.
    
    """
    return octave < 0 and ',' * -octave or "'" * octave


def octaveToNum(octave):
    """Converts string octave to an integer:
    
    "" -> 0 ; "," -> -1 ; "'''" -> 3 ; etc.
    
    """
    return octave.count("'") - octave.count(",")


_pitchReaders = {}
_pitchWriters = {}


def pitchReader(language):
    """Returns a PitchReader for the specified language."""
    try:
        return _pitchReaders[language]
    except KeyError:
        res = _pitchReaders[language] = PitchReader(*pitchInfo[language])
        return res


def pitchWriter(language):
    """Returns a PitchWriter for the specified language."""
    try:
        return _pitchWriters[language]
    except KeyError:
        res = _pitchWriters[language] = PitchWriter(*pitchInfo[language])
        res.language = language
        return res


class PitchIterator(object):
    """Iterate over notes or pitches in a source."""
    
    def __init__(self, source, language="nederlands"):
        """Initialize with a ly.document.Source.
        
        The language is by default set to "nederlands".
        
        """
        self.source = source
        self.setLanguage(language)
    
    def setLanguage(self, lang):
        r"""Changes the pitch name language to use.
        
        Called internally when \language or \include tokens are encountered
        with a valid language name/file.
        
        Sets the language attribute to the language name and the read attribute
        to an instance of ly.pitch.PitchReader.
        
        """
        if lang in pitchInfo.keys():
            self.language = lang
            return True
    
    def tokens(self):
        """Yield all the tokens from the source, following the language."""
        for t in self.source:
            yield t
            if isinstance(t, ly.lex.lilypond.Keyword):
                if t in ("\\include", "\\language"):
                    for t in self.source:
                        if not isinstance(t, ly.lex.Space) and t != '"':
                            lang = t[:-3] if t.endswith('.ly') else t[:]
                            if self.setLanguage(lang):
                                yield LanguageName(lang, t.pos)
                            break
                        yield t
    
    def read(self, token):
        """Reads the token and returns (note, alter) or None."""
        return pitchReader(self.language)(token)
    
    def pitches(self):
        """Yields all tokens, but collects Note and Octave tokens.
        
        When a Note is encountered, also reads octave and octave check and then
        a Pitch is yielded instead of the tokens.
        
        """
        tokens = self.tokens()
        for t in tokens:
            while isinstance(t, ly.lex.lilypond.Note):
                p = self.read(t)
                if not p:
                    break
                p = Pitch(*p)
                
                p.note_token = t
                p.octave_token = None
                p.accidental_token = None
                p.octavecheck_token = None
                
                t = None # prevent hang in this loop
                for t in tokens:
                    if isinstance(t, ly.lex.lilypond.Octave):
                        p.octave = octaveToNum(t)
                        p.octave_token = t
                    elif isinstance(t, ly.lex.lilypond.Accidental):
                        p.accidental_token = p.accidental = t
                    elif isinstance(t, ly.lex.lilypond.OctaveCheck):
                        p.octavecheck = octaveToNum(t)
                        p.octavecheck_token = t
                        break
                    elif not isinstance(t, ly.lex.Space):
                        break
                yield p
                if t is None:
                    break
            else:
                yield t
        
    def position(self, t):
        """Returns the cursor position for the given token or Pitch."""
        if isinstance(t, Pitch):
            t = t.note_token
        return self.source.position(t)
    
    def write(self, pitch, language=None):
        """Output a changed Pitch.
        
        The Pitch is written in the Source's document.
        
        To use this method reliably, you must instantiate the PitchIterator
        with a ly.document.Source that has tokens_with_position set to True.
        
        """
        document = self.source.document
        pwriter = pitchWriter(language or self.language)
        note = pwriter(pitch.note, pitch.alter)
        end = pitch.note_token.end
        if note != pitch.note_token:
            document[pitch.note_token.pos:end] = note
        octave = octaveToString(pitch.octave)
        if octave != pitch.octave_token:
            if pitch.octave_token is None:
                document[end:end] = octave
            else:
                end = pitch.octave_token.end
                document[pitch.octave_token.pos:end] = octave
        if pitch.accidental:
            if pitch.accidental_token is None:
                document[end:end] = pitch.accidental
            elif pitch.accidental != pitch.accidental_token:
                end = pitch.accidental_token.end
                document[pitch.accidental_token.pos:end] = pitch.accidental
        elif pitch.accidental_token:
            del document[pitch.accidental_token.pos:pitch.accidental_token.end]
        if pitch.octavecheck is not None:
            octavecheck = '=' + octaveToString(pitch.octavecheck)
            if pitch.octavecheck_token is None:
                document[end:end] = octavecheck
            elif octavecheck != pitch.octavecheck_token:
                document[pitch.octavecheck_token.pos:pitch.octavecheck_token.end] = octavecheck
        elif pitch.octavecheck_token:
            del document[pitch.octavecheck_token.pos:pitch.octavecheck_token.end]


class LanguageName(ly.lex.Token):
    """A Token that denotes a language name."""
    pass


