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

# Carla plugin host (plugin UI)
# Copyright (C) 2013-2020 Filipe Coelho <falktx@falktx.com>
#
# 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 GPL.txt file

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

from PyQt5.QtGui import QKeySequence, QMouseEvent
from PyQt5.QtWidgets import QFrame, QSplitter

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

from carla_backend_qt import CarlaHostQtPlugin
from carla_host import *
from externalui import ExternalUI

# ------------------------------------------------------------------------------------------------------------
# Host Plugin object

class PluginHost(CarlaHostQtPlugin):
    def __init__(self):
        CarlaHostQtPlugin.__init__(self)

        if False:
            # kdevelop likes this :)
            self.fExternalUI = ExternalUI()

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

        self.fExternalUI = None

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

    def setExternalUI(self, extUI):
        self.fExternalUI = extUI

    def sendMsg(self, lines):
        if self.fExternalUI is None:
            return False

        return self.fExternalUI.send(lines)

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

    def engine_init(self, driverName, clientName):
        return True

    def engine_close(self):
        return True

    def engine_idle(self):
        self.fExternalUI.idleExternalUI()

    def is_engine_running(self):
        if self.fExternalUI is None:
            return False

        return self.fExternalUI.isRunning()

    def set_engine_about_to_close(self):
        return True

    def get_host_osc_url_tcp(self):
        return self.tr("(OSC TCP port not provided in Plugin version)")

# ------------------------------------------------------------------------------------------------------------
# Main Window

class CarlaMiniW(ExternalUI, HostWindow):
    def __init__(self, host, isPatchbay, parent=None):
        ExternalUI.__init__(self)
        HostWindow.__init__(self, host, isPatchbay, parent)

        if False:
            # kdevelop likes this :)
            host = PluginHost()

        self.host = host

        host.setExternalUI(self)

        self.fFirstInit = True

        self.setWindowTitle(self.fUiName)
        self.ready()

    # Override this as it can be called from several places.
    # We really need to close all UIs as events are driven by host idle which is only available when UI is visible
    def closeExternalUI(self):
        for i in reversed(range(self.fPluginCount)):
            self.host.show_custom_ui(i, False)

        ExternalUI.closeExternalUI(self)

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

    def uiShow(self):
        if self.parent() is not None:
            return
        self.show()

    def uiFocus(self):
        if self.parent() is not None:
            return

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

        self.raise_()
        self.activateWindow()

    def uiHide(self):
        if self.parent() is not None:
            return
        self.hide()

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

        if self != gui:
            gui.close()

        # there might be other qt windows open which will block carla-plugin from quitting
        app.quit()

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

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

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

        # there might be other qt windows open which will block carla-plugin from quitting
        app.quit()

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

    def msgCallback(self, msg):
        try:
            self.msgCallback2(msg)
        except Exception as e:
            print("msgCallback error, skipped for", msg, "error was:\n", e)

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

        #if not msg:
            #return

        if msg == "runtime-info":
            values = self.readlineblock().split(":")
            load = float(values[0])
            xruns = int(values[1])
            self.host._set_runtime_info(load, xruns)

        elif msg == "project-folder":
            self.fProjectFilename = self.readlineblock()

        elif msg == "transport":
            playing = self.readlineblock_bool()
            frame, bar, beat, tick = [int(i) for i in self.readlineblock().split(":")]
            bpm = self.readlineblock_float()
            self.host._set_transport(playing, frame, bar, beat, tick, bpm)

        elif msg.startswith("PEAKS_"):
            pluginId = int(msg.replace("PEAKS_", ""))
            in1, in2, out1, out2 = [float(i) for i in self.readlineblock().split(":")]
            self.host._set_peaks(pluginId, in1, in2, out1, out2)

        elif msg.startswith("PARAMVAL_"):
            pluginId, paramId = [int(i) for i in msg.replace("PARAMVAL_", "").split(":")]
            paramValue = self.readlineblock_float()
            if paramId < 0:
                self.host._set_internalValue(pluginId, paramId, paramValue)
            else:
                self.host._set_parameterValue(pluginId, paramId, paramValue)

        elif msg.startswith("ENGINE_CALLBACK_"):
            action   = int(msg.replace("ENGINE_CALLBACK_", ""))
            pluginId = self.readlineblock_int()
            value1   = self.readlineblock_int()
            value2   = self.readlineblock_int()
            value3   = self.readlineblock_int()
            valuef   = self.readlineblock_float()
            valueStr = self.readlineblock()

            self.host._setViaCallback(action, pluginId, value1, value2, value3, valuef, valueStr)
            engineCallback(self.host, action, pluginId, value1, value2, value3, valuef, valueStr)

        elif msg.startswith("ENGINE_OPTION_"):
            option = int(msg.replace("ENGINE_OPTION_", ""))
            forced = self.readlineblock_bool()
            value  = self.readlineblock()

            if self.fFirstInit and not forced:
                return

            if option == ENGINE_OPTION_PROCESS_MODE:
                self.host.processMode = int(value)
            elif option == ENGINE_OPTION_TRANSPORT_MODE:
                self.host.transportMode = int(value)
            elif option == ENGINE_OPTION_FORCE_STEREO:
                self.host.forceStereo = bool(value == "true")
            elif option == ENGINE_OPTION_PREFER_PLUGIN_BRIDGES:
                self.host.preferPluginBridges = bool(value == "true")
            elif option == ENGINE_OPTION_PREFER_UI_BRIDGES:
                self.host.preferUIBridges = bool(value == "true")
            elif option == ENGINE_OPTION_UIS_ALWAYS_ON_TOP:
                self.host.uisAlwaysOnTop = bool(value == "true")
            elif option == ENGINE_OPTION_MAX_PARAMETERS:
                self.host.maxParameters = int(value)
            elif option == ENGINE_OPTION_UI_BRIDGES_TIMEOUT:
                self.host.uiBridgesTimeout = int(value)
            elif option == ENGINE_OPTION_PATH_BINARIES:
                self.host.pathBinaries = value
            elif option == ENGINE_OPTION_PATH_RESOURCES:
                self.host.pathResources = value

        elif msg.startswith("PLUGIN_INFO_"):
            pluginId = int(msg.replace("PLUGIN_INFO_", ""))
            self.host._add(pluginId)

            type_, category, hints, uniqueId, optsAvail, optsEnabled = [int(i) for i in self.readlineblock().split(":")]
            filename  = self.readlineblock()
            name      = self.readlineblock()
            iconName  = self.readlineblock()
            realName  = self.readlineblock()
            label     = self.readlineblock()
            maker     = self.readlineblock()
            copyright = self.readlineblock()

            pinfo = {
                'type': type_,
                'category': category,
                'hints': hints,
                'optionsAvailable': optsAvail,
                'optionsEnabled': optsEnabled,
                'filename': filename,
                'name':  name,
                'label': label,
                'maker': maker,
                'copyright': copyright,
                'iconName': iconName,
                'patchbayClientId': 0,
                'uniqueId': uniqueId
            }
            self.host._set_pluginInfo(pluginId, pinfo)
            self.host._set_pluginRealName(pluginId, realName)

        elif msg.startswith("AUDIO_COUNT_"):
            pluginId, ins, outs = [int(i) for i in msg.replace("AUDIO_COUNT_", "").split(":")]
            self.host._set_audioCountInfo(pluginId, {'ins': ins, 'outs': outs})

        elif msg.startswith("MIDI_COUNT_"):
            pluginId, ins, outs = [int(i) for i in msg.replace("MIDI_COUNT_", "").split(":")]
            self.host._set_midiCountInfo(pluginId, {'ins': ins, 'outs': outs})

        elif msg.startswith("PARAMETER_COUNT_"):
            pluginId, ins, outs, count = [int(i) for i in msg.replace("PARAMETER_COUNT_", "").split(":")]
            self.host._set_parameterCountInfo(pluginId, count, {'ins': ins, 'outs': outs})

        elif msg.startswith("PARAMETER_DATA_"):
            pluginId, paramId = [int(i) for i in msg.replace("PARAMETER_DATA_", "").split(":")]
            paramType, paramHints, mappedControlIndex, midiChannel = [int(i) for i in self.readlineblock().split(":")]
            mappedMinimum, mappedMaximum = [float(i) for i in self.readlineblock().split(":")]
            paramName = self.readlineblock()
            paramUnit = self.readlineblock()
            paramComment = self.readlineblock()
            paramGroupName = self.readlineblock()

            paramInfo = {
                'name': paramName,
                'symbol': "",
                'unit': paramUnit,
                'comment': paramComment,
                'groupName': paramGroupName,
                'scalePointCount': 0,
            }
            self.host._set_parameterInfo(pluginId, paramId, paramInfo)

            paramData = {
                'type': paramType,
                'hints': paramHints,
                'index': paramId,
                'rindex': -1,
                'midiChannel': midiChannel,
                'mappedControlIndex': mappedControlIndex,
                'mappedMinimum': mappedMinimum,
                'mappedMaximum': mappedMaximum,
            }
            self.host._set_parameterData(pluginId, paramId, paramData)

        elif msg.startswith("PARAMETER_RANGES_"):
            pluginId, paramId = [int(i) for i in msg.replace("PARAMETER_RANGES_", "").split(":")]
            def_, min_, max_, step, stepSmall, stepLarge = [float(i) for i in self.readlineblock().split(":")]

            paramRanges = {
                'def': def_,
                'min': min_,
                'max': max_,
                'step': step,
                'stepSmall': stepSmall,
                'stepLarge': stepLarge
            }
            self.host._set_parameterRanges(pluginId, paramId, paramRanges)

        elif msg.startswith("PROGRAM_COUNT_"):
            pluginId, count, current = [int(i) for i in msg.replace("PROGRAM_COUNT_", "").split(":")]
            self.host._set_programCount(pluginId, count)
            self.host._set_currentProgram(pluginId, current)

        elif msg.startswith("PROGRAM_NAME_"):
            pluginId, progId = [int(i) for i in msg.replace("PROGRAM_NAME_", "").split(":")]
            progName = self.readlineblock()
            self.host._set_programName(pluginId, progId, progName)

        elif msg.startswith("MIDI_PROGRAM_COUNT_"):
            pluginId, count, current = [int(i) for i in msg.replace("MIDI_PROGRAM_COUNT_", "").split(":")]
            self.host._set_midiProgramCount(pluginId, count)
            self.host._set_currentMidiProgram(pluginId, current)

        elif msg.startswith("MIDI_PROGRAM_DATA_"):
            pluginId, midiProgId = [int(i) for i in msg.replace("MIDI_PROGRAM_DATA_", "").split(":")]
            bank, program = [int(i) for i in self.readlineblock().split(":")]
            name = self.readlineblock()
            self.host._set_midiProgramData(pluginId, midiProgId, {'bank': bank, 'program': program, 'name': name})

        elif msg.startswith("CUSTOM_DATA_COUNT_"):
            pluginId, count = [int(i) for i in msg.replace("CUSTOM_DATA_COUNT_", "").split(":")]
            self.host._set_customDataCount(pluginId, count)

        elif msg.startswith("CUSTOM_DATA_"):
            pluginId, customDataId = [int(i) for i in msg.replace("CUSTOM_DATA_", "").split(":")]

            type_ = self.readlineblock()
            key   = self.readlineblock()
            value = self.readlineblock()
            self.host._set_customData(pluginId, customDataId, {'type': type_, 'key': key, 'value': value})

        elif msg == "osc-urls":
            tcp = self.readlineblock()
            udp = self.readlineblock()
            self.host.fOscTCP = tcp
            self.host.fOscUDP = udp

        elif msg == "max-plugin-number":
            maxnum = self.readlineblock_int()
            self.host.fMaxPluginNumber = maxnum

        elif msg == "buffer-size":
            bufsize = self.readlineblock_int()
            self.host.fBufferSize = bufsize

        elif msg == "sample-rate":
            srate = self.readlineblock_float()
            self.host.fSampleRate = srate

        elif msg == "error":
            error = self.readlineblock()
            engineCallback(self.host, ENGINE_CALLBACK_ERROR, 0, 0, 0, 0, 0.0, error)

        elif msg == "show":
            self.fFirstInit = False
            self.uiShow()

        elif msg == "focus":
            self.uiFocus()

        elif msg == "hide":
            self.uiHide()

        elif msg == "quit":
            self.fQuitReceived = True
            self.uiQuit()

        elif msg == "uiTitle":
            uiTitle = self.readlineblock()
            self.uiTitleChanged(uiTitle)

        else:
            print("unknown message: \"" + msg + "\"")

# ------------------------------------------------------------------------------------------------------------
# Embed Widget

class QEmbedWidget(QWidget):
    def __init__(self, winId):
        QWidget.__init__(self)
        self.setAttribute(Qt.WA_LayoutUsesWidgetRect)
        self.move(0, 0)

        self.fPos = (0, 0)
        self.fWinId = 0

    def finalSetup(self, gui, winId):
        self.fWinId = int(self.winId())
        gui.ui.centralwidget.installEventFilter(self)
        gui.ui.menubar.installEventFilter(self)
        gCarla.utils.x11_reparent_window(self.fWinId, winId)
        self.show()

    def fixPosition(self):
        pos = gCarla.utils.x11_get_window_pos(self.fWinId)
        if self.fPos == pos:
            return
        self.fPos = pos
        self.move(pos[0], pos[1])
        gCarla.utils.x11_move_window(self.fWinId, pos[2], pos[3])

    def eventFilter(self, obj, ev):
        if isinstance(ev, QMouseEvent):
            self.fixPosition()
        return False

    def enterEvent(self, ev):
        self.fixPosition()
        QWidget.enterEvent(self, ev)

# ------------------------------------------------------------------------------------------------------------
# Embed plugin UI

class CarlaEmbedW(QEmbedWidget):
    def __init__(self, host, winId, isPatchbay):
        QEmbedWidget.__init__(self, winId)

        if False:
            host = CarlaHostPlugin()

        self.host = host
        self.fWinId = winId
        self.setFixedSize(1024, 712)

        self.fLayout = QVBoxLayout(self)
        self.fLayout.setContentsMargins(0, 0, 0, 0)
        self.fLayout.setSpacing(0)
        self.setLayout(self.fLayout)

        self.gui = CarlaMiniW(host, isPatchbay, self)
        self.gui.hide()

        self.gui.ui.act_file_quit.setEnabled(False)
        self.gui.ui.act_file_quit.setVisible(False)

        self.fShortcutActions = []
        self.addShortcutActions(self.gui.ui.menu_File.actions())
        self.addShortcutActions(self.gui.ui.menu_Plugin.actions())
        self.addShortcutActions(self.gui.ui.menu_PluginMacros.actions())
        self.addShortcutActions(self.gui.ui.menu_Settings.actions())
        self.addShortcutActions(self.gui.ui.menu_Help.actions())

        if self.host.processMode == ENGINE_PROCESS_MODE_PATCHBAY:
            self.addShortcutActions(self.gui.ui.menu_Canvas.actions())
            self.addShortcutActions(self.gui.ui.menu_Canvas_Zoom.actions())

        self.addWidget(self.gui.ui.menubar)
        self.addLine()
        self.addWidget(self.gui.ui.toolBar)

        if self.host.processMode == ENGINE_PROCESS_MODE_PATCHBAY:
            self.addLine()

        self.fCentralSplitter = QSplitter(self)
        policy = self.fCentralSplitter.sizePolicy()
        policy.setVerticalStretch(1)
        self.fCentralSplitter.setSizePolicy(policy)

        self.addCentralWidget(self.gui.ui.dockWidget)
        self.addCentralWidget(self.gui.centralWidget())
        self.fLayout.addWidget(self.fCentralSplitter)

        self.finalSetup(self.gui, winId)

    def addShortcutActions(self, actions):
        for action in actions:
            if not action.shortcut().isEmpty():
                self.fShortcutActions.append(action)

    def addWidget(self, widget):
        widget.setParent(self)
        self.fLayout.addWidget(widget)

    def addCentralWidget(self, widget):
        widget.setParent(self)
        self.fCentralSplitter.addWidget(widget)

    def addLine(self):
        line = QFrame(self)
        line.setFrameShadow(QFrame.Sunken)
        line.setFrameShape(QFrame.HLine)
        line.setLineWidth(0)
        line.setMidLineWidth(1)
        self.fLayout.addWidget(line)

    def keyPressEvent(self, event):
        modifiers    = event.modifiers()
        modifiersStr = ""

        if modifiers & Qt.ShiftModifier:
            modifiersStr += "Shift+"
        if modifiers & Qt.ControlModifier:
            modifiersStr += "Ctrl+"
        if modifiers & Qt.AltModifier:
            modifiersStr += "Alt+"
        if modifiers & Qt.MetaModifier:
            modifiersStr += "Meta+"

        keyStr = QKeySequence(event.key()).toString()
        keySeq = QKeySequence(modifiersStr + keyStr)

        for action in self.fShortcutActions:
            if not action.isEnabled():
                continue
            if keySeq.matches(action.shortcut()) != QKeySequence.ExactMatch:
                continue
            event.accept()
            action.trigger()
            return

        QEmbedWidget.keyPressEvent(self, event)

    def showEvent(self, event):
        QEmbedWidget.showEvent(self, event)

        if QT_VERSION >= 0x50600:
            self.host.set_engine_option(ENGINE_OPTION_FRONTEND_UI_SCALE, int(self.devicePixelRatioF() * 1000), "")
            print("Plugin UI pixel ratio is", self.devicePixelRatioF(),
                  "with %ix%i" % (self.width(), self.height()), "in size")

        # set our gui as parent for all plugins UIs
        if self.host.manageUIs:
            if MACOS:
                nsViewPtr = int(self.fWinId)
                winIdStr  = "%x" % gCarla.utils.cocoa_get_window(nsViewPtr)
            else:
                winIdStr = "%x" % int(self.fWinId)
            self.host.set_engine_option(ENGINE_OPTION_FRONTEND_WIN_ID, 0, winIdStr)

    def hideEvent(self, event):
        # disable parent
        self.host.set_engine_option(ENGINE_OPTION_FRONTEND_WIN_ID, 0, "0")

        QEmbedWidget.hideEvent(self, event)

    def closeEvent(self, event):
        self.gui.close()
        self.gui.closeExternalUI()
        QEmbedWidget.closeEvent(self, event)

        # there might be other qt windows open which will block carla-plugin from quitting
        app.quit()

    def setLoadRDFsNeeded(self):
        self.gui.setLoadRDFsNeeded()

# ------------------------------------------------------------------------------------------------------------
# Main

if __name__ == '__main__':
    # -------------------------------------------------------------
    # Get details regarding target usage

    try:
        winId = int(os.getenv("CARLA_PLUGIN_EMBED_WINID"))
    except:
        winId = 0

    usingEmbed = bool(LINUX and winId != 0)

    # -------------------------------------------------------------
    # Init host backend (part 1)

    isPatchbay = sys.argv[0].rsplit(os.path.sep)[-1].lower().replace(".exe","") == "carla-plugin-patchbay"

    host = initHost("Carla-Plugin", None, False, True, True, PluginHost)
    host.processMode       = ENGINE_PROCESS_MODE_PATCHBAY if isPatchbay else ENGINE_PROCESS_MODE_CONTINUOUS_RACK
    host.processModeForced = True
    host.nextProcessMode   = host.processMode

    # -------------------------------------------------------------
    # Set-up environment

    gCarla.utils.setenv("CARLA_PLUGIN_EMBED_WINID", "0")

    if usingEmbed:
        gCarla.utils.setenv("QT_QPA_PLATFORM", "xcb")

    # -------------------------------------------------------------
    # App initialization

    app = CarlaApplication("Carla2-Plugin")

    # -------------------------------------------------------------
    # Set-up custom signal handling

    setUpSignals()

    # -------------------------------------------------------------
    # Init host backend (part 2)

    loadHostSettings(host)

    # -------------------------------------------------------------
    # Create GUI

    if usingEmbed:
        gui = CarlaEmbedW(host, winId, isPatchbay)
    else:
        gui = CarlaMiniW(host, isPatchbay)

    # -------------------------------------------------------------
    # App-Loop

    app.exit_exec()
