#!/usr/bin/python
"""
    order queue display
    Copyright (C) 2007  James Cameron (quozl@us.netrek.org)

    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; either version 2 of the License, or
    (at your option) any later version.

    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, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

--

    A shop or event display node with multicast capability.

    Basic Usage: a node with a keyboard, and any number of nodes
    without keyboard, connected via network.  The operator types a
    message, which appears on their node during composition, then when
    the commit key (TAB) is pressed the message appears on all other
    nodes.

    Additional features:

    - messages are automatically sized to fit the available screen
      dimensions,

    - multi-line messages are possible, type Enter between lines,

    - editable history of previous messages, using the up and down
      arrow keys, in the style of a shell,

    - any node can be a master, all it takes is a keyboard,

    - nodes that are alive are listed on a master node, and if a node
      goes missing, it will fail to appear,

    - displays can be mounted upside down, inversion is by command
      line option or by keyboard control on the display node,

    - special control keys for reboot, poweroff, runlevel change,
      re-executing.

"""
import os, sys, time, socket, select, struct, pygame, math

license = [
"Order Queue Display",
"Copyright (C) 2007 James Cameron <quozl@us.netrek.org>",
" ",
"This program comes with ABSOLUTELY NO WARRANTY;",
"for details see source.",
" ",
"This is free software, and you are welcome to ",
"redistribute it under certain conditions; see ",
"source for details.",
" "
]

from optparse import OptionParser
parser= OptionParser(usage="usage: %prog [options] message",
                     version="%prog 0.2")
parser.add_option("--port", type="int", dest="port", default="8521",
                  help="UDP port number for communication")
parser.add_option("--updates",
                  type="int", dest="updates", default="10",
                  help="updates per second, default 10")
parser.add_option("--size",
                  type="int", dest="size", default="900",
                  help="initial font size, default 900")
parser.add_option("--verbose",
                  action="store_true", dest="verbose", default=False,
                  help="generate verbose output")
parser.add_option("--master",
                  action="store_true", dest="master", default=False,
                  help="this is a master station")
parser.add_option("--slave",
                  action="store_true", dest="slave", default=False,
                  help="this is a slave station, disable input")
parser.add_option("--invert",
                  action="store_true", dest="invert", default=False,
                  help="display is mounted upside down")
parser.add_option("--no-license",
                  action="store_true", dest="no_license", default=False,
                  help="do not display license")
(opt, args) = parser.parse_args()

if not opt.no_license:
    for line in license:
        print line

def show_license():
    global height, license, screen
    
    fn = fc.get(None, 55)
    x = 20
    y = 20
    for line in license:
        ts = fn.render(line, 1, (255, 255, 255))
        tr = ts.get_rect(left=x, top=y)
        y = tr.bottom
        screen.blit(ts, tr)
    pygame.display.flip()
    pygame.time.wait(500)

class FC:
    def __init__(self):
        self.cache = {}

    def read(self, name, size):
        font = pygame.font.Font(name, size)
        return font

    def get(self, name, size):
        key = (name, size)
        if key not in self.cache:
            self.cache[key] = self.read(name, size)
        return self.cache[key]

fc = FC()

others = {}
others_next = 0

def show_alive(host):
    global others, others_next, height, license, screen

    (a, b, c, d) = host.split('.')
    if not others.has_key(host):
        others_next = others_next + 1
        others[host] = others_next
    fn = fc.get(None, 40)
    x = others[host] * 100
    ts = fn.render(d, 1, (0, 255, 0))
    tr = ts.get_rect(centerx=x, bottom=height)
    screen.blit(ts, tr)
    pygame.display.flip()

class UDP:
    def __init__(self, callback=None):
        global opt
        
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        try:
            self.socket.bind(('224.0.0.1', opt.port))
        except:
            print 'no route to multicast?'
        self.callback = callback
        self.servers = {}

    def send(self, text):
        global opt
        if opt.verbose: print 'send', text
        self.socket.sendto(text, ('224.0.0.1', opt.port))

    def recv(self):
        global opt, size
        while 1:
            is_readable = [self.socket]
            is_writable = []
            is_error = []
            r, w, e = select.select(is_readable, is_writable, is_error, 1.0/opt.updates)
            if not r: return
            try:
                (text, address) = self.socket.recvfrom(2048)
                break
            except:
                return
        if opt.verbose: print 'recv', text, address
        if text[:6] == '2 text':
            try:
                (a, b) = text[7:].split(' ', 1)
                size = int(a)
                self.send('3 accept ' + str(size) + ' ' + b)
                emit(b)
                size = opt.size
            except:
                if opt.verbose: print 'unhandled'
        elif text[:7] == '3 accept':
            (host, port) = address
            if opt.master:
                show_alive(host)
        elif text[:7] == '1 alive':
            (host, port) = address
            if opt.master:
                show_alive(host)

udp = UDP()

def show_history():
    global history, cursor
    
    fn = fc.get(None, 55)
    x = 20
    y = 20
    for n, line in history.iteritems():
        colour = (127, 127, 127)
        if n == cursor:
            colour = (255, 255, 255)
        (text, size) = line
        ts = fn.render("%03d %s" % (n, text), 1, colour)
        tr = ts.get_rect(left=x, top=y)
        y = tr.bottom
        screen.blit(ts, tr)
    pygame.display.flip()

def kb(event):
    global opt, screen, size, text, history, current, cursor
    
    shift = (event.mod == pygame.KMOD_SHIFT or
             event.mod == pygame.KMOD_LSHIFT or
             event.mod == pygame.KMOD_RSHIFT)
    control = (event.mod == pygame.KMOD_CTRL or
               event.mod == pygame.KMOD_LCTRL or
               event.mod == pygame.KMOD_RCTRL)
    if event.key == pygame.K_LSHIFT or event.key == pygame.K_RSHIFT: pass
    elif event.key == pygame.K_LCTRL or event.key == pygame.K_RCTRL: pass
    elif event.key == pygame.K_r and control:
        os.system('reboot')
    elif event.key == pygame.K_o and control:
        os.system('poweroff')
    elif event.key == pygame.K_s and control:
        os.system('telinit 5')
    elif event.key == pygame.K_e and control:
        os.execv('/usr/local/bin/oqd.py', ['oqd.py'])
    elif event.key == pygame.K_i and control:
        opt.invert = True
        opt.master = False
    elif event.key == pygame.K_n and control:
        opt.invert = False
        opt.master = False
    elif event.key == pygame.K_d and control:
        sys.exit()
    elif event.key == pygame.K_w and control:
        # control/w, clear buffer
        size = opt.size
        text = ''
        emit(text)
    elif event.key == pygame.K_TAB:
        # tab, send to slaves
        if cursor == current:
            history[current] = (text, size)
            current = current + 1
            cursor = current
            udp.send('2 text ' + str(size) + ' ' + text)
            text = ''
            size = opt.size
        else:
            history[cursor] = (text, size)
            udp.send('2 text ' + str(size) + ' ' + text)
    elif event.key == pygame.K_UP:
        # up arrow, move back through history
        history[cursor] = (text, size)
        if cursor != 0:
            cursor = cursor - 1
            (text, size) = history[cursor]
            emit(text, (255, 0, 255))
            show_history()
    elif event.key == pygame.K_DOWN:
        # down arrow, move back through history
        history[cursor] = (text, size)
        if cursor != current:
            cursor = cursor + 1
            (text, size) = history[cursor]
            emit(text, (255, 0, 255))
            show_history()
    elif event.key == pygame.K_BACKSPACE:
        # backspace, undo typing
        size = opt.size
        text = text[:-1]
        emit(text, (255, 255, 0))
    elif event.key == pygame.K_RETURN:
        # return, insert line break
        text = text + '\n'
        emit(text, (255, 255, 0))
    elif event.key > 31 and event.key < 255 and not control:
        text = text + event.unicode
        emit(text, (255, 255, 0))
    else:
        print 'unhandled key', event.key

def text_to_image(text, colour, size):
    global width, height

    # build a list of surfaces containing the rendered lines
    surfaces = []
    lines = text.split('\n')
    for line in lines:
        fn = fc.get(None, size)
        ts = fn.render(line, 1, colour)
        if ts.get_width() > width:
            return None
        surfaces.append(ts)

    # calculate maximum width and total height when stacked
    my = 0
    mx = 0
    for surface in surfaces:
        sx = surface.get_width()
        sy = surface.get_height()
        if sx > mx:
            mx = sx
        my = my + sy
        if mx > width or my > height:
            return None

    # make an image to hold it
    image = pygame.Surface((mx, my))

    # lay out the smaller images on the master image
    y = 0
    for surface in surfaces:
        rect = surface.get_rect(centerx = mx/2, top = y);
        image.blit(surface, rect)
        y = y + rect.height

    return image
    
def emit(text, colour=(255, 255, 255)):
    global screen, background, size, width, height

    ts = text_to_image(text, colour, size)
    while ts == None:
        size = int(size * 0.75)
        if size < 10: break
        ts = text_to_image(text, colour, size)

    if opt.invert:
        ts = pygame.transform.flip(ts, True, True)
    screen.blit(background, (0, 0))
    tr = ts.get_rect(center=(width/2, height/2))
    screen.blit(ts, tr)
    pygame.display.flip()
    
pygame.init()

size = opt.size
width, height = 1200, 900
screen = pygame.display.set_mode((width, height))
pygame.mouse.set_visible(False)

background = screen.copy()
background.fill((0, 0, 0))

if not opt.no_license:
    show_license()

udp.send('0 start')
alive = 0
text = ''
history = {}
current = cursor = 0
while 1:
    udp.recv()
    
    alive = alive + 1
    if alive > 99:
        udp.send('1 alive')
        alive = 0
        
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            if not opt.slave:
                opt.master = True
                kb(event)