# -*- coding: utf-8 -*-
"""
Main program which runs continually and controls the moving head.

It receives commands from the ARTA driver and converts them to DMX commands.
"""
from multiprocessing.connection import Listener
import numpy as np
from PyQt6.QtCore import QSize, Qt
from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel
# There is an error in the package PyDMXControl. To fix it, open
# PyDMXControl\controllers\ _Controller.py
# and comment-out the line "from ..web import WebController"
from PyDMXControl.controllers import SerialController
from PyDMXControl.profiles.Generic import Custom
from serial.serialutil import SerialException
import sys
import threading
import time

# Communication with ARTA driver
address = ('localhost', 6000)  # family is deduced to be 'AF_INET'
authkey = b'FtGz6EyHQ38ki6RaUKP4'

# Port of USB --> DMX adapter (FTDI FT232R)
port = "COM3"


class RepeatedTimer(object):
    """
    Periodically execute a function.

    https://stackoverflow.com/questions/474528/how-to-repeatedly-execute-a
    -function-every-x-seconds
    """

    def __init__(self, interval, function, *args, **kwargs):
        self._timer = None
        self.interval = interval
        self.function = function
        self.args = args
        self.kwargs = kwargs
        self.is_running = False
        self.start()

    def _run(self):
        self.is_running = False
        self.start()
        self.function(*self.args, **self.kwargs)

    def start(self):
        """Start timer."""
        if not self.is_running:
            self._timer = threading.Timer(self.interval, self._run)
            self._timer.start()
            self.is_running = True

    def stop(self):
        """Stop making new function calls."""
        self._timer.cancel()
        self.is_running = False


def angle_to_dmx(angle):
    """
    Convert angle to dmx value.

    Arg_:
        angle: [-360, 360] (deg)

    Return_:
        dmx value: [0, 255] (-)
    """
    a = np.asarray([-262.5, 270.5])     # range of angles
    d = np.asarray([0, 255])            # range of dmx values
    dval = np.round(np.interp(angle, a, d, left=0, right=255)).astype(np.uint8)
    return dval


class MainWindow(QMainWindow):
    """Graphical feedback for ARTA user."""

    def __init__(self):
        QMainWindow.__init__(self)
        self.setWindowTitle("ARTA turntable driver")
        self.setFixedSize(QSize(400, 300))

        self.label = QLabel(self)
        self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.label.setFont(QFont("Arial", 30))
        self.label.setText("Waiting for ARTA")
        self.setCentralWidget(self.label)

        try:
            self.init_dmx()
            self.listener = Listener(address, authkey=authkey)
            self.rt = RepeatedTimer(0.05, self.update_function)
        except SerialException:
            self.txt(f"Cannot connect\nto USB --> DMX\non port {port}")

    def init_dmx(self):
        """
        Initialize DMX settings.

        ADJ DJ Spot 300
        DMX channels
        1: pan                  0=fully counter clockwise
        2: tilt
        3: color wheel;         0=white
        4: gobo wheel;          0=open
        5: gobo rotation;       171=no rotation
        6: shutter / strobe;    0=closed / 255=open
        7: special features;    0-191=gobo shake / 191-255=reset
        8: pan/tilt speed;      0=fast / 255=slow

        Subtract 1 from the channels to get the Python index.
        """
        # Initialize channel data
        x = np.zeros(8, dtype=np.uint8)
        x[0] = angle_to_dmx(0)  # pan position
        x[1] = 0    # tilt position
        x[2] = 0    # white
        x[3] = 0    # spot
        x[4] = 171  # no rotation
        x[5] = 0    # shutter closed = lamp off
        x[6] = 0    # special features: none selected
        x[7] = 0    # fast movement

        # Initialize controller
        self.dmx = SerialController(port)                     # Controller
        self.turntable = Custom(channels=8, start_channel=1)  # Fixture
        self.dmx.add_fixture(self.turntable)

        # Write data
        self.turntable.set_channels(values=x)

    def txt(self, msg):
        """Update label text."""
        self.label.setText(msg)

    def update_function(self):
        """Periodically check for incoming commands."""
        with self.listener.accept() as conn:
            s = conn.recv()
            if s == '-r':
                # Reset
                self.txt("Reset")
                self.turntable.set_channel(channel=6, value=255)
                xb = self.x.tobytes()
                self.usb_dmx.set_data(xb)
                time.sleep(6)  # the reset is carried out after ~3 seconds
                # release reset and move to 0 degrees
                self.turntable.set_channel(channel=6, value=0)
                self.turntable.set_channel(channel=0, value=angle_to_dmx(0))
            else:
                # Move to position
                dval = angle_to_dmx(s)
                self.txt(f"{s}" + u"\u00b0")
                self.turntable.set_channel(channel=0, value=dval)

    def closeEvent(self, event):
        """Clean up."""
        self.txt("Goodbye")
        self.rt.stop()
        self.listener.close()
        self.dmx.close()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec()
