#!/usr/bin/env python3 

import locale
import argparse
import sys
import time

from PyQt5.QtCore import Qt, QVariant, QTimer
from PyQt5.QtWidgets import QApplication, QPushButton, QWidget, QGroupBox, QLabel, QDialog, QSlider, QVBoxLayout, QHBoxLayout, QProxyStyle, QStyle, QMainWindow, QMenu, QAction, QFileDialog, QComboBox

from blc.workspace import Workspace, CHASER, SHOW, QLC_INFTY
from blc.audio import DefaultAudioPlayer, AudioPlayer, FFPlayer
from blc.output import LightingOutput
from blc.ola import OLAOutput

class DirectSliderStyle(QProxyStyle):
    def styleHint(self, hint, option=0, widget=0, returnData=0):
        if hint == QStyle.SH_Slider_AbsoluteSetButtons:
            return (Qt.LeftButton | Qt.MidButton | Qt.RightButton)
        return super().styleHint(hint, option, widget, returnData)

class QFader(QGroupBox):
    """Class for creating a single fader. 

    Allows for the value to be set by the application and overriden by the user. 

    @param title the title for the fader 
    @param parent the parent widget
    """
    @property
    def sliderValue(self):
        """Return the fader's current value."""
        return self.slider.value()
    
    @sliderValue.setter 
    def sliderValue(self, value):
        self._sliderValue = value
        if not self.held:
            self.slider.setValue(value)

    def _sliderPressed(self):
        if not self.held:
            self.held = True
            self.button.setDisabled(False)
        self._sliderMoved(self.slider.value())

    def _releaseHold(self):
        self.held = False
        self.button.setDisabled(True)
        self.slider.setValue(self._sliderValue)
        self._sliderMoved(self._sliderValue)

    def _sliderMoved(self, value):
        if self.on_update is not None:
            self.on_update(self.channel, value)

    def __init__(self, title, channel=None, on_update=None, parent=None):
        super().__init__(title=title, parent=parent)
        self.slider = QSlider(Qt.Vertical, self)
        self.slider.setStyle(DirectSliderStyle(self.slider.style()))
        self.slider.setMaximum(255)
        self.slider.setMinimum(0)
        self.slider.setMinimumHeight(200)

        self.channel = channel
        self.on_update = on_update

        self.slider.sliderMoved.connect(self._sliderMoved)

        self._sliderValue = 0
        self.held = False

        self.label = QLabel(self)
        self.label.setText('0')
        self.label.setAlignment(Qt.AlignCenter)
        self.slider.valueChanged.connect(lambda a: self.label.setText(str(a)))
        self.slider.sliderPressed.connect(self._sliderPressed)

        self.button = QPushButton("", self)
        self.button.setDisabled(True)
        self.button.clicked.connect(self._releaseHold)

        layout = QVBoxLayout()
        layout.setAlignment(Qt.AlignCenter)
        layout.addWidget(self.slider)
        layout.addWidget(self.label)
        layout.addWidget(self.button)
        self.setLayout(layout)

        self.setFixedWidth(45)

class QFaderBank(QGroupBox):
    """Class for creating a bank of faders. 

    @param title the title of the fader bank 
    @param faders a list of (address, title) elements, where address is unique 
    @param parent the parent widget 
    """
    def set_values(self, values):
        values = tuple(values)
        for c, v in values:
            ua = (*c,)
            if ua not in self.sliders:
                continue
            self.sliders[ua].sliderValue = v
        if self.output is not None:
            self.output.set_values(values)

    def slider_changed(self, channel, value):
        if self.output is not None:
            self.output.set_values(((channel, value),))

    def __init__(self, title="Faders", faders=(), output=None, parent=None):
        super().__init__(title=title, parent=parent)

        layout = QHBoxLayout()
        layout.setSpacing(0)

        self.sliders = {}
        for a, t in faders:
            ua = (*a,)
            slider = QFader(t, channel=ua, on_update=self.slider_changed, parent=self)
            layout.addWidget(slider)
            self.sliders[ua] = slider

        self.setLayout(layout)

        self.output = output

class QtRenderer:
    def _start(self):
        """Start the function."""
        if self.running:
            raise ValueError("Already running")
        self.nx = 0
        self.tnx = 0
        self.data = None
        self.time_elapsed = 0
        self.last_rendered = 0
        self.last_time = None
        self.running = True
        self.aplayers = set()
        self.audio_players = []

    def play(self):
        if not self.running:
            self._start()
        else:
            self.last_time = time.monotonic()
            for ap in self.audio_players:
                ap.play()
        self.timer.start(self.minnx)

    def _prune_ap(self):
        nap = []
        for i in self.audio_players:
            if not i.playing:
                i.terminate()
            else:
                nap.append(i)
        self.audio_players = nap

    def pause(self):
        self.timer.stop()
        self._prune_ap()
        for ap in self.audio_players:
            ap.pause()

    def stop(self):
        self.timer.stop()
        self.running = False
        self._prune_ap()
        for ap in self.audio_players:
            ap.stop()

    def _do_render(self):
        t = int(1000*self.time_elapsed+1) + self.minnx

        vals, acues, self.nx, self.data = self.f.render(t, data=self.data)
        self.nx = max(self.minnx, self.nx)
        self.tnx = self.nx/1000

        for c, v in vals:
            self.values[c] = v
        for st, aid, fname, *_ in acues:
            if aid not in self.aplayers:
                self.aplayers.add(aid)
                ap = self.ap(fname)
                ap.volume = 100
                self.audio_players.append(ap)
                self.anext.append((st, ap))

        self.last_rendered = self.time_elapsed

    def render_step(self):
        """Output the current step and render the next one."""
        if self.last_time is None:
            self._do_render()
            self.nx = 0
            self.tnx = 0
            self.last_time = time.monotonic()
        else:
            t = time.monotonic()
            self.time_elapsed += t - self.last_time
            self.last_time = t
        self.timestep(self.time_elapsed)

        self.lo.set_values(tuple(self.values.items()))
        for st, ap in self.anext:
            ap.play(max(int(self.time_elapsed*1000+1)-st, 0))
        self.anext = []

        self._do_render()

    def advance(self):
        """Advance the function, if possible.

        It is not an error to call this function when dealing with non-Advanceable toplevel 
        functions; this will just do nothing.
        """
        ## TODO: Make this work when not stalled
        if not self.running:
            raise ValueError("Cannot advance a function that has not been started!")

        t = 1000*self.time_elapsed
        self.data = self.f.advance(t, self.data)
        *_, self.data = self.f.render(t, data=self.data)
        self.nx = self.minnx

    def terminate(self):
        for i in self.audio_players:
            i.terminate()

    def __init__(self, f, timer: QTimer, timestep, lo:LightingOutput, 
                 ap: AudioPlayer=DefaultAudioPlayer, minnx=20):
        if f.type not in (SHOW, CHASER):
            raise ValueError("Only Shows and Chasers may be used as toplevel functions")

        self.time_elapsed = 0
        self.last_rendered = 0
        self.last_time = 0
        self.f = f
        self.minnx = minnx
        self.nx = 0
        self.data = None
        self.values = {c: 0 for c in self.f.scope}
        self.lo = lo
        self.ap = ap
        self.aplayers = set()
        self.audio_players = []
        self.anext = []
        self.timestep = timestep
        self.timer = timer
        self.running = False
        self.tnx = 0
        self.tminnx = self.minnx/1000
        
        self.timer.timeout.connect(self.render_step)

class BLCQtMain(QMainWindow):
    def blackout(self):
        self.faders.set_values(((i, 0) for i in self.channels))

    def ask_load_workspace(self):
        fname, _ = QFileDialog.getOpenFileName(self, "Open File", "", "QLC Workspaces (*.qxw)")
        if not fname:
            return
        self.load_workspace(fname)

    def load_workspace(self, fname):
        ## Load the workspace
        self.workspace = Workspace.load(fname)

        ## Iterate over all fixtures and set them to 0
        self.channels = sum((tuple(f.channels) for f in self.workspace.fixtures.values()), ())
        self.blackout()

        ## Update the list of functions
        functions = sorted((f for f in self.workspace.functions.values() if f.type == SHOW or (f.type == CHASER and f.is_chaser) ),
                           key=lambda f: (f.type, f.name, f.id))
        self.function_select.clear()
        for n, f in enumerate(functions):
            self.function_select.insertItem(n, ("[C] " if f.type == CHASER else "[S] ")+f.name, QVariant((f.id)))
        if functions:
            self._choose_function(0)

    def _choose_function(self, index: int):
        fid = self.function_select.currentData()
        if self.function is not None and self.function.id == fid:
            return

        self.function = self.workspace.functions[fid]
        self.timer = QTimer()
        if self.renderer is not None:
            self.renderer.terminate()
        self.renderer = QtRenderer(self.function, self.timer, self._update_time, self.faders)
        self.func_label.setText("Current Function: None")
        self._set_controls(True, False, False, False)

    def _update_time(self, t: float):
        if 1000*t > self.function.actual_duration:
            self._stop()
        elif self.function.type == CHASER and self.renderer.data is not None:
            cs = self.renderer.data.current_step
            self.func_label.setText("Current Function: "+self.function.steps[cs].function.name+(" (%d)" % (cs+1)))
        self.time_label.setText("Time: %.1fs" % t)

    def _set_controls(self, play, pause, stop, advance):
        self.play_button.setEnabled(play)
        self.pause_button.setEnabled(pause)
        self.stop_button.setEnabled(stop)
        self.advance_button.setEnabled(advance)

    def _play(self):
        self._set_controls(False, True, True, True)
        if self.function.type == SHOW:
            self.func_label.setText("Current Function: Show")
        self.function_select.setEnabled(False)
        self.renderer.play()

    def _stop(self):
        self.renderer.stop()
        self._set_controls(True, False, False, False)
        self.function_select.setEnabled(True)
        self._update_time(0)
        self.func_label.setText("Current Function: None")
        self.blackout()

    def _pause(self):
        self.renderer.pause()
        self._set_controls(True, False, True, False)

    def _advance(self):
        self.renderer.advance()

    def terminate(self):
        if self.renderer is not None:
            self.renderer.terminate()

    def __init__(self):
        super().__init__()

        self.root = QWidget(self)
        self.setWindowTitle("BLC-Qt")

        self.menu = self.menuBar()
        self.fileMenu = self.menu.addMenu("File")
        
        loadw = QAction("Load Workspace", self.root)
        loadw.triggered.connect(self.ask_load_workspace)
        self.fileMenu.addAction(loadw)

        self.faders = QFaderBank(title="Channels", faders=(((0, i), str(i)) for i in range(1,37)),
                                 parent=self.root)

        function_box = QGroupBox(self, title="Functions")
        self.function_select = QComboBox(function_box)
        self.function_select.activated.connect(self._choose_function)

        self.time_label = QLabel(function_box)
        self.time_label.setText("Time: 0.0s")
        self.func_label = QLabel(function_box)
        self.func_label.setText("Current Function: None")
        self.play_button = QPushButton("Play", function_box)
        self.play_button.clicked.connect(self._play)
        self.pause_button = QPushButton("Pause", function_box)
        self.pause_button.clicked.connect(self._pause)
        self.stop_button = QPushButton("Stop", function_box)
        self.stop_button.clicked.connect(self._stop)
        self.advance_button = QPushButton("Advance", function_box)
        self.advance_button.clicked.connect(self._advance)
        self._set_controls(False, False, False, False)

        fblayout = QVBoxLayout()
        fblayout.addWidget(self.function_select)
        fblayout.addWidget(self.func_label)
        fblayout.addWidget(self.play_button)
        fblayout.addWidget(self.stop_button)
        fblayout.addWidget(self.pause_button)
        fblayout.addWidget(self.advance_button)
        fblayout.addWidget(self.time_label)
        function_box.setLayout(fblayout)

        layout = QVBoxLayout()
        layout.addWidget(self.faders)
        layout.addWidget(function_box)
        self.root.setLayout(layout)

        self.setCentralWidget(self.root)

        self.workspace = None
        self.function = None

        self.timer = None 
        self.renderer = None

        self.channels = []

class PrintOutput:
    def set_values(self, values):
        for c, v in values:
            _, a = c
            #print("%3d to %3d" % (a, v))

parser = argparse.ArgumentParser()
parser.add_argument("workspace", nargs="?", action="store", type=str, help="workspace file to load")

args = parser.parse_args()

app = QApplication([])

## Necessary for MPV to work, the QApplication constructor changes this
locale.setlocale(locale.LC_NUMERIC, "C")

main = BLCQtMain()
if args.workspace is not None:
    main.load_workspace(args.workspace)
main.faders.output = OLAOutput()
main.show()
ret = app.exec_()
main.terminate()
sys.exit(ret)
