#from gui_tools import *
from gi.repository import GObject, Gdk, Gtk, GooCanvas, GLib
import gui_tools

def guint(x):
    value = GObject.Value()
    value.init(GObject.TYPE_UINT)
    value.set_uint(x)
    return value

PPQN = 48

def standard_filter(patterns, name):
    f = Gtk.FileFilter()
    for p in patterns:
        f.add_pattern(p)
    f.set_name(name)
    return f

def hide_item(citem):
    citem.visibility = GooCanvas.CanvasItemVisibility.HIDDEN

def show_item(citem):
    citem.visibility = GooCanvas.CanvasItemVisibility.VISIBLE

def polygon_to_path(points):
    assert len(points) % 2 == 0, "Invalid number of points (%d) in %s" % (len(points), repr(points))
    path = ""
    if len(points) > 0:
        path += "M %s %s" % (points[0], points[1])
        if len(points) > 1:
            for i in range(2, len(points), 2):
                path += " L %s %s" % (points[i], points[i + 1])
    return path

class NoteModel(object):
    def __init__(self, pos, channel, row, vel, len = 1):
        self.pos = int(pos)
        self.channel = int(channel)
        self.row = int(row)
        self.vel = int(vel)
        self.len = int(len)
        self.item = None
        self.selected = False
    def __str__(self):
        return "pos=%s row=%s vel=%s len=%s" % (self.pos, self.row, self.vel, self.len)

# This is stupid and needs rewriting using a faster data structure
class DrumPatternModel(GObject.GObject):
    def __init__(self, beats, bars):
        GObject.GObject.__init__(self)
        self.ppqn = PPQN
        self.beats = beats
        self.bars = bars
        self.notes = []
        
    def import_data(self, data):
        self.clear()
        active_notes = {}
        for t in data:
            cmd = t[1] & 0xF0
            if len(t) == 4 and (cmd == 0x90) and (t[3] > 0):
                note = NoteModel(t[0], (t[1] & 15) + 1, t[2], t[3])
                active_notes[t[2]] = note
                self.add_note(note)
            if len(t) == 4 and ((cmd == 0x90 and t[3] == 0) or cmd == 0x80):
                if t[2] in active_notes:
                    active_notes[t[2]].len = t[0] - active_notes[t[2]].pos
                    del active_notes[t[2]]
        end = self.get_length()
        for n in active_notes.values():
            n.len = end - n.pos

    def clear(self):
        self.notes = []
        self.changed()

    def add_note(self, note, send_changed = True):
        self.notes.append(note)
        if send_changed:
            self.changed()
        
    def remove_note(self, pos, row, channel):
        self.notes = [note for note in self.notes if note.pos != pos or note.row != row or (channel is not None and note.channel != channel)]
        self.changed()
        
    def set_note_vel(self, note, vel):
        note.vel = int(vel)
        self.changed()
            
    def set_note_len(self, note, len):
        note.len = int(len)
        self.changed()
            
    def has_note(self, pos, row, channel):
        for n in self.notes:
            if n.pos == pos and n.row == row and (channel is None or n.channel == channel):
                return True
        return False
            
    def get_note(self, pos, row, channel):
        for n in self.notes:
            if n.pos == pos and n.row == row and (channel is None or n.channel == channel):
                return n
        return None
            
    def items(self):
        return self.notes
        
    def get_length(self):
        return int(self.beats * self.bars * self.ppqn)
        
    def changed(self):
        self.emit('changed')

    def delete_selected(self):
        self.notes = [note for note in self.notes if not note.selected]
        self.changed()
        
    def group_set(self, **vals):
        for n in self.notes:
            if not n.selected:
                continue
            for k, v in vals.items():
                setattr(n, k, v)
        self.changed()

    def transpose_selected(self, amount):
        for n in self.notes:
            if not n.selected:
                continue
            if n.row + amount < 0 or n.row + amount > 127:
                continue
            n.row += amount
        self.changed()

GObject.type_register(DrumPatternModel)
GObject.signal_new("changed", DrumPatternModel, GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ())

channel_ls = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT)
for ch in range(1, 17):
    channel_ls.append((str(ch), ch))
snap_settings_ls = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT)
for row in [("1/4", PPQN), ("1/8", PPQN // 2), ("1/8T", PPQN//3), ("1/16", PPQN//4), ("1/16T", PPQN//6), ("1/32", PPQN//8), ("1/32T", PPQN//12), ("1/64", PPQN//4)]:
    snap_settings_ls.append(row)
edit_mode_ls = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING)
for row in [("Drum", "D"), ("Melodic", "M")]:
    edit_mode_ls.append(row)

class DrumEditorToolbox(Gtk.HBox):
    def __init__(self, canvas):
        Gtk.HBox.__init__(self, spacing = 5)
        self.canvas = canvas
        self.vel_adj = Gtk.Adjustment(100, 1, 127, 1, 10, 0)
        
        self.pack_start(Gtk.Label("Channel:"), False, False, 0)
        self.channel_setting = gui_tools.standard_combo(channel_ls, active_item_lookup = self.canvas.channel, lookup_column = 1)
        self.channel_setting.connect('changed', lambda w: self.canvas.set_channel(w.get_model()[w.get_active()][1]))
        self.pack_start(self.channel_setting, False, True, 0)
        
        self.pack_start(Gtk.Label("Mode:"), False, False, 0)
        self.mode_setting = gui_tools.standard_combo(edit_mode_ls, active_item_lookup = self.canvas.edit_mode, lookup_column = 1)
        self.mode_setting.connect('changed', lambda w: self.canvas.set_edit_mode(w.get_model()[w.get_active()][1]))
        self.pack_start(self.mode_setting, False, True, 0)
        
        self.pack_start(Gtk.Label("Snap:"), False, False, 0)
        self.snap_setting = gui_tools.standard_combo(snap_settings_ls, active_item_lookup = self.canvas.grid_unit, lookup_column = 1)
        self.snap_setting.connect('changed', lambda w: self.canvas.set_grid_unit(w.get_model()[w.get_active()][1]))
        self.pack_start(self.snap_setting, False, True, 0)
        
        self.pack_start(Gtk.Label("Velocity:"), False, False, 0)
        self.pack_start(Gtk.SpinButton(adjustment = self.vel_adj, climb_rate = 0, digits = 0), False, False, 0)
        button = Gtk.Button("Load")
        button.connect('clicked', self.load_pattern)
        self.pack_start(button, True, True, 0)
        button = Gtk.Button("Save")
        button.connect('clicked', self.save_pattern)
        self.pack_start(button, True, True, 0)
        button = Gtk.Button("Double")
        button.connect('clicked', self.double_pattern)
        self.pack_start(button, True, True, 0)
        self.pack_start(Gtk.Label("--"), False, False, 0)

    def update_edit_mode(self):
        self.mode_setting.set_active(gui_tools.ls_index(edit_mode_ls, self.canvas.edit_mode, 1))

    def load_pattern(self, w):
        dlg = Gtk.FileChooserDialog('Open a drum pattern', self.get_toplevel(), Gtk.FileChooserAction.OPEN,
            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.APPLY))
        dlg.add_filter(standard_filter(["*.cbdp"], "Drum patterns"))
        dlg.add_filter(standard_filter(["*"], "All files"))
        try:
            if dlg.run() == Gtk.ResponseType.APPLY:
                pattern = self.canvas.pattern
                f = file(dlg.get_filename(), "r")
                pattern.clear()
                pattern.beats, pattern.bars = [int(v) for v in f.readline().strip().split(";")]
                for line in f.readlines():
                    line = line.strip()
                    if not line.startswith("n:"):
                        pos, row, vel = line.split(";")
                        row = int(row) + 36
                        channel = 10
                        len = 1
                    else:
                        pos, channel, row, vel, len = line[2:].split(";")
                    self.canvas.pattern.add_note(NoteModel(pos, channel, row, vel, len), send_changed = False)
                f.close()
                self.canvas.pattern.changed()
                self.canvas.update_grid()
                self.canvas.update_notes()
        finally:
            dlg.destroy()
    def save_pattern(self, w):
        dlg = Gtk.FileChooserDialog('Save a drum pattern', self.get_toplevel(), Gtk.FileChooserAction.SAVE, 
            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.APPLY))
        dlg.add_filter(standard_filter(["*.cbdp"], "Drum patterns"))
        dlg.add_filter(standard_filter(["*"], "All files"))
        dlg.set_current_name("pattern.cbdp")
        try:
            if dlg.run() == Gtk.ResponseType.APPLY:            
                pattern = self.canvas.pattern
                f = file(dlg.get_filename(), "w")
                f.write("%s;%s\n" % (pattern.beats, pattern.bars))
                for i in self.canvas.pattern.items():
                    f.write("n:%s;%s;%s;%s;%s\n" % (i.pos, i.channel, i.row, i.vel, i.len))
                f.close()
        finally:
            dlg.destroy()
    def double_pattern(self, w):
        len = self.canvas.pattern.get_length()
        self.canvas.pattern.bars *= 2
        new_notes = []
        for note in self.canvas.pattern.items():
            new_notes.append(NoteModel(note.pos + len, note.channel, note.row, note.vel, note.len))
        for note in new_notes:
            self.canvas.pattern.add_note(note, send_changed = False)
        self.canvas.pattern.changed()
        self.canvas.update_size()
        self.canvas.update_grid()
        self.canvas.update_notes()

class DrumCanvasCursor(object):
    def __init__(self, canvas):
        self.canvas = canvas
        self.canvas_root = canvas.get_root_item()
        self.frame = GooCanvas.CanvasRect(parent = self.canvas_root, x = -6, y = -6, width = 12, height = 12 , stroke_color = "gray", line_width = 1)
        hide_item(self.frame)
        self.vel = GooCanvas.CanvasText(parent = self.canvas_root, x = 0, y = 0, fill_color = "blue", stroke_color = "blue", anchor = GooCanvas.CanvasAnchorType.S)
        hide_item(self.vel)
        self.rubberband = False
        self.rubberband_origin = None
        self.rubberband_current = None

    def hide(self):
        hide_item(self.frame)
        hide_item(self.vel)
        
    def show(self):
        show_item(self.frame)
        show_item(self.vel)
        
    def move_to_note(self, note):
        self.move(note.pos, note.row, note)
            
    def move(self, pulse, row, note):
        x = self.canvas.pulse_to_screen_x(pulse)
        y = self.canvas.row_to_screen_y(row) + self.canvas.row_height / 2
        dx = 0
        if note is not None:
            dx = self.canvas.pulse_to_screen_x(pulse + note.len) - x
        self.frame.set_properties(x = x - 6, width = 12 + dx, y = y - 6, height = 12)
        cy = y - self.canvas.row_height * 1.5 if y >= self.canvas.rows * self.canvas.row_height / 2 else y + self.canvas.row_height * 1.5
        if note is None:
            text = ""
        else:
            text = "[%s] %s" % (note.channel, note.vel)
        self.vel.set_properties(x = x, y = cy, text = text)
        
    def start_rubberband(self, x, y):
        self.rubberband = True
        self.rubberband_origin = (x, y)
        self.rubberband_current = (x, y)
        self.update_rubberband_frame()
        show_item(self.frame)
        
    def update_rubberband(self, x, y):
        self.rubberband_current = (x, y)
        self.update_rubberband_frame()
        
    def end_rubberband(self, x, y):
        self.rubberband_current = (x, y)
        self.update_rubberband_frame()
        hide_item(self.frame)
        self.rubberband = False
        
    def cancel_rubberband(self):
        hide_item(self.frame)
        self.rubberband = False
        
    def update_rubberband_frame(self):
        self.frame.set_properties(x = self.rubberband_origin[0],
            y = self.rubberband_origin[1],
            width = self.rubberband_current[0] - self.rubberband_origin[0],
            height = self.rubberband_current[1] - self.rubberband_origin[1])
        
    def get_rubberband_box(self):
        x1, y1 = self.rubberband_origin
        x2, y2 = self.rubberband_current
        return (min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2))

class DrumCanvas(GooCanvas.Canvas):
    def __init__(self, rows, pattern):
        GooCanvas.Canvas.__init__(self)
        self.rows = rows
        self.pattern = pattern
        self.row_height = 24
        self.grid_unit = PPQN / 4 # unit in pulses
        self.zoom_in = 2
        self.instr_width = 120
        self.edited_note = None
        self.orig_velocity = None
        self.orig_length = None
        self.orig_y = None
        self.channel_modes = ['D' if ch == 10 else 'M' for ch in range(1, 17)]
        
        self.update_size()
        
        self.grid = GooCanvas.CanvasGroup(parent = self.get_root_item(), x = self.instr_width)
        self.update_grid()

        self.notes = GooCanvas.CanvasGroup(parent = self.get_root_item(), x = self.instr_width)

        self.names = GooCanvas.CanvasGroup(parent = self.get_root_item(), x = 0)
        self.update_names()
        
        self.cursor = DrumCanvasCursor(self)
        hide_item(self.cursor)
        
        self.connect('event', self.on_grid_event)

        self.channel = 10
        self.edit_mode = self.channel_modes[self.channel - 1]
        self.toolbox = DrumEditorToolbox(self)

        self.add_events(Gdk.EventMask.POINTER_MOTION_HINT_MASK)

        self.grab_focus(self.grid)
        self.update_notes()

    def set_edit_mode(self, mode):
        self.edit_mode = mode
        self.channel_modes[self.channel - 1] = mode
        
    def calc_size(self):
        return (self.instr_width + self.pattern.get_length() * self.zoom_in + 1, self.rows * self.row_height + 1)
        
    def set_grid_unit(self, grid_unit):
        self.grid_unit = grid_unit
        self.update_grid()
        
    def set_channel(self, channel):
        self.channel = channel
        self.set_edit_mode(self.channel_modes[self.channel - 1])
        self.update_notes()
        self.toolbox.update_edit_mode()
        
    def update_size(self):
        sx, sy = self.calc_size()
        self.set_bounds(0, 0, sx, self.rows * self.row_height)
        self.set_size_request(sx, sy)
    
    def update_names(self):
        for i in self.names.items:
            i.destroy()
        for i in range(0, self.rows):
            #GooCanvas.CanvasText(parent = self.names, text = gui_tools.note_to_name(i), x = self.instr_width - 10, y = (i + 0.5) * self.row_height, anchor = Gtk.AnchorType.E, size_points = 10, font = "Sans", size_set = True)
            GooCanvas.CanvasText(parent = self.names, text = gui_tools.note_to_name(i), x = self.instr_width - 10, y = (i + 0.5) * self.row_height, anchor = GooCanvas.CanvasAnchorType.E, font = "Sans")
        
    def update_grid(self):
        for i in self.grid.items:
            i.destroy()
        bg = GooCanvas.CanvasRect(parent = self.grid, x = 0, y = 0, width = self.pattern.get_length() * self.zoom_in, height = self.rows * self.row_height, fill_color = "white")
        bar_fg = "blue"
        beat_fg = "darkgray"
        grid_fg = "lightgray"
        row_grid_fg = "lightgray"
        row_ext_fg = "black"
        for i in range(0, self.rows + 1):
            color = row_ext_fg if (i == 0 or i == self.rows) else row_grid_fg
            GooCanvas.CanvasPath(parent = self.grid, data = "M %s %s L %s %s" % (0, i * self.row_height, self.pattern.get_length() * self.zoom_in, i * self.row_height), stroke_color = color, line_width = 1)
        for i in range(0, self.pattern.get_length() + 1, int(self.grid_unit)):
            color = grid_fg
            if i % self.pattern.ppqn == 0:
                color = beat_fg
            if (i % (self.pattern.ppqn * self.pattern.beats)) == 0:
                color = bar_fg
            GooCanvas.CanvasPath(parent = self.grid, data = "M %s %s L %s %s" % (i * self.zoom_in, 1, i * self.zoom_in, self.rows * self.row_height - 1), stroke_color = color, line_width = 1)
            
    def update_notes(self):
        while self.notes.get_n_children() > 0:
            self.notes.remove_child(0)
        for item in self.pattern.items():
            x = self.pulse_to_screen_x(item.pos) - self.instr_width
            y = self.row_to_screen_y(item.row + 0.5)
            if item.channel == self.channel:
                fill_color = 0xC0C0C0 - int(item.vel * 1.5) * 0x000101
                stroke_color = 0x808080
                if item.selected:
                    stroke_color = 0xFF8080
            else:
                fill_color = 0xE0E0E0
                stroke_color = 0xE0E0E0
            if item.len > 1:
                x2 = self.pulse_to_screen_x(item.pos + item.len) - self.pulse_to_screen_x(item.pos)
                polygon = [-2, 0, 0, -5, x2 - 5, -5, x2, 0, x2 - 5, 5, 0, 5]
            else:
                polygon = [-5, 0, 0, -5, 5, 0, 0, 5, -5, 0]
            item.item = GooCanvas.CanvasPath(parent = self.notes, data = polygon_to_path(polygon), line_width = 1, fill_color = ("#%06x" % fill_color), stroke_color = ("#%06x" % stroke_color))
            #item.item.set_property('stroke_color_rgba', guint(stroke_color))
            item.item.translate(x, y)

    def set_selection_from_rubberband(self):
        sx, sy, ex, ey = self.cursor.get_rubberband_box()
        for item in self.pattern.items():
            x = self.pulse_to_screen_x(item.pos)
            y = self.row_to_screen_y(item.row + 0.5)
            item.selected = (x >= sx and x <= ex and y >= sy and y <= ey)
        self.update_notes()

    def on_grid_event(self, item, event):
        if event.type == Gdk.EventType.KEY_PRESS:
            return self.on_key_press(item, event)
        if event.type in [Gdk.EventType.BUTTON_PRESS, Gdk.EventType._2BUTTON_PRESS, Gdk.EventType.LEAVE_NOTIFY, Gdk.EventType.MOTION_NOTIFY, Gdk.EventType.BUTTON_RELEASE]:
            return self.on_mouse_event(item, event)

    def on_key_press(self, item, event):
        keyval, state = event.keyval, event.state
        kvname = Gdk.keyval_name(keyval)
        if kvname == 'Delete':
            self.pattern.delete_selected()
            self.update_notes()
        elif kvname == 'c':
            self.pattern.group_set(channel = self.channel)
            self.update_notes()
        elif kvname == 'v':
            self.pattern.group_set(vel = int(self.toolbox.vel_adj.get_value()))
            self.update_notes()
        elif kvname == 'plus':
            self.pattern.transpose_selected(1)
            self.update_notes()
        elif kvname == 'minus':
            self.pattern.transpose_selected(-1)
            self.update_notes()
        #else:
        #    print kvname

    def on_mouse_event(self, item, event):
        ex, ey = self.convert_to_item_space(self.get_root_item(), event.x, event.y)
        column = self.screen_x_to_column(ex)
        row = self.screen_y_to_row(ey)
        pulse = column * self.grid_unit
        epulse = (ex - self.instr_width) / self.zoom_in
        unit = self.grid_unit * self.zoom_in
        if self.cursor.rubberband:
            if event.type == Gdk.EventType.MOTION_NOTIFY:
                self.cursor.update_rubberband(ex, ey)
                return
            button = event.get_button()[1]
            if event.type == Gdk.EventType.BUTTON_RELEASE and button == 1:
                self.cursor.end_rubberband(ex, ey)
                self.set_selection_from_rubberband()
                self.request_update()
                return
            return
        if event.type == Gdk.EventType.BUTTON_PRESS:
            button = event.get_button()[1]
            self.grab_focus(self.grid)
            if ((event.state & Gdk.ModifierType.SHIFT_MASK) == Gdk.ModifierType.SHIFT_MASK) and button == 1:
                self.cursor.start_rubberband(ex, ey)
                return
            if pulse < 0 or pulse >= self.pattern.get_length():
                return
            note = self.pattern.get_note(pulse, row, self.channel)
            if note is not None:
                if button == 3:
                    vel = int(self.toolbox.vel_adj.get_value())
                    self.pattern.set_note_vel(note, vel)
                    self.cursor.move(pulse, row, note)
                    self.update_notes()
                    return
                self.toolbox.vel_adj.set_value(note.vel)
            else:
                note = NoteModel(pulse, self.channel, row, int(self.toolbox.vel_adj.get_value()), self.grid_unit if self.edit_mode == 'M' else 1)
                self.pattern.add_note(note)
            self.edited_note = note
            self.orig_length = note.len
            self.orig_velocity = note.vel
            self.orig_y = ey
            self.grab_add()
            self.cursor.move(self.edited_note.pos, self.edited_note.row, note)
            self.update_notes()
            return
        if event.type == Gdk.EventType._2BUTTON_PRESS:
            if pulse < 0 or pulse >= self.pattern.get_length():
                return
            if self.pattern.has_note(pulse, row, self.channel):
                self.pattern.remove_note(pulse, row, self.channel)
            self.cursor.move(pulse, row, None)
            self.update_notes()
            if self.edited_note is not None:
                self.grab_remove()
                self.edited_note = None
            return
        if event.type == Gdk.EventType.LEAVE_NOTIFY and self.edited_note is None:
            hide_item(self.cursor)
            return
        if event.type == Gdk.EventType.MOTION_NOTIFY and self.edited_note is None:
            if pulse < 0 or pulse >= self.pattern.get_length():
                hide_item(self.cursor)
                return
            
            if abs(pulse - epulse) > 5:
                hide_item(self.cursor)
                return
            note = self.pattern.get_note(column * self.grid_unit, row, self.channel)
            self.cursor.move(pulse, row, note)
            show_item(self.cursor)
            return
        if event.type == Gdk.EventType.MOTION_NOTIFY and self.edited_note is not None:
            vel = int(self.orig_velocity - self.snap(ey - self.orig_y) / 2)
            if vel < 1: vel = 1
            if vel > 127: vel = 127
            self.pattern.set_note_vel(self.edited_note, vel)
            len = pulse - self.edited_note.pos
            if self.edit_mode == 'D':
                len = 1
            elif len <= -self.grid_unit:
                len = self.orig_length
            elif len <= self.grid_unit:
                len = self.grid_unit
            else:
                len = int((len + 1) /  self.grid_unit) * self.grid_unit - 1
            self.pattern.set_note_len(self.edited_note, len)
            self.toolbox.vel_adj.set_value(vel)
            self.cursor.move_to_note(self.edited_note)
            self.update_notes()
            self.request_update()
            return
        if event.type == Gdk.EventType.BUTTON_RELEASE and self.edited_note is not None:
            self.edited_note = None
            self.grab_remove()
            return
            
    def screen_x_to_column(self, x):
        unit = self.grid_unit * self.zoom_in
        return int((x - self.instr_width + unit / 2) / unit)
        
    def screen_y_to_row(self, y):
        return int((y - 1) / self.row_height)
        
    def pulse_to_screen_x(self, pulse):
        return pulse * self.zoom_in + self.instr_width
        
    def column_to_screen_x(self, column):
        unit = self.grid_unit * self.zoom_in
        return column * unit + self.instr_width
        
    def row_to_screen_y(self, row):
        return row * self.row_height + 1
        
    def snap(self, val):
        if val > -10 and val < 10:
            return 0
        if val >= 10:
            return val - 10
        if val <= -10:
            return val + 10
        assert False

class DrumSeqWindow(Gtk.Window):
    def __init__(self, length, pat_data):
        Gtk.Window.__init__(self, Gtk.WindowType.TOPLEVEL)
        self.vbox = Gtk.VBox(spacing = 5)
        self.pattern = DrumPatternModel(4, length / (4 * PPQN))
        if pat_data is not None:
            self.pattern.import_data(pat_data)

        self.canvas = DrumCanvas(128, self.pattern)
        sw = Gtk.ScrolledWindow()
        sw.set_size_request(640, 400)
        sw.add_with_viewport(self.canvas)
        self.vbox.pack_start(sw, True, True, 0)
        self.vbox.pack_start(self.canvas.toolbox, False, False, 0)
        self.add(self.vbox)

if __name__ == "__main__":
    w = DrumSeqWindow()
    w.set_title("Drum pattern editor")
    w.show_all()
    w.connect('destroy', lambda w: Gtk.main_quit())

    Gtk.main()
