# GNU Solfege - free ear training software
# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2007, 2008  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 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/>.

from __future__ import absolute_import
import operator

import gtk
import pango

from solfege.mpd import const
from solfege.mpd import mpdutils

fetadir = "feta"
class dim20:
    linespacing = 8
    stemlen = linespacing * 3.5
    xshift = 10
    col_width = 20
    first_staff_ypos = 80
    staff_spacing = 100
    ledger_left = -4
    ledger_right = 14
    accidental_widths = { -2: 12, -1: 7, 0: 5, 1: 8, 2: 8}
    clef_yoffset = {
        'G': -38 + 3 * linespacing,
        'F':  -8 + 3 * linespacing,
        'C': -15 + 3 * linespacing,
    }

dimentions = {20: dim20}

accidental_y_offset = {const.ACCIDENTAL__2: -7,
                       const.ACCIDENTAL__1: -7,
                       const.ACCIDENTAL_0: -5,
                       const.ACCIDENTAL_1: -6,
                       const.ACCIDENTAL_2: -2}

class Engraver:
    def __init__(self, timepos, fontsize):
        self.m_fontsize = 20
        self.m_timepos = timepos
    def get_width(self):
        """
        Get the width of all engravers that does not implement their
        own get_width function.
        """
        return 20
    def __str__(self):
        return '(Engraver)'


class ClefEngraver(Engraver):
    def __init__(self, timepos, fontsize, clef):
        Engraver.__init__(self, timepos, fontsize)
        self.m_clef = clef
    def set_xpos(self, dict):
        self.m_xpos = dict[self.m_timepos].m_clef
    def get_width(self):
        return 25
    def engrave(self, widget, gc, staff_yoffset):
        widget.window.draw_pixbuf(gc,
            gtk.gdk.pixbuf_new_from_file(fetadir+'/feta%i-clefs-%s.xpm' \
                % (self.m_fontsize, self.m_clef.get_symbol())),
            0, 0, self.m_xpos,
            dimentions[self.m_fontsize].clef_yoffset[self.m_clef.get_symbol()] - self.m_clef.get_stafflinepos() * dimentions[self.m_fontsize].linespacing\
                + staff_yoffset)

class TimeSignatureEngraver(Engraver):
    def __init__(self, timepos, fontsize, timesig):
        Engraver.__init__(self, timepos, fontsize)
        self.m_timesig = timesig
    def set_xpos(self, dict):
        self.m_xpos = dict[self.m_timepos].m_timesignature
    def engrave(self, widget, gc, staff_yoffset):
        x = 0
        for idx in range(len(str(self.m_timesig.m_num))):
            n = str(self.m_timesig.m_num)[idx]
            widget.window.draw_pixbuf(gc,
                gtk.gdk.pixbuf_new_from_file(fetadir+'/feta%i-number-%s.xpm' \
                    % (self.m_fontsize, n)),
                0, 0,
                self.m_xpos+x, staff_yoffset-12)
            x  += 10
        x = 0
        for idx in range(len(str(self.m_timesig.m_den))):
            n = str(self.m_timesig.m_den)[idx]
            widget.window.draw_pixbuf(gc,
                gtk.gdk.pixbuf_new_from_file(fetadir+'/feta%i-number-%s.xpm' %\
                    (self.m_fontsize, n)),
                0, 0,
                self.m_xpos+x, staff_yoffset+3)
            x += 10
    def __str__(self):
        return '(TimeSignatureEngraver:%i/%i, xpos:%s)' % (self.m_timesig.m_den,
            self.m_timesig.m_num, self.m_xpos)


class TieEngraver(Engraver):
    def __init__(self, fontsize, pos1, pos2, shift1, shift2, ylinepos):
        Engraver.__init__(self, None, fontsize)
        self.m_pos1 = pos1
        self.m_pos2 = pos2
        self.m_shift1 = shift1
        self.m_shift2 = shift2
        self.m_ylinepos = ylinepos
    def set_xpos(self, dict):
        dim = dimentions[self.m_fontsize]
        self.m_xpos = dict[self.m_pos1].m_music
        self.m_xpos2 = dict[self.m_pos2].m_music
        self.m_xpos += 3 + (self.m_shift1 + 1) * dim.xshift
        self.m_xpos2 += - 3 + self.m_shift2 * dim.xshift
    def engrave(self, widget, gc, staff_yoffset):
        dim = dimentions[self.m_fontsize]
        w = self.m_xpos2 - self.m_xpos
        h = 8
        widget.window.draw_arc(gc, False,
              self.m_xpos, staff_yoffset + dim.linespacing*(self.m_ylinepos-1)/2 + 3,
              w, h, 64 * 180, 64*180)


class AccidentalsEngraver(Engraver):
    def __init__(self, timepos, fontsize, accs):
        Engraver.__init__(self, timepos, fontsize)
        self.m_accs = accs
        def f(v, w=dimentions[fontsize].accidental_widths):
            x = 0
            for a in v:
                x += w[a]
            return x
        self.m_usize = reduce(operator.__add__, map(f, accs.values())) + len(accs)
    def set_xpos(self, dict, tpd):
        self.m_xpos = dict[self.m_timepos].m_accidentals + tpd[self.m_timepos].m_accidentals - self.get_width()
    def get_width(self):
        return self.m_usize
    def engrave(self, widget, gc, staff_yoffset):
        x = 0
        for y in self.m_accs:
            for acc in self.m_accs[y]:
                widget.window.draw_pixbuf(gc,
                    gtk.gdk.pixbuf_new_from_file(
                        fetadir+'/feta%i-accidentals-%i.xpm' % \
                            (self.m_fontsize, acc)),
                        0, 0,
                        self.m_xpos + x,
                        int(accidental_y_offset[acc] + staff_yoffset
                         + dimentions[self.m_fontsize].linespacing*y/2
                         + accidental_y_offset[acc]))
                x += dimentions[self.m_fontsize].accidental_widths[acc] + 1


class KeySignatureEngraver(Engraver):
    def __init__(self, timepos, fontsize, old_key, key, clef):
        Engraver.__init__(self, timepos, fontsize)
        self.m_old_key = old_key
        self.m_key = key
        self.m_clef = clef
        self.m_old_accidentals = mpdutils.key_to_accidentals(old_key)
        self.m_accidentals = mpdutils.key_to_accidentals(key)
    def set_xpos(self, dict):
        self.m_xpos = dict[self.m_timepos].m_keysignature
    def get_width(self):
        #FIXME this value depends on the fontsize, and on what kind of
        #accidental we are drawing. A sharp is wider that a flat.
        return len(self.m_accidentals) * 10 + len(self.m_old_accidentals) * 6 + 8
    def engrave(self, widget, gc, staff_yoffset):
        x = 0
        dolist = []
        # natural signs
        for acc in self.m_old_accidentals:
            if acc in self.m_accidentals:
                continue
            ypos = self.m_clef.an_to_ylinepos(acc)
            dolist.append((0, x, ypos))
            #FIXME see FIXME msg in .get_width
            x += 6
        # accidentals
        for acc in self.m_accidentals:
            if acc.endswith('eses'):
                ktype = -2
            elif acc.endswith('es'):
                ktype = -1
            elif acc.endswith('isis'):
                ktype = 2
            else:
                ktype = 1
            ypos = self.m_clef.an_to_ylinepos(acc)
            dolist.append((ktype, x, ypos))
            #FIXME see FIXME msg in .get_width
            x += 10
        for ktype, x, ypos in dolist:
            widget.window.draw_pixbuf(gc,
                gtk.gdk.pixbuf_new_from_file(
                    fetadir+'/feta%i-accidentals-%i.xpm' \
                    % (self.m_fontsize, ktype)),
                0, 0,
                self.m_xpos + x,
                int(accidental_y_offset[ktype] + staff_yoffset
                     + dimentions[self.m_fontsize].linespacing*ypos/2
                     + accidental_y_offset[ktype]))


class NoteheadEngraver(Engraver):
    def __init__(self, timepos, fontsize, shift, ypos, head, numdots, midi_int, mgroup):
        """
        m_ypos == 0 is the middle line on the staff.
        Negative value is up, positive is down.
        The value counts notesteps, not pixels!
        FIXME, right now it also draws dots, but they should be an own class
        because the dots has to be handled special with noteheads are xshifted.
        """
        Engraver.__init__(self, timepos, fontsize)
        self.m_head = head
        self.m_shift = shift
        self.m_numdots = numdots
        self.m_ypos = ypos
        self.m_midi_int = midi_int
        self.m_mgroup = mgroup
    def set_xpos(self, dict):
        self.m_xpos = dict[self.m_timepos].m_music
    def engrave(self, widget, gc, staff_yoffset):
        dim = dimentions[self.m_fontsize]
        # Have to adjust holenotes a little to the left to be on the middle
        # of the ledger line.
        if self.m_head == 0:
            xx = -2
        else:
            xx = 0
        widget.window.draw_pixbuf(gc, 
            gtk.gdk.pixbuf_new_from_file(fetadir+'/feta%i-noteheads-%i.xpm' \
                % (self.m_fontsize, self.m_head)),
            0, 0,
            self.m_xpos + self.m_shift * dim.xshift + xx,
            int(staff_yoffset + dim.linespacing*self.m_ypos/2 - 4))
        for n in range(self.m_numdots):
            widget.window.draw_pixbuf(gc,
                gtk.gdk.pixbuf_new_from_file(fetadir+'/feta20-dots-dot.xpm'),
                0, 0,
                int(self.m_xpos+dim.xshift*(self.m_shift+1.5+n/2.0)),
                -3 + staff_yoffset + dim.linespacing*self.m_ypos/2)
    def __str__(self):
        return "(NoteheadEngraver: xpos:%i, ypos:%i, head:%i)" % (self.m_xpos,
                                                                    self.m_ypos,
                                                                    self.m_head)

class BarlineEngraver(Engraver):
    def __init__(self, timepos, fontsize, barline_type):
        Engraver.__init__(self, timepos, fontsize)
        self.m_type = barline_type
    def set_xpos(self, dict):
        self.m_xpos = dict[self.m_timepos].m_barline
    def get_width(self):
        return 8
    def engrave(self, widget, gc, staff_yoffset):
        dim = dimentions[self.m_fontsize]
        widget.window.draw_line(gc, self.m_xpos, staff_yoffset - dim.linespacing*2,
                                    self.m_xpos, staff_yoffset + dim.linespacing*2)

class TupletEngraver(Engraver):
    def __init__(self, fontsize, n):
        Engraver.__init__(self, None, fontsize)
        self.m_stems = []
        self.m_den, self.m_direction = n
    def set_xpos(self, dict):
        pass
    def add_stem(self, stem):
        self.m_stems.append(stem)
    def do_layout(self):
        dim = dimentions[self.m_fontsize]
        top = []
        bottom = []
        for s in self.m_stems:
            if s.m_mgroup.m_stemdir == const.UP:
                top.append(min(s.m_yposes)-7)
                bottom.append(max(s.m_yposes)+1)
            else:
                top.append(min(s.m_yposes))
                bottom.append(max(s.m_yposes)+5)
        self.m_t = min(top) - 2
        self.m_b = max(bottom) + 2
        self.m_xpos1 = self.m_stems[0].m_xpos
        self.m_xpos2 = self.m_stems[-1].m_xpos + dim.xshift
    def engrave(self, widget, gc, staff_yoffset):
        dim = dimentions[self.m_fontsize]
        m = self.m_xpos1 + (self.m_xpos2 - self.m_xpos1)/2
        x1 = m - 6
        x2 = m + 6
        if self.m_direction == const.UP or self.m_direction == const.BOTH:
            y = min(staff_yoffset - dim.linespacing * 3,
                   staff_yoffset + self.m_t * dim.linespacing / 2)
            d = 1
        else: # == const.DOWN
            y = max(staff_yoffset + dim.linespacing * 5,
                    staff_yoffset + self.m_b * dim.linespacing /2)
            d = -1
        widget.window.draw_line(gc, self.m_xpos1, y, x1, y)
        widget.window.draw_line(gc, x2, y, self.m_xpos2, y)
        widget.window.draw_line(gc, self.m_xpos1, y, self.m_xpos1, y+5*d)
        widget.window.draw_line(gc, self.m_xpos2, y, self.m_xpos2, y+5*d)
        cc = widget.create_pango_context()
        ll = pango.Layout(cc)
        ll.set_text(str(self.m_den))
        ll.set_font_description(pango.FontDescription("Sans 10"))
        sx, sy = [i / pango.SCALE for i in ll.get_size()]
        widget.window.draw_layout(gc, m - sx / 2, y - sy / 2, ll)

class BeamEngraver(Engraver):
    def __init__(self, fontsize):
        Engraver.__init__(self, None, fontsize)
        self.m_stems = []
    def add_stem(self, stem_engraver):
        self.m_stems.append(stem_engraver)
    def set_xpos(self, dict):
        pass
    def do_layout(self):
        self.decide_beam_stemdir()
        self.set_stemlens(self.find_lowhigh_ypos())
    def set_stemlens(self, lh):
        l, h = lh
        for e in self.m_stems:
            if self.m_stemdir == const.UP:
                e.m_beamed_stem_top = l - 6
            else:
                assert self.m_stemdir == const.DOWN
                e.m_beamed_stem_top = h + 6
    def find_lowhigh_ypos(self):
        """
        Find the lowest and highest notehead in the beam.
        """
        mn = 1000
        mx = -1000
        for se in self.m_stems:
            for ylinepos in se.m_yposes:
                if mn > ylinepos:
                    mn = ylinepos
                if mx < ylinepos:
                    mx = ylinepos
        return mn, mx
    def decide_beam_stemdir(self):
        """
        Decide the direction for the stems in this beam, and set
        the stemdir for all stems.
        """
        v = {const.UP: 0, const.DOWN: 0}
        for e in self.m_stems:
            v[e.m_mgroup.m_stemdir] = v[e.m_mgroup.m_stemdir] + 1
            e.m_stemlen = 50
        if v[const.UP] > v[const.DOWN]:
            stemdir = const.UP
        else:
            stemdir = const.DOWN
        for e in self.m_stems:
            e.m_mgroup.m_stemdir = stemdir
        self.m_stemdir = stemdir
        for e in self.m_stems:
            e.m_mgroup.m_stemdir = stemdir
    def engrave(self, widget, gc, staff_yoffset):
        dim = dimentions[self.m_fontsize]
        d = 0
        if self.m_stemdir == const.UP:
            d = 1
        else:
            d = -1
        x1 = self.m_stems[0].m_xpos
        x2 = self.m_stems[-1].m_xpos
        if self.m_stems[0].m_beamed_stem_top % 2 == 1:
            for x in range(len(self.m_stems)):
                self.m_stems[x].m_beamed_stem_top =\
                    self.m_stems[x].m_beamed_stem_top - d
        y1 = self.m_stems[0].m_beamed_stem_top * dim.linespacing/2 + staff_yoffset
        beamw = 3
        beaml = 10
        for y in range(beamw):
            widget.window.draw_line(gc, x1, y1 + y*d, x2, y1 + y*d)
        for stem in self.m_stems:
            stem._beam_done = 8
        def short_beam(stem, xdir, beamnum, d=d, widget=widget, beamw=beamw, gc=gc, beaml=beaml, y1=y1):
            for y in range(beamw):
                widget.window.draw_line(gc, stem.m_xpos, y1 + y + beamnum*d*beamw*2,
                       stem.m_xpos + xdir*beaml, y1 + y + beamnum*d*beamw*2)
        for nl, yc in ((16, 0), (32, 1), (64, 2)):
            for i in range(len(self.m_stems)-1):
                if self.m_stems[i].m_mgroup.m_duration.m_nh >= nl \
                        and self.m_stems[i+1].m_mgroup.m_duration.m_nh >= nl:
                    for y in range(beamw):
                        widget.window.draw_line(gc,
                            self.m_stems[i].m_xpos, d*beamw*yc*2 + y1 + y + d*beamw*2,
                            self.m_stems[i+1].m_xpos, d*beamw*yc*2 + y1 + y + d*beamw*2)
                    self.m_stems[i]._beam_done = nl
                    self.m_stems[i+1]._beam_done = nl
                if self.m_stems[i].m_mgroup.m_duration.m_nh >= nl \
                      and self.m_stems[i+1].m_mgroup.m_duration.m_nh <= nl/2 \
                      and self.m_stems[i]._beam_done < nl:
                    if i == 0:
                        stemdir = 1
                    else:
                        if self.m_stems[i-1].m_mgroup.m_duration.m_nh < \
                           self.m_stems[i+1].m_mgroup.m_duration.m_nh:
                            stemdir = 1
                        else:
                            stemdir = -1
                    short_beam(self.m_stems[i], stemdir, yc+1, d)
                    self.m_stems[i]._beam_done = nl
            if self.m_stems[-1].m_mgroup.m_duration.m_nh >= nl \
                    and self.m_stems[-1]._beam_done <= nl/2:
                short_beam(self.m_stems[-1], -1, yc+1, d)


class StemEngraver(Engraver):
    """
    Every notehead belong to a stem, even if the stem is invisible.
    """
    def __init__(self, timepos, fontsize, yposes, mgroup, is_beamed):
        Engraver.__init__(self, timepos, fontsize)
        self.m_mgroup = mgroup
        self.m_yposes = yposes
        self.m_is_beamed = is_beamed
        if len(self.m_yposes) == 1:
            x = self.m_yposes[0]
        else:
            x = self.m_yposes[0] + self.m_yposes[-1]
        if self.m_mgroup.m_stemdir == const.UP \
            or (self.m_mgroup.m_stemdir == const.BOTH and x >= 0):
            self.m_mgroup.m_stemdir = const.UP
        else:
            self.m_mgroup.m_stemdir = const.DOWN
        self.m_stemlen = dimentions[self.m_fontsize].stemlen
    def set_xpos(self, dict):
        self.m_xpos = dict[self.m_timepos].m_music
    def calc_xpos(self):
        if self.m_mgroup.m_stempos == 1 or self.m_mgroup.m_stemdir == const.UP:
            self.m_xpos = self.m_xpos + dimentions[self.m_fontsize].xshift
    def engrave(self, widget, gc, staff_yoffset):
        dim = dimentions[self.m_fontsize]
        # draw flags
        if not self.m_is_beamed and self.m_mgroup.m_duration.m_nh > 4:
            if self.m_mgroup.m_stemdir == const.UP:
                widget.window.draw_pixbuf(gc,
                    gtk.gdk.pixbuf_new_from_file(
                        fetadir+'/feta%i-flags-%s.xpm' % \
                            (self.m_fontsize, self.get_flag(const.UP))),
                    0, 0,
                    self.m_xpos,
                    int(staff_yoffset - self.m_stemlen + self.m_yposes[0] * dim.linespacing/2))
            else:
                widget.window.draw_pixbuf(gc,
                    gtk.gdk.pixbuf_new_from_file(
                        fetadir+'/feta%i-flags-%s.xpm' \
                        % (self.m_fontsize, self.get_flag(const.DOWN))),
                    0, 0,
                    self.m_xpos,
                    int(staff_yoffset + 4 + self.m_yposes[-1] * dim.linespacing/2))
        # draw stem
        if self.m_mgroup.m_stemdir == const.DOWN:
            if self.m_is_beamed:
                yroot = self.m_beamed_stem_top
            else:
                yroot = self.m_yposes[-1] + 6
            ytop = self.m_yposes[0]
        else:
            if self.m_is_beamed:
                yroot = self.m_beamed_stem_top
            else:
                yroot = self.m_yposes[0] - 6
            ytop = self.m_yposes[-1]
        widget.window.draw_line(gc,
                     self.m_xpos,
                     ytop * dim.linespacing/2 + staff_yoffset,
                     self.m_xpos,
                     yroot * dim.linespacing/2 + staff_yoffset)
    def get_flag(self, stemdir):
        assert self.m_mgroup.m_duration.m_nh > 4
        return {8:{const.UP: const.FLAG_8UP,
                             const.DOWN: const.FLAG_8DOWN},
                16: {const.UP: const.FLAG_16UP,
                               const.DOWN: const.FLAG_16DOWN},
                32: {const.UP: const.FLAG_32UP,
                               const.DOWN: const.FLAG_32DOWN},
                64: {const.UP: const.FLAG_64UP,
                               const.DOWN: const.FLAG_64DOWN}} \
                              [self.m_mgroup.m_duration.m_nh][stemdir]
    def __str__(self):
        return "(StemEngraver)"


class LedgerLineEngraver(Engraver):
    def __init__(self, timepos, fontsize, up, down):
        """
        up: number of ledger lines above staff
        down: number of ledger lines below staff
        """
        Engraver.__init__(self, timepos, fontsize)
        self.m_up = up
        self.m_down = down
    def set_xpos(self, dict):
        self.m_xpos = dict[self.m_timepos].m_music
    def engrave(self, widget, gc, staff_yoffset):
        dim = dimentions[self.m_fontsize]
        if self.m_up:
            for y in range(self.m_up):
                widget.window.draw_line(gc,
                    self.m_xpos+dim.ledger_left, (-y-3) * dim.linespacing + staff_yoffset,
                    self.m_xpos+dim.ledger_right, (-y-3) * dim.linespacing + staff_yoffset)
        if self.m_down:
            for y in range(self.m_down):
                widget.window.draw_line(gc,
                    self.m_xpos+dim.ledger_left, (y+3) * dim.linespacing + staff_yoffset,
                    self.m_xpos+dim.ledger_right, (y+3) * dim.linespacing + staff_yoffset)
    def __str__(self):
        return "(LedgerLineEngraver xpos:%i, updown%i%i" % (
            self.m_xpos, self.m_up, self.m_down)


class RestEngraver(Engraver):
    def __init__(self, timepos, fontsize, ypos, dur):
        Engraver.__init__(self, timepos, fontsize)
        self.m_ypos = ypos
        self.m_dots = dur.m_dots
        self.m_type = {1: 0, 2: 1, 4: 2, 8: 3, 16: 4, 32: 5, 64: 6}[dur.m_nh]
    def set_xpos(self, dict):
        self.m_xpos = dict[self.m_timepos].m_music
    def get_width(self):
        #FIXME write me!
        return 20
    def engrave(self, widget, gc, staff_yoffset):
        dim = dimentions[self.m_fontsize]
        if self.m_type == 0:
            my = dim.linespacing/2
        else:
            my = 0
        widget.window.draw_pixbuf(gc,
            gtk.gdk.pixbuf_new_from_file(fetadir+'/feta%i-rests-%i.xpm' \
                % (self.m_fontsize, self.m_type)),
            0, 0,
            self.m_xpos,
            int(staff_yoffset - my + dim.linespacing*self.m_ypos/2 - 4))
        for n in range(self.m_dots):
            widget.window.draw_pixbuf(gc,
                gtk.gdk.pixbuf_new_from_file(fetadir+'/feta20-dots-dot.xpm'),
                0, 0,
                int(self.m_xpos+dim.xshift*(1.5+n/2.0)),
                -3 + staff_yoffset + dim.linespacing*self.m_ypos/2)
    def __str__(self):
        return "(RestEngraver)"
