#!/usr/bin/python

from __future__ import absolute_import

import re

import gtk


if __name__ == '__main__':
    import sys
    sys.path.insert(0, ".")
    import solfege.i18n
    solfege.i18n.setup(".")

from solfege.mpd.duration import Duration
from solfege.mpd import const
from solfege.mpd import engravers
from solfege.mpd.rat import Rat
from solfege.lessonfile import Bar, RhythmStaff

class RhythmWidget(gtk.DrawingArea):
    """
    The rhythm widget will know how long rhythm to enter, and will
    know of all time signature changes and bar lines.
    """
    staff_ypos = 40
    class Cursor:
        def __init__(self, staff, bar=0, idx=0):
            self.m_staff = staff
            self.m_bar = bar
            self.m_idx = idx
        def __eq__(self, other):
            if not other:
                return False
            return (self.m_staff == other.m_staff
                    and self.m_bar == other.m_bar
                    and self.m_idx == other.m_idx)
        def get(self):
            """
            Return the item we are pointing to.
            """
            return self.m_staff[self.m_bar][self.m_idx]
        def clone(self):
            return RhythmWidget.Cursor(self.m_staff, self.m_bar, self.m_idx)
        def next_note(self):
            """
            Return a cursor object pointing to the next note, if there
            is written a note there. If we are on the last entered
            note in a bar, but there are empty space left, we will return
            None instead of pointing to the next bar, if the next bar has
            a note.
            """
            if not self.on_item():
                return
            # If we are on the last item in the bar
            # & the bar is not full
            if (self.m_idx == len(self.m_staff[self.m_bar]) - 1
                and not self.m_staff[self.m_bar].isfull()):
                return None
            c = self.clone()
            try:
                c.go_next()
            except IndexError:
                return
            if c.on_item() and c.get().m_isnote:
                return c
        def next_note2(self):
            """
            Return the next note, even if there are rests or empty space
            left in the bar. Return None if there are no more notes on the
            staff.
            """
            c = self.clone()
            while True:
                try:
                    c.go_next()
                except IndexError:
                    return
                if c.get().m_isnote:
                    return c
        def go_next(self):
            """
            Move the cursor to the next item.
            Raise IndexError if we are last.
            """
            if self.m_idx + 1 < len(self.m_staff[self.m_bar]):
                self.m_idx += 1
            elif self.m_bar + 1 < len(self.m_staff) and self.m_staff[self.m_bar+1]:
                self.m_bar += 1
                self.m_idx = 0
            else:
                raise IndexError()
        def on_item(self):
            """
            Return True if the cursor is placed on a note or a rest
            """
            if not self.m_staff[self.m_bar]:
                # empty bar
                return False
            if self.m_idx >= len(self.m_staff[self.m_bar]):
                return False
            return True
        def on_note(self):
            return self.on_item() and self.m_staff[self.m_bar][self.m_idx].m_isnote
        def __repr__(self):
            return "CURSOR<%i, %i>" % (self.m_bar, self.m_idx)
    def __init__(self):
        gtk.DrawingArea.__init__(self)
        self.set_size_request(700, 100)
        self.connect("expose_event", self.on_expose_event)
        self.add_events(gtk.gdk.KEY_RELEASE_MASK)
        self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
        self.connect("key-release-event", self.on_keypress) 
        self.set_flags(gtk.CAN_FOCUS)
        def f(*w):
            self.grab_focus()
        self.connect("button-press-event", f)
        self.m_staff = None
    def set_staff(self, staff):
        self.m_staff = staff
        self.m_staff.do_layout()
        self.m_cursor = self.Cursor(self.m_staff)
        self.queue_draw()
    def on_keypress(self, widget, event):
        if not self.m_editable:
            return 
        if event.keyval in (gtk.keysyms.Right, gtk.keysyms.KP_Right):
            return self.on_key_right()
        elif event.keyval in (gtk.keysyms.Left, gtk.keysyms.KP_Left):
            return self.on_key_left()
        elif event.keyval in (gtk.keysyms.Delete, gtk.keysyms.KP_Delete):
            self.on_delete()
        elif event.keyval == gtk.keysyms.BackSpace:
            self.on_backspace()
        elif event.keyval == gtk.keysyms.Home:
            self.m_cursor.m_idx = 0
            self.m_cursor.m_bar = 0
            self.queue_draw()
        elif event.keyval == gtk.keysyms.End:
            # End will move to after the last note
            self.on_end()
        elif event.keyval == gtk.keysyms._1:
            self.on_add_item(Bar.Item(True, Duration(1, 0), False))
        elif event.keyval == gtk.keysyms._2:
            self.on_add_item(Bar.Item(True, Duration(2, 0), False))
        elif event.keyval == gtk.keysyms._3:
            self.on_add_item(Bar.Item(True, Duration(4, 0), False))
        elif event.keyval == gtk.keysyms._4:
            self.on_add_item(Bar.Item(True, Duration(8, 0), False))
        elif event.keyval == gtk.keysyms._5:
            self.on_add_item(Bar.Item(True, Duration(16, 0), False))
        elif event.keyval == gtk.keysyms._6:
            self.on_add_item(Bar.Item(True, Duration(32, 0), False))
        elif event.keyval == gtk.keysyms.q:
            self.on_add_item(Bar.Item(False, Duration(1, 0), False))
        elif event.keyval == gtk.keysyms.w:
            self.on_add_item(Bar.Item(False, Duration(2, 0), False))
        elif event.keyval == gtk.keysyms.e:
            self.on_add_item(Bar.Item(False, Duration(4, 0), False))
        elif event.keyval == gtk.keysyms.r:
            self.on_add_item(Bar.Item(False, Duration(8, 0), False))
        elif event.keyval == gtk.keysyms.t:
            self.on_add_item(Bar.Item(False, Duration(16, 0), False))
        elif event.keyval == gtk.keysyms.y:
            self.on_add_item(Bar.Item(False, Duration(32, 0), False))
        elif event.keyval == gtk.keysyms.b:
            self.on_toggle_tie()
        elif event.keyval == gtk.keysyms.period:
            self.on_toggle_dots(1)
        elif event.keyval == gtk.keysyms.colon:
            self.on_toggle_dots(-1)
        return False
    def on_end(self):
        for baridx in reversed(range(len(self.m_staff))):
            # this bar is full
            if self.m_staff[baridx].isfull():
                self.m_cursor.m_bar = baridx
                self.m_cursor.m_idx = len(self.m_staff[self.m_cursor.m_bar]) - 1
                break
            # bar has some notes
            elif self.m_staff[baridx]:
                self.m_cursor.m_bar = baridx
                self.m_cursor.m_idx = len(self.m_staff[baridx])
                break
            # this is empty, prev bar is full
            elif baridx > 0 and self.m_staff[baridx - 1].isfull() and self.m_staff[baridx - 1].isfull():
                self.m_cursor.m_bar = baridx
                self.m_cursor.m_idx = 0
                break
        self.queue_draw()
    def on_delete(self):
        if self.m_cursor.on_item():
            del self.m_staff[self.m_cursor.m_bar][self.m_cursor.m_idx]
            self.m_staff.do_layout()
            self.queue_draw()
    def on_backspace(self):
        if self.m_cursor.m_idx > 0:
            del self.m_staff[self.m_cursor.m_bar][self.m_cursor.m_idx - 1]
            self.m_cursor.m_idx -= 1
        elif self.m_cursor.m_bar > 0:
            if self.m_staff[self.m_cursor.m_bar - 1].isfull():
                del self.m_staff[self.m_cursor.m_bar - 1][-1]
                self.m_cursor.m_bar -= 1
                self.m_cursor.m_idx = len(self.m_staff[self.m_cursor.m_bar])
            else:
                self.m_cursor.m_bar -= 1
                self.m_cursor.m_idx = len(self.m_staff[self.m_cursor.m_bar])
        self.m_staff.do_layout()
        self.queue_draw()
    def on_toggle_tie(self):
        if self.m_cursor.on_note():
            next_note = self.m_cursor.next_note()
            i = self.m_cursor.get()
            if next_note or i.m_tie:
                i.m_tie = not i.m_tie
                self.queue_draw()
    def on_toggle_dots(self, direction):
        if self.m_cursor.on_note():
            item = self.m_staff[self.m_cursor.m_bar][self.m_cursor.m_idx]
            if direction == 1:
                if item.m_duration.m_dots == 2:
                    item.m_duration.m_dots = 0
                else:
                    item.m_duration.m_dots +=1
            elif direction == -1:
                if item.m_duration.m_dots == 0:
                    item.m_duration.m_dots = 2
                else:
                    item.m_duration.m_dots -=1
            self.queue_draw()
    def on_add_item(self, item):
        """
        Append item if the cursor are on an empty place in the bar.
        Replace if on a note.
        """
        bar = self.m_staff[self.m_cursor.m_bar]
        # Check if the cursor is on a note
        if self.m_cursor.m_idx < len(bar):
            # Check if there is space in the bar for the note we want
            if bar.duration() - bar[self.m_cursor.m_idx].m_duration.get_rat_value() + item.m_duration.get_rat_value() <= bar.m_keysig:
                bar[self.m_cursor.m_idx] = item
            else:
                return
        else: # The cursor is on a empty space
            if bar.can_append(item):
                bar.append(item)
            else:
                return
        self.on_key_right()
        self.m_staff.do_layout()
        self.queue_draw()
    def on_key_right(self):
        bar = self.m_staff[self.m_cursor.m_bar]
        if self.m_cursor.m_idx < len(bar) - 1:
            self.m_cursor.m_idx += 1
        elif not self.m_staff[self.m_cursor.m_bar].isfull() and self.m_cursor.m_idx == len(bar) - 1:
            self.m_cursor.m_idx += 1
        elif self.m_cursor.m_bar < len(self.m_staff) - 1:
            self.m_cursor.m_bar += 1
            self.m_cursor.m_idx = 0
        self.queue_draw()
        return True
    def on_key_left(self):
        if self.m_cursor.m_idx > 0:
            self.m_cursor.m_idx -= 1
        elif self.m_cursor.m_bar > 0:
            self.m_cursor.m_bar -= 1
            self.m_cursor.m_idx = len(self.m_staff[self.m_cursor.m_bar]) - 1
        self.queue_draw()
        return True
    def on_expose_event(self, darea, event):
        width = self.get_allocation().width
        win = self.window
        win.draw_rectangle(self.style.white_gc, True,
            0, 0, self.get_allocation().width, self.get_allocation().height)
        if not self.m_staff:
            return
        first_bar = 0
        # the first bar that should be visible
        while sum(b.m_width for b in self.m_staff[first_bar:self.m_cursor.m_bar+1]) > width:
            first_bar += 1
        # the last bar that should be visible
        last_bar = first_bar
        while last_bar < len(self.m_staff) and sum(b.m_width for b in self.m_staff[first_bar:last_bar]) < width:
            last_bar += 1
        # subtract xd from all horizontal coords
        xd = self.m_staff[first_bar].m_xpos
            
        self.m_style = self.get_style()
        self.bg = self.style.bg_gc[gtk.STATE_NORMAL]
        self.fg = self.style.fg_gc[gtk.STATE_NORMAL]
        self.cu = self.style.fg_gc[gtk.STATE_PRELIGHT]
        colormap = win.get_colormap()
        red = colormap.alloc_color('#FF0000', True, True)
        red = win.new_gc(red)
        # draw the staff line
        stafflen = sum(b.m_width for b in self.m_staff[first_bar:last_bar])
        win.draw_line(self.style.black_gc, 10, self.staff_ypos, stafflen, self.staff_ypos)
        layout = self.create_pango_layout("")
        #
        for bar_idx, bar in enumerate(self.m_staff[first_bar:last_bar]):
            if bar.m_show_timesig:
                e = engravers.TimeSignatureEngraver(Rat(0, 1), 20,
                    bar.m_keysig)
                e.m_xpos = bar.m_xpos + 10 - xd
                e.engrave(self, self.style.black_gc, 40)
            for i_idx, i in enumerate(bar):
                if i.m_isnote:
                    # Same code as in mpd/parser.py
                    if i.m_duration.m_nh == 1:
                        h = 0
                    elif i.m_duration.m_nh == 2:
                        h = 1
                    else:
                        h = 2
                    e = engravers.NoteheadEngraver(Rat(0, 1), 20,
                            0, 0, h, i.m_duration.m_dots, 0, 0)
                    e.m_xpos = bar.m_xpos + i.m_xpos - xd
                    e.engrave(self, self.style.black_gc, 40)
                    if i.m_duration.m_nh > 4:
                        beam = engravers.BeamEngraver(20)
                        
                    if i.m_duration.m_nh > 1:
                        class dummy:
                            m_stemdir = const.UP
                            m_duration = i.m_duration
                        e = engravers.StemEngraver(Rat(0, 1), 20,
                            [0], dummy, False)
                        e.m_xpos = bar.m_xpos + i.m_xpos + 10 - xd
                        e.engrave(self, self.style.black_gc, 40)
                    if i.m_tie:
                        e = engravers.TieEngraver(Rat(0, 1), 20,
                            0, 0, 0, 2)
                        e.m_xpos = bar.m_xpos + i.m_xpos - xd + 5
                        c = RhythmWidget.Cursor(self.m_staff, bar_idx, i_idx)
                        c = c.next_note2()
                        if c:
                            c2 = RhythmWidget.Cursor(self.m_staff, bar_idx, i_idx)
                            c2 = c2.next_note()
                            e.m_xpos2 = self.m_staff[c.m_bar].m_xpos + self.m_staff[c.m_bar][c.m_idx].m_xpos
                            if c == c2:
                                e.engrave(self, self.style.black_gc, 40)
                            else:
                                e.engrave(self, red, 40)
                            
                else: # rest
                    e = engravers.RestEngraver(Rat(0, 1), 20, -2, i.m_duration)
                    e.m_xpos = bar.m_xpos + i.m_xpos - xd
                    e.engrave(self, self.style.black_gc, 40)

            # barline
            win.draw_line(self.style.black_gc, 
                bar.m_xpos + bar.m_width - xd, self.staff_ypos - 10,
                bar.m_xpos + bar.m_width - xd, self.staff_ypos + 10)
        # if cursor on note
        if self.m_cursor.m_idx < len(self.m_staff[self.m_cursor.m_bar]):
            cursor_xpos = (self.m_staff[self.m_cursor.m_bar].m_xpos
                + self.m_staff[self.m_cursor.m_bar][self.m_cursor.m_idx].m_xpos)
        # else cursor after note
        else:
            # empty bar
            if len(self.m_staff[self.m_cursor.m_bar]) == 0:
                cursor_xpos = self.m_staff[self.m_cursor.m_bar].m_xpos + Bar.stepwidth
            else:
                cursor_xpos = self.m_staff[self.m_cursor.m_bar].m_xpos + self.m_staff[self.m_cursor.m_bar][-1].m_xpos + Bar.stepwidth
        if self.m_editable:
            win.draw_rectangle(red, True,
                cursor_xpos - xd, self.staff_ypos + 20, 10, 5)

class Controller(gtk.HBox):
    def __init__(self, rwidget):
        """
        rwidget is the RhythmWidget we are controlling
        """
        gtk.HBox.__init__(self)
        self.g_rwidget = rwidget
        for k in (1, 2, 4, 8, 16, 32):
            b = gtk.Button(unicode(k))
            self.pack_start(b, False)
            def f(widget, i):
                self.g_rwidget.on_add_item(Bar.Item(True, Duration(i, 0), False))
                self.g_rwidget.grab_focus()
            b.connect('clicked', f, k)
        for k in (1, 2, 4, 8, 16, 32):
            b = gtk.Button(u"r%i" % k)
            self.pack_start(b, False)
            def f(widget, i):
                self.g_rwidget.on_add_item(Bar.Item(False, Duration(i, 0), False))
                self.g_rwidget.grab_focus()
            b.connect('clicked', f, k)
        b = gtk.Button("+.")
        b.connect('clicked', lambda w: self.g_rwidget.on_toggle_dots(1))
        self.pack_start(b, False)
        b = gtk.Button("-.")
        b.connect('clicked', lambda w: self.g_rwidget.on_toggle_dots(-1))
        self.pack_start(b, False)
        self.show_all()
    def set_editable(self, b):
        self.g_rwidget.m_editable = b
        self.g_rwidget.queue_draw()
        self.set_sensitive(b)



class TestWin(gtk.Window):
    def __init__(self):
        gtk.Window.__init__(self)
        vbox = gtk.VBox()
        self.add(vbox)
        self.set_default_size(600, 400)
        self.w = RhythmWidget()
        staff = RhythmStaff()
        staff.append(Bar(Rat(4, 4)))
        staff.append(Bar(Rat(4, 4)))
        self.w.set_staff(staff)
        vbox.pack_start(self.w)
        c = Controller(self.w)
        vbox.pack_start(c, False)
        c.show()
        c.set_editable(True)
        self.connect('delete_event', self.quit)
    def quit(self, *w):
        gtk.main_quit()

if __name__ == '__main__':
    w = TestWin()
    w.show_all()
    gtk.main()
