#!/usr/bin/env python

# leash.py - a simple curses-based ui for boodler

# For this script to be useful, you must be running Boodler with 
# the listen.Leash module:
#    python boodler.py --listen listen.Leash

# By Paul Mazaitis: comments to comment@goob.com

# Version 1.0 - 02.10.2002

# Version 0.9 - 02.03.2002

# Licensed under the GPL (see http://www.gnu.org/licenses/gpl.html)

# Note: this is not happy code.  But it works, more or less.

import curses, traceback
import socket, getopt
import os, re, string, sys
from boodle import listen
from boodle.agent import *

# constants

sendboodler = 1

keepgoing = 1

# Command line options

(opts,args) = getopt.getopt(sys.argv[1:], 'qh',
            ['quiet', 'help'])

if (len(opts) > 0 ) :
    if (opts[0][0] == '-h') or (opts[0][0] == '--help') :
        print 'usage:', sys.argv[0], '[--quiet] [--help]'
        print '    --quiet: do not send events to boodler process'
        print '    --help:  print this message'
        print ''

        sys.exit()

if (sys.version[0] == '1' or sys.version[0:3] == '2.0'):
    print 'Sorry; leash needs python of at least version 2.1 to run...'
    print ''

    sys.exit()

# Functions

def main(stdscr):

    global sendboodler
    global opts

    if (len(opts) > 0) :
        if (opts[0][0] == '-q') or (opts[0][0] == '--quiet') :
            sendboodler = 0

    global statuswidth
    global statusheight
    global statuswidth_t
    global groupheight_t
    global groupwidth_t
    global effectheight_t
    global effectwidth_t

    # Screen Constants

    geo = stdscr.getmaxyx()

    if (geo[1] < 40) or (geo[0] < 10) :
        curses.echo()
        curses.nocbreak()
        curses.curs_set(1)
        curses.endwin()
        print 'Leash needs at least a 40x10 screen!'
        print ''
        sys.exit()

    screenwidth = geo[1] - 1
    screenheight = geo[0] - 1

    statuswidth = screenwidth
    statusheight = 4

    statuswidth_t = statuswidth - 2
    statusheight_t = statusheight - 2

    groupwidth = 20
    groupheight = screenheight - statusheight

    groupwidth_t = groupwidth - 2
    groupheight_t = groupheight - 2

    effectwidth = screenwidth - groupwidth - 1
    effectheight = screenheight - statusheight

    effectwidth_t = effectwidth - 2
    effectheight_t = effectheight - 2

    # Set up the socket connections and such

    thing = listen.Listener(handleRCE,31864)

    global sock

    try:
        if (sendboodler):
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.connect(('localhost', 31863))
    except:
        curses.echo()
        curses.nocbreak()
        curses.curs_set(1)
        curses.endwin()
        print 'Cannot connect to boodler! (run boodler, or use --quiet)'
        sys.exit()

    # Set up windows

    global status_s
    status_s = stdscr.subwin(statusheight, statuswidth, 0, 0)
    status_s.box()
    status_s.addstr(1,1,padstring("Welcome to boodler! (h or 'menu' for help)",statuswidth_t))
    status_s.refresh()

    global group_s
    group_s = stdscr.subwin(groupheight, groupwidth, statusheight + 1, 0)
    group_s.box()
    group_s.refresh()

    global effect_s
    effect_s = stdscr.subwin(effectheight, effectwidth, statusheight+1, groupwidth+1)
    effect_s.box()
    effect_s.refresh()

    # Get the big DS of modules

    global biglist
    biglist = getdata()

    # Grumpy defaults

    # state = hold the current program state in a tuple
    # Current group, current effect, mode (0 = group, 1 = effect)

    state = [0,0,0]

    # display

    displaygroups(state)
    displayhelp()

    # loop for event

    global keepgoing

    while (keepgoing == 1) :
#    while (1) :

        y = thing.poll()

        key = stdscr.getch()

        # Handle key input here

        if key == curses.KEY_UP :
            if state[2] == 0 :
                group_up(state)
            else:
                effect_up(state)
        elif key == curses.KEY_DOWN :
            if state[2] == 0 :
                group_down(state)
            else:
                effect_down(state)
        elif key == curses.KEY_RIGHT :
            if state[2] == 0 :
                state[2] = 1
                displaygroups(state)
                displayeffects(state)

        elif key == curses.KEY_LEFT :
            if state[2] == 1 :
                state[2] = 0
                displaygroups(state)
                displayeffects(state)

        elif key == 10 :
            if state[2] == 0 :
                state[2] = 1
                displaygroups(state)
                displayeffects(state)
            else :
                playsound(state)
        elif (key == ord('t')) or (key == ord('T')) :
            playtime()
        elif (key == ord('s')) or (key == ord('S')) :
            stopsound()
        elif (key == ord('h')) or (key == ord('H')) :
            displayhelp()
        elif (key == ord('q')) or (key == ord('Q')) :
            keepgoing = 0

    # Here endeth the while

def handleRCE(event):

    global keepgoing

    if event == 'remote up' :
        if state[2] == 0 :
            group_up(state)
        else:
            effect_up(state)
    elif event == 'remote down' :
        if state[2] == 0 :
            group_down(state)
        else:
            effect_down(state)
    elif event == 'remote left' :
        if state[2] == 0 :
            state[2] = 1
            displaygroups(state)
            displayeffects(state)
    elif event == 'remote right' :
        if state[2] == 1 :
            state[2] = 0
            displaygroups(state)
            displayeffects(state)
    elif event == 'remote enter' :
        if state[2] == 0 :
            state[2] = 1
            displaygroups(state)
            displayeffects(state)
        else :
            playsound(state)
    elif event == 'remote stop' :
        stopsound()
    elif event == 'remote menu' :
        displayhelp()
    elif event == 'remote play' :
        playtime()
    elif event == 'remote exit' :
        keepgoing = 0

    return

def padstring(string, margin) :

    if (len(string) < margin) :
        padding = ( ' ' * (margin - len(string)))
        paddedstring = string + padding
    else :
        paddedstring = string[0:(margin-4)] + '...'
    return paddedstring

def displayhelp() :

    global effectheight_t
    global effectwidth_t

    helptext = [ "Commands: (keyboard, remote)",
                 "",
                 "Navigaion: arrow keys",
                 "Selection: enter, OK",
                 "Help: h, menu",
                 "Stop: s, stop",
                 "Quit: q, exit",
                 ""]
#                 "Current Time: t, play"]

    for x in range(0,len(helptext)) :
        line = padstring(helptext[x], effectwidth_t)
        effect_s.addstr((x+1),1,line)

    for y in range(len(helptext), effectheight_t) :
        effect_s.addstr(y,1,(effectwidth_t * ' '))

    effect_s.refresh()

    return

def playtime():

    if (sendboodler) :
        sock.send('time\n')

    return

def stopsound():

    global sock
    global statuswidth_t

    status_s.addstr(1,1,padstring("Welcome to boodler! (h or 'menu' for help)",statuswidth_t))
    status_s.addstr(2,1,(' ' * statuswidth_t))
    status_s.refresh()

    if (sendboodler) :
        sock.send('agent play.NullAgent \n')

    return

def playsound(state) :

    global sendboodler
    global sock
    global statuswidth_t

    desc = padstring(biglist[state[0]][1][state[1]]['desc'], statuswidth_t)

    status_s.addstr(1,1,padstring("You are listening to:",statuswidth_t))
    status_s.addstr(2,1, desc)
    status_s.refresh()

    if (sendboodler) :
        sock.send('agent ' + biglist[state[0]][1][state[1]]['module'] + '\n')

    return

def group_up(state):

    state[1] = 0
    if state[0] > 0 :
        state[0] = state[0] - 1
        displaygroups(state)
        displayeffects(state)

    return state

def group_down(state):

    state[1] = 0
    if (state[0] < (len(biglist) - 1) ) :
        state[0] = state[0] + 1
        displaygroups(state)
        displayeffects(state)

    return state

def effect_up(state):

    if state[1] > 0 :
        state[1] = state[1] - 1
        displaygroups(state)
        displayeffects(state)

    return state

def effect_down(state):

    if (state[1] < (len(biglist[state[0]][1]) -1)) :
        state[1] = state[1] + 1
        displaygroups(state)
        displayeffects(state)

    return state

def displaygroups(state):

    maxoffset = 0

    global groupheight_t
    global groupwidth_t

    if len(biglist) > groupheight_t :
        lines = groupheight_t
        maxoffset = len(biglist) - groupheight_t
    else:
        lines = len(biglist)

    offset = 0

    if (state[0] == lines) :
        offset = 1

    if (state[0] > lines) :
        offset = state[0] - lines + 1

    for index in range(offset, (lines+offset)) :

        name = padstring(biglist[index][0], groupwidth_t)

        if (index == state[0]) and (state[2] == 0) :
            group_s.addstr((index+1-offset),1,name, curses.A_REVERSE)
        elif (index == state[0]) and (state[2] == 1) :
            group_s.addstr((index+1-offset),1,name, curses.A_BOLD)
        else:
            group_s.addstr((index+1-offset),1,name)

    # Draw the scroll arrows, as necessary

    group_s.box()

    if (offset) :
        # Draw up arrow
        group_s.addstr(0,2,'(-)')

    if (maxoffset) and (offset < maxoffset) :
        # Draw down arrow
        group_s.addstr((groupheight_t+1),2,'(+)')

    group_s.refresh()

    return

def displayeffects(state):

    global effectwidth_t
    global effectheight_t

    maxoffset = 0

    if len(biglist[state[0]][1]) > effectheight_t :
        lines = effectheight_t
        maxoffset = len(biglist[state[0]][1]) - effectheight_t
        cleanend = 0
    else:
        lines = len(biglist[state[0]][1])
        cleanend = 1

    offset = 0

    if (state[1] == lines) :
        offset = 1

    if (state[1] > lines) :
        offset = state[1] - lines + 1



    for index in range(offset, (lines+offset)) :

        name = padstring(biglist[state[0]][1][index]['name'], effectwidth_t)

        if (index == state[1]) and (state[2] == 1) :
            effect_s.addstr((index+1-offset),1,name, curses.A_REVERSE)
        else:
            effect_s.addstr((index+1-offset),1,name)

    if (cleanend) :
        for clean in range((lines+1), (effectheight_t + 1)) :
            effect_s.addstr(clean,1,(effectwidth_t * ' '))

    # Draw the scroll arrows, as necessary

    effect_s.box()

    if (offset) :
        # Draw up arrow
        effect_s.addstr(0,2,'(-)')

    if (maxoffset) and (offset < maxoffset) :
        # Draw down arrow
        effect_s.addstr((effectheight_t+1),2,'(+)')


    effect_s.refresh()

    return

def getdata():

    # Get all of the effects from the boodler effects path envvar,
    # and update the python path to match

    effects_dir = os.environ.get('BOODLER_EFFECTS_PATH')

    if (effects_dir):
        effects_dir = string.split(effects_dir, ':')
        for dir in effects_dir:
            if (len(dir) > 0):
                sys.path.append(dir)

    # Get a list of effects, weeding out compiled modules and boodler
    # system modules
    #
    # XXX this only handles the first directory!

    group_name_list = []

    effects_path = os.environ.get('BOODLER_EFFECTS_PATH')

    if (effects_path) :
        effects_dirs = string.split(effects_path, ':')
        for bdir in effects_dirs :
            if (len(bdir) > 0):
                for bfile in os.listdir(bdir) :
                    if (re.match(".*\.py$", bfile)) :
                        if not(bfile in ['listen.py', 'manager.py', 'play.py', 'temp.py']) :
                            if not (bfile[0:-3] in group_name_list) :
                                group_name_list.append(bfile[0:-3])

    # We want to sort here for display and indexing later

    group_name_list.sort()

    biglist = []

    for group in group_name_list :
        things = []
        mod = __import__(group)
        for key in mod.__dict__.keys():
            obj = mod.__dict__[key]
            if (type(obj) == type(Agent) and issubclass(obj, Agent) and (obj.name) and not(obj in [Agent, FadeOutAgent, FadeInOutAgent, StopAgent, NullAgent])):
                data={ 'module' : group + "." + key, 'name' : key, 'desc' : obj.name }
                things.append(data)
        biglist.append((group,things))

    return biglist

if __name__=='__main__':

    try:

        # Initialize curses
        stdscr=curses.initscr()
        stdscr.keypad(1)
        curses.noecho()
        curses.cbreak()
        curses.halfdelay(1)
        curses.curs_set(0)
        # Enter main loop
        main(stdscr)
        # When we come back, set everything to normal
        curses.echo()
        curses.nocbreak()
        curses.curs_set(1)
        curses.endwin()
    except:
        # If we blow it, return the terminal to a useful state
        curses.echo()
        curses.nocbreak()
        curses.curs_set(1)
        curses.endwin()
#        traceback.print_exc()
