# j4cDAC test code
#
# Copyright 2011 Jacob Potter
#
# 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, version 3.
#
# 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.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import socket
import time
import struct

def pack_point(x, y, r, g, b, i = -1, u1 = 0, u2 = 0, flags = 0):
    """Pack some color values into a struct dac_point.

    Values must be specified for x, y, r, g, and b. If a value is not
    passed in for the other fields, i will default to max(r, g, b); the 
    rest default to zero.
    """
    #print(r,g,b)
    if i < 0:
        i = max(r, g, b)

    return struct.pack("<HhhHHHHHH", flags, x, y, r, g, b, i, u1, u2)


class ProtocolError(Exception):
    """Exception used when a protocol error is detected."""
    pass


class Status(object):
    """Represents a status response from the DAC."""

    def __init__(self, data):
        """Initialize from a chunk of data."""
        self.protocol_version, self.le_state, self.playback_state, \
          self.source, self.le_flags, self.playback_flags, \
          self.source_flags, self.fullness, self.point_rate, \
          self.point_count = \
            struct.unpack("<BBBBHHHHII", data)

    def dump(self, prefix = " - "):
        """Dump to a string."""
        lines = [
            "Light engine: state %d, flags 0x%x" %
                (self.le_state, self.le_flags),
            "Playback: state %d, flags 0x%x" %
                (self.playback_state, self.playback_flags),
            "Buffer: %d points" %
                (self.fullness, ),
            "Playback: %d kpps, %d points played" %
                (self.point_rate, self.point_count),
            "Source: %d, flags 0x%x" %
                (self.source, self.source_flags)
        ]
        for l in lines:
            print(prefix + l)


class BroadcastPacket(object):
    """Represents a broadcast packet from the DAC."""

    def __init__(self, st):
        """Initialize from a chunk of data."""
        self.mac = st[:6]
        self.hw_rev, self.sw_rev, self.buffer_capacity, \
        self.max_point_rate = struct.unpack("<HHHI", st[6:16])
        self.status = Status(st[16:36])

    def dump(self, prefix = " - "):
        """Dump to a string."""
        lines = [
            "MAC: " + ":".join(
                "%02x" % (ord(o), ) for o in self.mac),
            "HW %d, SW %d" %
                (self.hw_rev, self.sw_rev),
            "Capabilities: max %d points, %d kpps" %
                (self.buffer_capacity, self.max_point_rate)
        ]
        for l in lines:
            print(prefix + l)
        self.status.dump(prefix)


class DAC(object):
    """A connection to a DAC."""

    def read(self, l):
        """Read exactly length bytes from the connection."""
        while l > len(self.buf):

            buffy = self.conn.recv(4096)
            #print(buffy)

            self.buf += buffy

        obuf = self.buf
        self.buf = obuf[l:]
        return obuf[:l]

    def readresp(self, cmd):
        """Read a response from the DAC."""
        data = self.read(22)
        response = data[0]
        cmdR = chr(data[1])
        status = Status(data[2:])

        status.dump()

        if cmdR != cmd:
            raise ProtocolError("expected resp for %r, got %r"
                % (cmd, cmdR))
        if response != ord('a'):
            raise ProtocolError("expected ACK, got %r"
                % (response, ))
        
        self.last_status = status
        return status

    def __init__(self, host, port = 7765):
        """Connect to the DAC over TCP."""
        conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        conn.connect((host, port))
        conn.settimeout(1)
        self.conn = conn
        self.buf = b''

        # Read the "hello" message
        first_status = self.readresp("?")
        first_status.dump()

        '''
        self.conn.sendall('v')
        self.firmware_string = self.read(32).replace("\x00", " ").strip()
        print "Firmware: %s" % (self.firmware_string, )
        '''

    def begin(self, lwm, rate):
        cmd = struct.pack("<cHI", b'b', lwm, rate)
        self.conn.sendall(cmd)
        return self.readresp("b")

    def update(self, lwm, rate):
        cmd = struct.pack("<cHI", b'u', lwm, rate)
        self.conn.sendall(cmd)
        return self.readresp("u")

    def encode_point(self, point):
        return pack_point(*point)

    def write(self, points):
        epoints = list(map(self.encode_point, points))
        cmd = struct.pack("<cH", b'd', len(epoints))
        #print(cmd + b''.join(epoints))
        self.conn.sendall(cmd + b''.join(epoints))
        return self.readresp("d")

    def prepare(self):
        self.conn.sendall(b'p')
        return self.readresp('p')

    def stop(self):
        self.conn.sendall('s')
        return self.readresp('s')

    def estop(self):
        self.conn.sendall("\xFF")
        return self.readresp("\xFF")

    def clear_estop(self):
        self.conn.sendall('c')
        return self.readresp('c')

    def ping(self):
        self.conn.sendall('?')
        return self.readresp('?')

    def play_stream(self, stream):
        # First, prepare the stream
        if self.last_status.playback_state == 2:
            raise Exception("already playing?!")
        elif self.last_status.playback_state == 0:
            self.prepare()

        started = 0

        while True:
            # How much room?
            cap = 1799 - self.last_status.fullness
            points = stream.read(cap)

            if cap < 100:
                time.sleep(0.005)
                cap += 150

#           print "Writing %d points" % (cap, )
            t0 = time.time()
            self.write(points)
            t1 = time.time()
#           print "Took %f" % (t1 - t0, )

            if not started:
                self.begin(0, 30000)
                started = 1


def find_dac():
    """Listen for broadcast packets."""

    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.bind(("0.0.0.0", 7654))

    while True:
        data, addr = s.recvfrom(1024)
        bp = BroadcastPacket(data)
        
        print("Packet from %s: " % (addr, ))
        bp.dump()

def find_first_dac():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.bind(("0.0.0.0", 7654))
    data, addr = s.recvfrom(1024)
    bp = BroadcastPacket(data)
    print("Packet from %s: " % (addr, ))
    return addr[0]
