#!/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, and executing python commands.

"""
import os, sys, time, socket, select, pygame, code

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.",
" "
]

class StdoutBuffer:
    def __init__(self):
        self.erase()
    def write(self, data):
        self.buffer = self.buffer + data
    def read(self):
        return self.buffer
    def erase(self):
        self.buffer = ''

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

    def read(self, name, size):
        if name == None:
            return pygame.font.Font(None, size)
        
        path = '/usr/share/fonts/truetype/ttf-dejavu/'
        try:
            return pygame.font.Font(path + name, size)
        except:
            return pygame.font.Font(None, size)

    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]

class UDP:
    def __init__(self, callback=None):
        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):
        if opt.verbose: print 'send', text
        self.socket.sendto(text, ('224.0.0.1', opt.port))

    def recv(self):
        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 None
            try:
                (data, address) = self.socket.recvfrom(2048)
                break
            except:
                return None
        return (data, address)

def text_to_image(text, colour, size):
    """ convert text to a centered image """
    
    # build a list of surfaces containing the rendered lines
    surfaces = []
    lines = text.split('\n')
    for line in lines:
        fn = fc.get('DejaVuSans-Bold.ttf', 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 size

    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()

def show_license():
    fn = fc.get('DejaVuSansMono.ttf', 35)
    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)

others = {}
others_next = 0

def show_alive(host):
    global others, others_next

    (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()

def show_history():
    fn = fc.get('DejaVuSansMono.ttf', 45)
    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("%02d %s" % (n, text), 1, colour)
        tr = ts.get_rect(left=x, top=y)
        y = tr.bottom
        screen.blit(ts, tr)
    pygame.display.flip()

def op_shell():
    global sys, text

    realstdout = sys.stdout
    sys.stdout = mf
    ii.runsource(text)
    sys.stdout = realstdout
    text = mf.read()
    mf.erase()
    # TODO: strip last newline
    size = opt.size
    try:
        emit(text)
    except:
        emit('result\ncannot be\nrendered')

def op_reboot():
    os.system('reboot')

def op_poweroff():
    os.system('poweroff')

def op_sugar():
    os.system('telinit 5')
    
def op_exec():
    os.execv('/usr/local/bin/oqd.py', ['oqd.py'])

def op_invert():
    global opt
    
    opt.invert = True
    opt.master = False

def op_normal():
    global opt
    
    opt.invert = False
    opt.master = False

def op_quit():
    sys.exit()

def ed_clear():
    """ clear text entry """
    global size, text
    
    size = opt.size
    text = ''
    emit(text)
    
def ed_up():
    """ move back through history """
    global history, cursor, text, size
    
    history[cursor] = (text, size)
    if cursor != 0:
        cursor = cursor - 1
        (text, size) = history[cursor]
        emit(text, (255, 0, 255))
        show_history()

def ed_down():
    """ move forward through history """
    global history, cursor, text, size
    
    history[cursor] = (text, size)
    if cursor != current:
        cursor = cursor + 1
        (text, size) = history[cursor]
        emit(text, (255, 0, 255))
        show_history()

def ed_backspace():
    """ undo typing """
    global size, text
    
    size = opt.size
    text = text[:-1]
    emit(text, (255, 255, 0))

def ed_newline():
    """ insert line break """
    global text
    
    text = text + '\n'
    emit(text, (255, 255, 0))

def ed_insert(event):
    """ insert entered text """
    global text
    
    text = text + event.unicode
    emit(text, (255, 255, 0))

def op_send():
    """ send to slaves """
    global cursor, current, history, text, size
    
    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)

# control keyboard table, relates keys to functions
kb_table_control = {
    pygame.K_EQUALS: op_shell,
    pygame.K_r: op_reboot,
    pygame.K_o: op_poweroff,
    pygame.K_s: op_sugar,
    pygame.K_e: op_exec,
    pygame.K_i: op_invert,
    pygame.K_n: op_normal,
    pygame.K_d: op_quit,
    pygame.K_w: ed_clear
    }

# normal keyboard table, relates keys to functions
kb_table_normal = {
    pygame.K_UP: ed_up,
    pygame.K_DOWN: ed_down,
    pygame.K_BACKSPACE: ed_backspace,
    pygame.K_RETURN: ed_newline,
    pygame.K_TAB: op_send
    }

def kb(event):
    """ handle keyboard events caused by user """

    # if we are started as a slave, ignore keyboard
    if opt.slave:
        return

    # assume we are a master if there is any ambiguity
    opt.master = True

    # ignore the shift and control keys
    if event.key == pygame.K_LSHIFT or event.key == pygame.K_RSHIFT: return
    if event.key == pygame.K_LCTRL or event.key == pygame.K_RCTRL: return

    # check for control key sequences pressed
    if (event.mod == pygame.KMOD_CTRL or
        event.mod == pygame.KMOD_LCTRL or
        event.mod == pygame.KMOD_RCTRL):
        if kb_table_control.has_key(event.key):
            handler = kb_table_control[event.key]
            handler()
        return

    # check for normal keys pressed
    if kb_table_normal.has_key(event.key):
        handler = kb_table_normal[event.key]
        handler()
        return

    # all other keys that are printable, insert them on screen
    if event.key > 31 and event.key < 255:
        ed_insert(event)
        return
    
    print 'unhandled key', event.key

def inbound(data, address):
    global size
    
    if opt.verbose: print 'recv', data, address
    if data[:6] == '2 text':
        (a, b) = data[7:].split(' ', 1)
        size = int(a)
        udp.send('3 accept ' + str(size) + ' ' + b)
        emit(b)
        size = opt.size
    elif data[:7] == '3 accept':
        (host, port) = address
        if opt.master:
            show_alive(host)
    elif data[:7] == '1 alive':
        (host, port) = address
        if opt.master:
            show_alive(host)

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

pygame.init()

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

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

fc = FontCache()
if not opt.no_license:
    show_license()

mf = StdoutBuffer()
ii = code.InteractiveInterpreter()

udp = UDP()
udp.send('0 start')
alive = 0

text = ''
size = opt.size

history = {}
current = cursor = 0

while True:
    # handle any quit or keyboard events from pygame interface
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            kb(event)

    # handle any network packets from peers
    pair = udp.recv()
    if pair != None:
        (data, address) = pair
        inbound(data, address)

    # occasionally inform peers we are alive
    alive = alive + 1
    if alive > 99:
        udp.send('1 alive')
        alive = 0
