#!/usr/bin/python
import os, sys, socket, select, pygame

from optparse import OptionParser
parser= OptionParser()
parser.add_option("--server", dest="server", default="leds",
                  help="k74 server host name or ip address")
parser.add_option("--port-watch", type="int", dest="pw", default="7299",
                  help="base port number for k74 watch")
parser.add_option("--port-set", type="int", dest="ps", default="7200",
                  help="base port number for k74 set requests")
parser.add_option("--port-clear", type="int", dest="pc", default="7300",
                  help="base port number for k74 clear requests")
parser.add_option("--label-font-size", type="int", dest="lfs", default="36",
                  help="label font size")
parser.add_option("--read-only",
                  action="store_true", dest="ro", default=False,
                  help="perform no state changes here")
parser.add_option("--sound",
                  action="store_true", dest="sound", default=False,
                  help="enable sound effects")
parser.add_option("--title",
                  dest="title", default="k74 view",
                  help="window title name")
parser.add_option("--assets",
                  dest="assets", default="/usr/share/k74",
                  help="location of package imagery and sound assets")
parser.add_option("--horizontal",
                  action="store_true", dest="horizontal", default=False,
                  help="layout horizontal")
(opt, args) = parser.parse_args()

class MultipleImageSprite(pygame.sprite.Sprite):
    """ a sprite class consisting of multiple images overlaid
        the images are blitted over each other in the order they are added
    """
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)

    def mi_begin(self):
        self.ml = []
        self.mr = None

    def mi_add(self, image, rect):
        self.ml.append((image, rect))
        if self.mr == None:
            self.mr = rect
        else:
            self.mr = pygame.Rect.union(self.mr, rect)

    def mi_add_image(self, image):
        rect = image.get_rect()
        self.mi_add(image, rect)

    def mi_commit(self):
        width = self.mr.width
        height = self.mr.height
        self.image = pygame.Surface((width, height), pygame.SRCALPHA, 32)
        for x in self.ml:
            (image, rect) = x
            rect.center = (width/2, height/2)
            self.image.blit(image, rect)
        self.rect = self.image.get_rect()

class BitSprite(MultipleImageSprite):
    def __init__(self, x, y, label, name):
        MultipleImageSprite.__init__(self)
        self.mi_begin()

        try:
            g1 = pygame.image.load(name+'.png')
        except:
            g1 = pygame.image.load(os.path.join(opt.assets, name+'.png'))
        g1 = g1.convert_alpha()
        self.mi_add_image(g1)
        font = pygame.font.Font(None, opt.lfs)
        text = font.render(label, 1, (0, 0, 0))
        self.mi_add_image(text)
        self.mi_commit()
        self.rect.center = (x, y)

class Client:
    def __init__(self, port, reconnect=False):
        self.socket = None
        self.port = port
        self.reconnect = reconnect
        self.status = 0
        self.connect()

    def connect(self):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.connect((opt.server, self.port))
        # FIXME: can get socket.error:(111, 'Connection refused') if
        # the server is not yet running, we should probably show that
        # and try again.
        self.buffer = ''

    def close(self):
        self.socket.close()
        self.socket = None
        if self.reconnect:
            self.connect()

    def recv(self):
        while 1:
            is_readable = [self.socket]
            is_writable = []
            is_error = [self.socket]
            r, w, e = select.select(is_readable, is_writable, is_error, 0.1)
            if e:
                self.close()
                return False
            if not r: return False
            try:
                byte = self.socket.recv(1)
                if len(byte) == 1:
                    self.recv_byte(byte)
                    return True
                else:
                    self.recv_eof()
                    return False
            except:
                self.recv_eof()
                return False

    def recv_byte(self, byte):
        if byte == '\n':
            self.status = int(self.buffer)
            self.buffer = ''
        else:
            self.buffer = self.buffer + byte

    def recv_eof(self):
        self.close()

def led_set(led):
    if opt.ro: return
    request = Client(opt.ps + led)
    request.close()

def led_clear(led):
    if opt.ro: return
    request = Client(opt.pc + led)
    request.close()

def mb(event):
    if opt.horizontal:
        i = event.pos[0] / 100 + 1
    else:
        i = event.pos[1] / 100 + 1
    if event.button == 1:
        if opt.sound:
            sound_click.play()
        led_set(i)
    elif event.button == 3:
        if opt.sound:
            sound_click.play()
        led_clear(i)

def kb(event):
    keys = { pygame.K_1: 1,
             pygame.K_2: 2,
             pygame.K_3: 3,
             pygame.K_4: 4,
             pygame.K_5: 5,
             pygame.K_6: 6,
             pygame.K_7: 7,
             pygame.K_8: 8,
             pygame.K_KP1: 1,
             pygame.K_KP2: 2,
             pygame.K_KP3: 3,
             pygame.K_KP4: 4,
             pygame.K_KP5: 5,
             pygame.K_KP6: 6,
             pygame.K_KP7: 7,
             pygame.K_KP8: 8 }
    if keys.has_key(event.key):
        n = keys[event.key]
        shift = (event.mod == pygame.KMOD_SHIFT or
                 event.mod == pygame.KMOD_LSHIFT or
                 event.mod == pygame.KMOD_RSHIFT)
        if shift:
            led_set(n)
        else:
            led_clear(n)

def led_show_set(x):
    group.add(bit_sprites_1[x])
    group.remove(bit_sprites_0[x])

def led_show_clear(x):
    group.remove(bit_sprites_1[x])
    group.add(bit_sprites_0[x])

def led_flush():
    group.clear(screen, background)
    group.update()
    dirty = group.draw(screen)
    pygame.display.update(dirty)

def led_show_all(new):
    for x in range(8):
        if new & (1 << x):
            led_show_set(x)
        else:
            led_show_clear(x)
    led_flush()

def led_change(xor, new):
    for x in range(8):
        if xor & (1 << x):
            if new & (1 << x):
                """ a led turned on """
                led_show_set(x)
                if opt.sound:
                    sound_on.play()
            else:
                """ a led turned off """
                led_show_clear(x)
                if opt.sound:
                    sound_off.play()
    led_flush()

#! @bug: full screen black flash, due this?
if opt.sound:
    pygame.mixer.pre_init(44100)

pygame.init()
pygame.display.set_caption(opt.title)

if opt.horizontal:
    size = width, height = 800, 100
else:
    size = width, height = 100, 800
screen = pygame.display.set_mode(size)

def load_sound(name):
    try:
        sound = pygame.mixer.Sound(name)
    except:
        sound = pygame.mixer.Sound(os.path.join(opt.assets, name))
    return sound

if opt.sound:
    sound_on = load_sound('on.ogg')
    sound_off = load_sound('off.ogg')
    sound_click = load_sound('click.ogg')

try:
    background = pygame.image.load("background.png")
except:
    background = pygame.image.load(os.path.join(opt.assets, "background.png"))
background = background.convert_alpha()
bh = background.get_height()
bw = background.get_width()
for y in range(screen.get_height() / bh + 1):
    for x in range(screen.get_width() / bw + 1):
        screen.blit(background, (x*bw, y*bh))
background = screen.copy()

group = pygame.sprite.OrderedUpdates(())
bit_sprites_1 = {}
bit_sprites_0 = {}
if opt.horizontal:
    mx = 100
    my = 0
else:
    mx = 0
    my = 100
for i in range(8):
    x = i * mx + 50
    y = i * my + 50
    l = str(i+1)
    bit_sprites_0[i] = BitSprite(x, y, l, 'led-0-100x100')
    bit_sprites_1[i] = BitSprite(x, y, l, 'led-1-100x100')
pygame.display.flip()

watch = Client(opt.pw, reconnect=True)
watch.recv()

""" display the current status """
new = watch.status
led_show_all(new)
old = new

while 1:
    if watch.recv():
        pygame.time.set_timer(pygame.USEREVENT, 10100)
    new = watch.status
    xor = new ^ old;
    if xor != 0:
        led_change(xor, new)
        old = new
    for event in pygame.event.get():
        if event.type == pygame.MOUSEBUTTONDOWN:
            mb(event)
        elif event.type == pygame.KEYDOWN:
            kb(event)
        elif event.type == pygame.QUIT:
            sys.exit(0)
        elif event.type == pygame.USEREVENT:
            print "k74-view: inactivity timeout, network socket, retrying"
            watch.close()

# FIXME: event log view
# FIXME: add explanation for each bit
# FIXME: full screen mode on xo
# FIXME: if no activity for ten seconds on socket, close and retry
# FIXME: if system suspended and then resumed, socket may be left
# open, but never receive any further data.
