#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# A piano roll viewer/editor
# Copyright (C) 2012-2015 Filipe Coelho <falktx@falktx.com>
# Copyright (C) 2014-2015 Perry Nguyen
#
# 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 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.
#
# For a full copy of the GNU General Public License see the doc/GPL.txt file.

# ------------------------------------------------------------------------------------------------------------
# Imports (Config)

from carla_config import *

# ------------------------------------------------------------------------------------------------------------
# Imports (Global)

if config_UseQt5:
    from PyQt5.QtCore import pyqtSlot, Qt, QEvent
    from PyQt5.QtGui import QKeyEvent
    from PyQt5.QtWidgets import QMainWindow
else:
    from PyQt4.QtCore import pyqtSlot, Qt, QEvent
    from PyQt4.QtGui import QKeyEvent, QMainWindow

# ------------------------------------------------------------------------------------------------------------
# Imports (Custom)

from carla_shared import *
from carla_utils import *
from pianoroll import *

import ui_midipattern

# ------------------------------------------------------------------------------------------------------------
# Imports (ExternalUI)

from carla_app import CarlaApplication
from externalui import ExternalUI

# ------------------------------------------------------------------------------------------------------------

class MidiPatternW(ExternalUI, QMainWindow):
    PPQ = 48.0

    def __init__(self):
        ExternalUI.__init__(self)
        QMainWindow.__init__(self)
        self.ui = ui_midipattern.Ui_MidiPatternW()
        self.ui.setupUi(self)
        self.ui.piano = self.ui.graphicsView.piano

        # to be filled with note-on events, while waiting for their matching note-off
        self.fPendingNoteOns = [] # (channel, note, velocity, time)

        self.fTransportInfo = {
            "playing": False,
            "frame": 0,
            "bar": 0,
            "beat": 0,
            "tick": 0,
            "bpm": 120.0,
            "sigNum": 4.0,
            "sigDenom": 4.0
        }

        self.ui.act_edit_insert.triggered.connect(self.slot_editInsertMode)
        self.ui.act_edit_velocity.triggered.connect(self.slot_editVelocityMode)
        self.ui.act_edit_select_all.triggered.connect(self.slot_editSelectAll)

        self.ui.piano.midievent.connect(self.sendMsg)
        self.ui.piano.measureupdate.connect(self.updateMeasureBox)
        self.ui.piano.modeupdate.connect(self.ui.modeIndicator.changeMode)
        self.ui.piano.modeupdate.connect(self.slot_modeChanged)

        self.ui.timeSigBox.currentIndexChanged[int].connect(self.slot_paramChanged)
        self.ui.measureBox.currentIndexChanged[int].connect(self.slot_paramChanged)
        self.ui.defaultLengthBox.currentIndexChanged[int].connect(self.slot_paramChanged)
        self.ui.quantizeBox.currentIndexChanged[int].connect(self.slot_paramChanged)

        self.ui.timeSigBox.currentIndexChanged[str].connect(self.ui.piano.setTimeSig)
        self.ui.measureBox.currentIndexChanged[str].connect(self.ui.piano.setMeasures)
        self.ui.defaultLengthBox.currentIndexChanged[str].connect(self.ui.piano.setDefaultLength)
        self.ui.quantizeBox.currentIndexChanged[str].connect(self.ui.piano.setGridDiv)
        self.ui.hSlider.valueChanged.connect(self.ui.graphicsView.setZoomX)
        self.ui.vSlider.valueChanged.connect(self.ui.graphicsView.setZoomY)

        self.ui.graphicsView.setFocus()

        self.fIdleTimer = self.startTimer(30)
        self.setWindowTitle(self.fUiName)
        self.ready()

    def slot_editInsertMode(self):
        ev = QKeyEvent(QEvent.User, Qt.Key_F, Qt.NoModifier)
        self.ui.piano.keyPressEvent(ev)

    def slot_editVelocityMode(self):
        ev = QKeyEvent(QEvent.User, Qt.Key_D, Qt.NoModifier)
        self.ui.piano.keyPressEvent(ev)

    def slot_editSelectAll(self):
        ev = QKeyEvent(QEvent.User, Qt.Key_A, Qt.NoModifier)
        self.ui.piano.keyPressEvent(ev)

    def slot_modeChanged(self, mode):
        if mode == "insert_mode":
            self.ui.act_edit_insert.setChecked(True)
            self.ui.act_edit_velocity.setChecked(False)
        elif mode == "velocity_mode":
            self.ui.act_edit_insert.setChecked(False)
            self.ui.act_edit_velocity.setChecked(True)
        else:
            self.ui.act_edit_insert.setChecked(False)
            self.ui.act_edit_velocity.setChecked(False)

    def slot_paramChanged(self, index):
        sender = self.sender()

        if sender == self.ui.timeSigBox:
            param = 0
        elif sender == self.ui.measureBox:
            param = 1
            index += 1
        elif sender == self.ui.defaultLengthBox:
            param = 2
        elif sender == self.ui.quantizeBox:
            param = 3
        else:
            return

        self.sendControl(param, index)

    # -------------------------------------------------------------------
    # DSP Callbacks

    def dspParameterChanged(self, index, value):
        value = int(value)

        if index == 0: # TimeSig
            self.ui.timeSigBox.setCurrentIndex(value)
        elif index == 1: # Measures
            self.ui.measureBox.setCurrentIndex(value-1)
        elif index == 2: # DefLength
            self.ui.defaultLengthBox.setCurrentIndex(value)
        elif index == 3: # Quantize
            self.ui.quantizeBox.setCurrentIndex(value)

    def dspStateChanged(self, key, value):
        pass

    # -------------------------------------------------------------------
    # ExternalUI Callbacks

    def uiShow(self):
        self.show()

    def uiFocus(self):
        self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
        self.show()

        self.raise_()
        self.activateWindow()

    def uiHide(self):
        self.hide()

    def uiQuit(self):
        self.closeExternalUI()
        self.close()
        app.quit()

    def uiTitleChanged(self, uiTitle):
        self.setWindowTitle(uiTitle)

    # -------------------------------------------------------------------
    # Qt events

    def timerEvent(self, event):
        if event.timerId() == self.fIdleTimer:
            self.idleExternalUI()
        QMainWindow.timerEvent(self, event)

    def closeEvent(self, event):
        self.closeExternalUI()
        QMainWindow.closeEvent(self, event)

        # there might be other qt windows open which will block the UI from quitting
        app.quit()

    # -------------------------------------------------------------------
    # Custom callback

    def updateMeasureBox(self, index):
        self.measureBox.setCurrentIndex(index-1)

    def sendMsg(self, data):
        msg = data[0]
        if msg == "midievent-remove":
            note, start, length, vel = data[1:5]
            note_start = start * 60. / self.fTransportInfo["bpm"] * 4. / self.fTransportInfo["sigDenom"] * self.PPQ
            note_stop = note_start + length * 60. / self.fTransportInfo["bpm"] * 4. * self.fTransportInfo["sigNum"] / self.fTransportInfo["sigDenom"] * self.PPQ
            self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel])
            self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel])

        elif msg == "midievent-add":
            note, start, length, vel = data[1:5]
            note_start = start * 60. / self.fTransportInfo["bpm"] * self.PPQ
            note_stop = note_start + length * 60. / self.fTransportInfo["bpm"] * 4. * self.fTransportInfo["sigNum"] / self.fTransportInfo["sigDenom"] * self.PPQ
            self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel])
            self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel])

    def msgCallback(self, msg):
        msg = charPtrToString(msg)

        if msg == "midi-clear-all":
            # clear all notes
            self.ui.piano.clearNotes()

        elif msg == "midievent-add":
            # adds single midi event
            time = int(self.readlineblock())
            size = int(self.readlineblock())
            data = []

            for x in range(size):
                data.append(int(self.readlineblock()))

            self.handleMidiEvent(time, size, data)

        elif msg == "transport":
            playing = bool(self.readlineblock() == "true")
            frame, bar, beat, tick = [int(i) for i in self.readlineblock().split(":")]
            bpm, sigNum, sigDenom = [float(i) for i in self.readlineblock().split(":")]

            if beat != self.fTransportInfo["beat"]:
                print(beat)

            old_frame = self.fTransportInfo['frame']

            self.fTransportInfo = {
                "playing": playing,
                "frame": frame,
                "bar": bar,
                "beat": beat,
                "tick": tick,
                "bpm": bpm,
                "sigNum": sigNum,
                "sigDenom": sigDenom
            }

            if old_frame != frame:
                self.ui.piano.movePlayHead(self.fTransportInfo)

        else:
            ExternalUI.msgCallback(self, msg)

    # -------------------------------------------------------------------
    # Internal stuff

    def handleMidiEvent(self, time, size, data):
        #print("Got MIDI Event on UI", time, size, data)

        # NOTE: for now time comes in frames, which might not be desirable
        #       we'll convert it to a smaller value for now (seconds)
        #       later on we can have time as PPQ or similar

        time /= self.PPQ

        status  = MIDI_GET_STATUS_FROM_DATA(data)
        channel = MIDI_GET_CHANNEL_FROM_DATA(data)
        if status == MIDI_STATUS_NOTE_ON:
            note = data[1]
            velo = data[2]

            # append (channel, note, velo, time) for later
            self.fPendingNoteOns.append((channel, note, velo, time))

        elif status == MIDI_STATUS_NOTE_OFF:
            note = data[1]
            velo = data[2]

            # find previous note-on that matches this note and channel
            for noteOnMsg in self.fPendingNoteOns:
                channel_, note_, velo_, time_ = noteOnMsg

                if channel_ != channel:
                    continue
                if note_ != note:
                    continue

                # found it
                #print("{} {} {} {}\n".format(note, time_, time-time_, velo_))
                start = time_ / 60. * self.fTransportInfo["bpm"] / 4. * self.fTransportInfo["sigDenom"]
                length = (time - time_) / 60. * self.fTransportInfo["bpm"] / 4. / self.fTransportInfo["sigNum"] * self.fTransportInfo["sigDenom"]
                self.ui.piano.drawNote(note, start, length, velo_)

                # remove from list
                self.fPendingNoteOns.remove(noteOnMsg)
                break

#--------------- main ------------------
if __name__ == '__main__':
    import resources_rc

    pathBinaries, pathResources = getPaths()
    gCarla.utils = CarlaUtils(os.path.join(pathBinaries, "libcarla_utils." + DLL_EXTENSION))
    gCarla.utils.set_process_name("MidiPattern")

    app = CarlaApplication("MidiPattern")
    gui = MidiPatternW()
    app.exit_exec()
