import math
import time

import serial


def clamp(x, ab):
    (a, b) = ab
    return max(a, min(b, x))


def rescale(x, from_limits, to_limits):
    (a, b) = from_limits
    x_0_1 = (x - a) / (b - a)

    (c, d) = to_limits
    return c + (d - c) * x_0_1


class MovingHead:
    def __init__(self, start_addr):
        self.start_addr = start_addr

        self.pan = 0  # -3pi/2 to 3pi/2
        self.tilt = 0  # -pi/2 to pi/2
        self.speed = 0
        self.dimmer = 0  # 0 to 1
        self.rgbw = (0, 0, 0, 0)

    def __str__(self):

        return (
            f"MovingHead({self.start_addr}): pan={self.pan!r}, "
            f"tilt={self.tilt!r}, speed={self.speed!r}, "
            f"dimmer={self.dimmer!r}, rgbw={self.rgbw!r}"
        )

    def render(self, dst):

        pan = rescale(self.pan, (-1.5 * math.pi, 1.5 * math.pi), (255, 0))
        pan = clamp(int(pan), (0, 255))
        pan_fine = 0

        tilt = rescale(self.tilt, (-0.5 * math.pi, 0.5 * math.pi), (0, 255))
        tilt = clamp(int(tilt), (0, 255))
        tilt_fine = 0

        dimmer = clamp(7 + int(127 * self.dimmer), (7, 134))

        (r, g, b, w) = self.rgbw

        channels = [
            pan,
            pan_fine,
            tilt,
            tilt_fine,
            self.speed,
            dimmer,
            r,
            g,
            b,
            w,
            0,  # color mode
            0,  # auto jump speed
            0,  # control mode
            0,  # reset
        ]

        offset = self.start_addr - 1

        dst[offset : offset + len(channels)] = channels


if __name__ == "__main__":
    
    head = MovingHead(1)
    head.rgbw = (0x00, 0x00, 0xFF, 0)
    head.tilt = -0.5 * math.pi
    head.dimmer = 1
    
    dmx_data = bytearray(512)

    with serial.Serial("/dev/ttyUSB0", 500_000) as ser:

        def sync():
            # wait for sync
            while True:
                b = ser.readline()
                if b.strip() == b"Sync.":
                    return

        print("syncing")
        sync()
        
        t0 = time.time()

        left = -1.5 * math.pi
        right = 1.5 * math.pi

        while True:
            
            now = time.time() - t0
            

            if int(now) % 10 < 5:
                head.pan = left
                head.rgbw = (0xFF, 0x00, 0x00, 0)
            else:
                head.pan = right
                head.rgbw = (0x00, 0xFF, 0x00, 0)
                
            head.render(dmx_data)

            ser.write(dmx_data)
            ser.flush()
            response = ser.readline()
            if response.strip() != b"Ack.":
                print(f"received bad response: {response!r}")
                sync()
