import cbox
import glob
import os
from gui_tools import *
import sfzparser

#sample_dir = "/media/resources/samples/dooleydrums/"
sample_dir = cbox.Config.get("init", "sample_dir")

####################################################################################################################################################

class SampleDirsModel(Gtk.ListStore):
    def __init__(self):
        Gtk.ListStore.__init__(self, GObject.TYPE_STRING, GObject.TYPE_STRING)
        found = False
        for entry in cbox.Config.keys("sample_dirs"):
            path = cbox.Config.get("sample_dirs", entry)
            self.append((entry, path))
            found = True
        if not found:
            print ("Warning: no sample directories defined. Please add one or more entries of a form: 'name=/path/to/my/samples' to [sample_dirs] section of .cboxrc")
            self.append(("home", os.getenv("HOME")))
            self.append(("/", "/"))
    def has_dir(self, dir):
        return dir in [path for entry, path in self]

####################################################################################################################################################

class SampleFilesModel(Gtk.ListStore):
    def __init__(self, dirs_model):
        self.dirs_model = dirs_model
        self.is_refreshing = False
        Gtk.ListStore.__init__(self, GObject.TYPE_STRING, GObject.TYPE_STRING)
        
    def refresh(self, sample_dir):
        try:
            self.is_refreshing = True
            self.clear()
            if sample_dir is not None:
                if not self.dirs_model.has_dir(sample_dir):
                    self.append((os.path.dirname(sample_dir.rstrip("/")) + "/", "(up)"))
                filelist = sorted(glob.glob("%s/*" % sample_dir))
                for f in sorted(filelist):
                    if os.path.isdir(f) and not self.dirs_model.has_dir(f + "/"):
                        self.append((f + "/", os.path.basename(f)+"/"))
                for f in sorted(filelist):
                    if f.lower().endswith(".wav") and not os.path.isdir(f):
                        self.append((f,os.path.basename(f)))
        finally:
            self.is_refreshing = False

####################################################################################################################################################

class KeyModelPath(object):
    def __init__(self, controller, var = None):
        self.controller = controller
        self.var = var
        self.args = []
    def plus(self, var):
        if self.var is not None:
            print ("Warning: key model plus used twice with %s and %s" % (self.var, var))
        return KeyModelPath(self.controller, var)
    def set(self, value):
        model = self.controller.get_current_layer_model()
        oldval = model.attribs[self.var]
        model.attribs[self.var] = value
        if value != oldval and not self.controller.no_sfz_update:
            print ("%s: set %s to %s" % (self.controller, self.var, value))
            self.controller.update_kit_later()

####################################################################################################################################################

layer_attribs = {
    'volume' : 0.0,
    'pan' : 0.0,
    'ampeg_attack' : 0.001,
    'ampeg_hold' : 0.001,
    'ampeg_decay' : 0.001,
    'ampeg_sustain' : 100.0,
    'ampeg_release' : 0.1,
    'tune' : 0.0,
    'transpose' : 0,
    'cutoff' : 22000.0,
    'resonance' : 0.7,
    'fileg_depth' : 0.0,
    'fileg_attack' : 0.001,
    'fileg_hold' : 0.001,
    'fileg_decay' : 0.001,
    'fileg_sustain' : 100.0,
    'fileg_release' : 0.1,
    'lovel' : 1,
    'hivel' : 127,
    'group' : 0,
    'off_by' : 0,
    'effect1' : 0,
    'effect2' : 0,
    'output' : 0,
}

####################################################################################################################################################

class KeySampleModel(object):
    def __init__(self, key, sample, filename):
        self.key = key
        self.sample = sample
        self.filename = filename
        self.mode = "one_shot"
        self.attribs = layer_attribs.copy()
    def set_sample(self, sample, filename):
        self.sample = sample
        self.filename = filename
    def to_sfz(self):
        if self.filename == '':
            return ""
        s = "<region> key=%d sample=%s loop_mode=%s" % (self.key, self.filename, self.mode)
        s += "".join([" %s=%s" % item for item in self.attribs.items()])
        return s + "\n"
    def to_markup(self):
        return "<small>%s</small>" % self.sample

####################################################################################################################################################

class KeyModel(Gtk.ListStore):
    def __init__(self, key):
        self.key = key
        Gtk.ListStore.__init__(self, GObject.TYPE_STRING, GObject.TYPE_PYOBJECT)
    def to_sfz(self):
        return "".join([ksm.to_sfz() for name, ksm in self])
    def to_markup(self):
        return "\n".join([ksm.to_markup() for name, ksm in self])

####################################################################################################################################################

class BankModel(dict):
    def __init__(self):
        dict.__init__(self)
        self.clear()
    def to_sfz(self):
        s = ""
        for key in self:
            s += self[key].to_sfz()
        return s
    def clear(self):
        dict.clear(self)
        for b in range(36, 36 + 16):
            self[b] = KeyModel(b)
    def from_sfz(self, data, path):
        self.clear()
        sfz = sfzparser.SFZ()
        sfz.parse(data)
        for r in sfz.regions:
            rdata = r.merged()
            if ('key' in rdata) and ('sample' in rdata) and (rdata['sample'] != ''):
                key = sfznote2value(rdata['key'])
                sample = rdata['sample']
                sample_short = os.path.basename(sample)
                if key in self:
                    ksm = KeySampleModel(key, sample_short, sfzparser.find_sample_in_path(path, sample))
                    for k, v in rdata.items():
                        if k in ksm.attribs:
                            if type(layer_attribs[k]) is float:
                                ksm.attribs[k] = float(v)
                            elif type(layer_attribs[k]) is int:
                                ksm.attribs[k] = int(float(v))
                            else:
                                ksm.attribs[k] = v
                    self[key].append((sample_short, ksm))

####################################################################################################################################################

class LayerListView(Gtk.TreeView):
    def __init__(self, controller):
        Gtk.TreeView.__init__(self, None)
        self.controller = controller
        self.insert_column_with_attributes(0, "Name", Gtk.CellRendererText(), text=0)
        self.set_cursor((0,))
        #self.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [("text/plain", 0, 1)], Gdk.DragAction.COPY)
        self.connect('cursor-changed', self.cursor_changed)
        #self.connect('drag-data-get', self.drag_data_get)
        self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
        self.drag_dest_set_target_list([])
        self.drag_dest_add_text_targets()
        self.connect('drag_data_received', self.drag_data_received)
    def cursor_changed(self, w):
        self.controller.on_layer_changed()
    def drag_data_received(self, widget, context, x, y, selection, info, etime):
        sample, filename = selection.get_text().split("|")
        pad_model = self.controller.get_current_pad_model()
        pad_model.append((sample, KeySampleModel(pad_model.key, sample, filename)))
        self.controller.current_pad.update_label()
        self.controller.on_sample_dragged(self)

####################################################################################################################################################

class LayerEditor(Gtk.VBox):
    def __init__(self, controller, bank_model):
        Gtk.VBox.__init__(self)
        self.table = Gtk.Table(len(self.fields) + 1, 2)
        self.table.set_size_request(240, -1)
        self.controller = controller
        self.bank_model = bank_model
        self.name_widget = Gtk.Label()
        self.table.attach(self.name_widget, 0, 2, 0, 1)
        self.refreshers = []
        for i in range(len(self.fields)):
            self.refreshers.append(self.fields[i].add_row(self.table, i + 1, KeyModelPath(controller), None))
            #self.table.attach(left_label(self.fields[i].label), 0, 1, i + 1, i + 2)
        self.pack_start(self.table, False, False, 0)
        
    def refresh(self):
        data = self.controller.get_current_layer_model()
        if data is None:
            self.name_widget.set_text("")
        else:
            self.name_widget.set_text(data.sample)
            data = data.attribs
        for r in self.refreshers:
            r(data)

    fields = [
        SliderRow("Volume", "volume", -100, 0),
        SliderRow("Pan", "pan", -100, 100),
        SliderRow("Effect 1", "effect1", 0, 100),
        SliderRow("Effect 2", "effect2", 0, 100),
        IntSliderRow("Output", "output", 0, 7),
        SliderRow("Tune", "tune", -100, 100),
        IntSliderRow("Transpose", "transpose", -48, 48),
        IntSliderRow("Low velocity", "lovel", 1, 127),
        IntSliderRow("High velocity", "hivel", 1, 127),
        MappedSliderRow("Amp Attack", "ampeg_attack", env_mapper),
        MappedSliderRow("Amp Hold", "ampeg_hold", env_mapper),
        MappedSliderRow("Amp Decay", "ampeg_decay", env_mapper),
        SliderRow("Amp Sustain", "ampeg_sustain", 0, 100),
        MappedSliderRow("Amp Release", "ampeg_release", env_mapper),
        MappedSliderRow("Flt Cutoff", "cutoff", filter_freq_mapper),
        MappedSliderRow("Flt Resonance", "resonance", LogMapper(0.707, 16, "%0.1f x")),
        SliderRow("Flt Depth", "fileg_depth", -4800, 4800),
        MappedSliderRow("Flt Attack", "fileg_attack", env_mapper),
        MappedSliderRow("Flt Hold", "fileg_hold", env_mapper),
        MappedSliderRow("Flt Decay", "fileg_decay", env_mapper),
        SliderRow("Flt Sustain", "fileg_sustain", 0, 100),
        MappedSliderRow("Flt Release", "fileg_release", env_mapper),
        IntSliderRow("Group", "group", 0, 15),
        IntSliderRow("Off by group", "off_by", 0, 15),
    ]
    
####################################################################################################################################################

class PadButton(Gtk.RadioButton):
    def __init__(self, controller, bank_model, key):
        Gtk.RadioButton.__init__(self, use_underline = False)
        self.set_mode(False)
        self.controller = controller
        self.bank_model = bank_model
        self.key = key
        self.set_size_request(100, 100)
        self.update_label()
        self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
        self.drag_dest_set_target_list([])
        self.drag_dest_add_text_targets()
        self.connect('drag_data_received', self.drag_data_received)
        #self.connect('toggled', lambda widget: widget.controller.on_pad_selected(widget) if widget.get_active() else None)
        self.connect('pressed', self.on_clicked)
    def get_key_model(self):
        return self.bank_model[self.key]
    def drag_data_received(self, widget, context, x, y, selection, info, etime):
        sample, filename = selection.get_text().split("|")
        self.get_key_model().clear()
        self.get_key_model().append((sample, KeySampleModel(self.key, sample, filename)))
        self.update_label()
        self.controller.on_sample_dragged(self)
    def update_label(self):
        data = self.get_key_model()
        if data == None:
            self.set_label("-")
        else:
            self.set_label("-")
            self.get_child().set_markup(data.to_markup())
            self.get_child().set_line_wrap(True)
    def on_clicked(self, w):
        self.controller.play_note(self.key)
        w.controller.on_pad_selected(w)

####################################################################################################################################################

class PadTable(Gtk.Table):
    def __init__(self, controller, bank_model, rows, columns):
        Gtk.Table.__init__(self, rows, columns, True)
        
        self.keys = {}
        group = None
        for r in range(0, rows):
            for c in range(0, columns):
                key = 36 + (rows - r - 1) * columns + c
                b = PadButton(controller, bank_model, key)
                if group is not None:
                    b.set_group(group)
                self.attach(standard_align(b, 0.5, 0.5, 0, 0), c, c + 1, r, r + 1)
                self.keys[key] = b
    def refresh(self):
        for pad in self.keys.values():
            pad.update_label()

####################################################################################################################################################

class FileView(Gtk.TreeView):
    def __init__(self, dirs_model, controller):
        self.controller = controller
        self.is_playing = True
        self.dirs_model = dirs_model
        self.files_model = SampleFilesModel(dirs_model)
        Gtk.TreeView.__init__(self, self.files_model)
        self.insert_column_with_attributes(0, "Name", Gtk.CellRendererText(), text=1)
        self.set_cursor((0,))
        self.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
        self.drag_source_add_text_targets()
        self.cursor_changed_handler = self.connect('cursor-changed', self.cursor_changed)
        self.connect('drag-data-get', self.drag_data_get)
        self.connect('row-activated', self.on_row_activated)

    def stop_playing(self):
        if self.is_playing:
            self.controller.stop_preview()
            self.is_playing = False

    def start_playing(self, fn):
        self.is_playing = True
        self.controller.start_preview(fn)

    def cursor_changed(self, w):
        if self.files_model.is_refreshing:
            self.stop_playing()
            return

        c = self.get_cursor()
        if c[0] is not None:
            fn = self.files_model[c[0].get_indices()[0]][0]
            if fn.endswith("/"):
                return
            if fn != "":
                self.start_playing(fn)
            else:
                self.stop_playing(fn)
            
    def drag_data_get(self, treeview, context, selection, target_id, etime):
        cursor = treeview.get_cursor()
        if cursor is not None:
            c = cursor[0].get_indices()[0]
            fr = self.files_model[c]
            selection.set_text(str(fr[1]+"|"+fr[0]), -1)
            
    def on_row_activated(self, treeview, path, column):
        c = self.get_cursor()
        fn, label = self.files_model[c[0].get_indices()[0]]
        if fn.endswith("/"):
            self.files_model.refresh(fn)
        
####################################################################################################################################################

class EditorDialog(Gtk.Dialog):
    def __init__(self, parent):
        self.prepare_scene()
        Gtk.Dialog.__init__(self, "Drum kit editor", parent, Gtk.DialogFlags.DESTROY_WITH_PARENT, 
            ())

        self.menu_bar = Gtk.MenuBar()
        self.menu_bar.append(create_menu("_Kit", [
            ("_New", self.on_kit_new),
            ("_Open...", self.on_kit_open),
            ("_Save as...", self.on_kit_save_as),
            ("_Close", lambda w: self.response(Gtk.ResponseType.OK)),
        ]))
        self.menu_bar.append(create_menu("_Layer", [
            ("_Delete", self.on_layer_delete),
        ]))
        self.vbox.pack_start(self.menu_bar, False, False, 0)
        
        self.hbox = Gtk.HBox(spacing = 5)
        
        self.update_source = None
        self.current_pad = None
        self.dirs_model = SampleDirsModel()
        self.bank_model = BankModel()
        self.tree = FileView(self.dirs_model, self)
        self.layer_list = LayerListView(self)
        self.layer_editor = LayerEditor(self, self.bank_model)
        self.no_sfz_update = False
        
        combo = Gtk.ComboBox(model = self.dirs_model)
        cell = Gtk.CellRendererText()
        combo.pack_start(cell, True)
        combo.add_attribute(cell, 'text', 0)
        combo.connect('changed', lambda combo, tree_model, combo_model: tree_model.refresh(combo_model[combo.get_active()][1] if combo.get_active() >= 0 else None), self.tree.get_model(), combo.get_model())
        combo.set_active(0)
        sw = Gtk.ScrolledWindow()
        sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
        sw.add(self.tree)
        
        left_box = Gtk.VBox(spacing = 5)
        left_box.pack_start(combo, False, False, 0)
        left_box.pack_start(sw, True, True, 5)
        self.hbox.pack_start(left_box, True, True, 0)
        sw.set_size_request(200, -1)
        
        self.pads = PadTable(self, self.bank_model, 4, 4)
        self.hbox.pack_start(self.pads, True, True, 5)
        
        right_box = Gtk.VBox(spacing = 5)
        sw = Gtk.ScrolledWindow()
        sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
        sw.set_size_request(320, 100)
        sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        sw.add(self.layer_list)
        right_box.pack_start(sw, True, True, 0)
        sw = Gtk.ScrolledWindow()
        sw.set_size_request(320, 200)
        sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.ALWAYS)
        sw.add_with_viewport(self.layer_editor)
        right_box.pack_start(sw, True, True, 0)
        self.hbox.pack_start(right_box, True, True, 0)
        
        self.vbox.pack_start(self.hbox, False, False, 0)
        self.vbox.show_all()
        widget = self.pads.keys[36]
        widget.set_active(True)
        
        self.update_kit()
        
    def prepare_scene(self):
        found_scene = None
        for scene in cbox.Document.get_engine().status().scenes:
            scene_status = scene.status()
            layers = [layer.status().instrument_name for layer in scene_status.layers]
            if '_preview_sample' in layers and '_preview_kit' in layers:
                found_scene = scene
                break
        if found_scene is None:
            self.scene = cbox.Document.get_engine().new_scene()
            self.scene.add_new_instrument_layer("_preview_sample", "stream_player", pos = 0)
            ps = self.scene.status().instruments['_preview_sample'][1]
            ps.cmd('/output/1/gain', None, -12.0)
            self.scene.add_new_instrument_layer("_preview_kit", "sampler", pos = 1)
        else:
            self.scene = found_scene
        _, self._preview_kit = self.scene.status().instruments['_preview_kit']
        _, self._preview_sample = self.scene.status().instruments['_preview_sample']

    def update_kit(self):
        self._preview_kit.engine.load_patch_from_string(0, "", self.bank_model.to_sfz(), "Preview")
        self.update_source = None
        return False
        
    def update_kit_later(self):
        if self.update_source is not None:
            glib.source_remove(self.update_source)
        self.update_source = glib.idle_add(self.update_kit)
        
    def on_sample_dragged(self, widget):
        self.update_kit()
        if widget == self.current_pad:
            self.layer_list.set_cursor(len(self.layer_list.get_model()) - 1)
        #    self.pad_editor.refresh()
        
    def refresh_layers(self):
        try:
            self.no_sfz_update = True
            if self.current_pad is not None:
                self.layer_list.set_model(self.bank_model[self.current_pad.key])
                self.layer_list.set_cursor(0)
            else:
                self.layer_list.set_model(None)
            self.layer_editor.refresh()
        finally:
            self.no_sfz_update = False
        
    def on_pad_selected(self, widget):
        self.current_pad = widget
        self.refresh_layers()
        
    def on_layer_changed(self):
        self.layer_editor.refresh()
    
    def on_layer_delete(self, w):
        if self.layer_list.get_cursor()[0] is None:
            return None
        model = self.layer_list.get_model()
        model.remove(model.get_iter(self.layer_list.get_cursor()[0]))
        self.current_pad.update_label()
        self.layer_editor.refresh()
        self.update_kit()
        self.layer_list.set_cursor(0)

    def get_current_pad_model(self):
        return self.current_pad.get_key_model()

    def get_current_layer_model(self):
        if self.layer_list.get_cursor()[0] is None:
            return None
        return self.layer_list.get_model()[self.layer_list.get_cursor()[0]][1]

    def on_kit_new(self, widget):
        self.bank_model.from_sfz('', '')
        self.pads.refresh()
        self.refresh_layers()
        self.update_kit()

    def on_kit_open(self, widget):
        dlg = Gtk.FileChooserDialog('Open a pad bank', self, Gtk.FileChooserAction.OPEN,
            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.APPLY))
        dlg.add_filter(standard_filter(["*.sfz", "*.SFZ"], "SFZ files"))
        dlg.add_filter(standard_filter(["*"], "All files"))
        try:
            if dlg.run() == Gtk.ResponseType.APPLY:
                sfz_data = open(dlg.get_filename(), "r").read()
                self.bank_model.from_sfz(sfz_data, dlg.get_current_folder())
            self.pads.refresh()
            self.refresh_layers()
            self.update_kit()
        finally:
            dlg.destroy()

    def on_kit_save_as(self, widget):
        dlg = Gtk.FileChooserDialog('Save a pad bank', self, Gtk.FileChooserAction.SAVE, 
            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.APPLY))
        dlg.add_filter(standard_filter(["*.sfz", "*.SFZ"], "SFZ files"))
        dlg.add_filter(standard_filter(["*"], "All files"))
        try:
            if dlg.run() == Gtk.ResponseType.APPLY:
                open(dlg.get_filename(), "w").write(self.bank_model.to_sfz())
        finally:
            dlg.destroy()
            
    def start_preview(self, filename):
        self._preview_sample.engine.load(filename)
        self._preview_sample.engine.play()
    def stop_preview(self):
        self._preview_sample.engine.unload()
    def play_note(self, note, vel = 127):
        self.scene.send_midi_event(0x90, note, vel)
        self.scene.send_midi_event(0x80, note, vel)
