#   Copyright (c) 2007 Axel Wachtler
#   All rights reserved.
#
#   Redistribution and use in source and binary forms, with or without
#   modification, are permitted provided that the following conditions
#   are met:
#
#   * Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#   * Neither the name of the authors nor the names of its contributors
#     may be used to endorse or promote products derived from this software
#     without specific prior written permission.
#
#   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#   AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#   ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
#   LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
#   CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
#   SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
#   INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
#   CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
#   ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
#   POSSIBILITY OF SUCH DAMAGE.

# $Id: sterm.py,v 1.1 2008/03/21 11:14:34 awachtler Exp $
##
# @file
# @brief serial terminal application
#
#

# === Imports ==========================================================
from Tkinter import *
from ScrolledText import ScrolledText
from FileDialog import FileDialog
from ConfigParser import ConfigParser
import sys, threading, time, Queue, pprint, serial, re

def ppd(x):
    ret = {}
    for e in dir(x):
        t = eval("type(x.%s)" % e)
        try:
            ret[t].append(e)
        except:
            ret[t] = [e]
    print "="*42
    print "Instance:"
    pprint.pprint(x)
    print "Members:"
    pprint.pprint(ret)

try:
    import readline, rlcompleter
    readline.parse_and_bind("tab:complete")
except:
    pass

# === Globals ==========================================================

FRAMEBORDER = {"relief":"groove", 'border':1}
FRAMEBORDER1 = {"relief":"ridge", 'border':3}

# === Classes ==========================================================

##
# @brief Terminal IO helper class using File Fifos
# This class is used to simulate serial IO connections between
# terminal windows.
#
class FifoIo:
    def __init__(self, name="dev"):
        self.inQueue = Queue.Queue()
        self.name = name
        self.outQueue = None
        self.peer = None
        self.isConnected = False

    def configure(self, cfg):
        if cfg.has_key('peer'):
            self.peer = eval(cfg["peer"])

    def config(self):
        cfg = {}
        cfg["peer"] = self.peer.name
        return cfg


    def disconnect(self):
        self.isConnected = False
        if self.peer:
            #self.peer.outQueue = None
            self.outQueue = None

    def connect(self):
        self.outQueue = self.peer.inQueue
        self.isConnected = True

    def write(self, data):
        try:
            self.outQueue.put(data)
        except:
            pass

    def read(self):
        return self.inQueue.get()

class SerialIo(FifoIo):
    def __init__(self, name="dev"):
        self.sport = serial.Serial()
        self.name = name
        self.isConnected = False

    def configure(self, cfg):
        if cfg.has_key('port'):
            self.sport.port = cfg['port']

    def config(self):
        cfg = {}
        cfg["port"] = self.sport.port
        return cfg

    def disconnect(self):
        self.isConnected = False
        self.sport.close()

    def connect(self):
        self.isConnected = True
        self.sport.open()

    def write(self, data):
        try:
            self.sport.write(data)
        except:
            print "Error Write"

    def read(self):
        return self.sport.read()

##
# terminal windows class
#
# @ingroup stermGUI
#
class TermWindow(Frame):
    ##
    # constructor method
    def __init__(self, master = None, **cnf):
        apply(Frame.__init__, (self, master), cnf)
        self.__create_widgets__()
        self.rxThread = None
        self.localEcho = False
        self.logFile = None

    ##
    # worker function for threaded reading from the input of
    # the assigned device
    #
    def __thrd_reader__(self):
        while 1:
            try:
                x = self.device.read()
                if x:
                    cmd = self.cmdVar.get()
                    if len(cmd):
                        self.cmdVar.set("")
                        if self.localEcho:
                            self.display("\n[%s]\n" % cmd, 'E')
                    self.write(x)
                else:
                    time.sleep(1)
            except:
                time.sleep(1)
    ##
    #
    def __create_widgets__(self):
        dfl_font = 'Helvetica 10'

        # the title frame of the terminal
        self.f_title = Frame(self)
        self.f_title.pack(fill=BOTH)
        self.f_title.configure(FRAMEBORDER)
        self.shVar = StringVar()
        # show/hide button
        self.b_sh = Button(self.f_title, textvariable=self.shVar, font=dfl_font)
        self.b_sh.pack(side=RIGHT)
        self.b_sh['command'] = self.show_hide_cont
        # clear screen button
        self.b_cls = Button(self.f_title, text="CLS", font=dfl_font, underline=0)
        self.b_cls.pack(side=RIGHT)
        self.b_cls['command'] = self.clear_screen
        # echo on/off button
        self.al_echo = Label(self.f_title, text = "ECHO", relief = RAISED,
                             font = dfl_font, padx='3m', pady='1m', underline=0)
        self.al_echo.pack(side=RIGHT, padx=1, pady=1, fill=BOTH)
        self.al_echo.bind("<Button-1>", self.__echo_handler__)
        # log on/off button
        self.al_log = Label(self.f_title, text = "LOG", relief = RAISED,
                            font = dfl_font, padx='3m', pady='1m', underline=0)
        self.al_log.pack(side=RIGHT, padx=1, pady=1, fill=BOTH)
        self.al_log.bind("<Button-1>", self.__log_handler__)
        # device connect button
        self.al_connect = Label(self.f_title, text = "CONNECT", relief = RAISED,
                                font = dfl_font, padx='3m', pady='1m', underline=1)
        self.al_connect.pack(side=RIGHT, padx=1, pady=1, fill=BOTH)
        self.al_connect.bind("<Button-1>", self.__connect_handler__)

        # title of terminal window
        self.tVar = StringVar()
        self.l_title = Label(self.f_title, text="foo", font=dfl_font)
        self.l_title['textvariable'] = self.tVar
        self.l_title.pack(side=LEFT, expand=1, fill=X)
        self.l_title['width'] = 42
        self.update_title("------ XXX ------")
        # frame for scrolled text window
        # (this frame handle needs to be kept fo show_hide_cont())
        self.f_cont = Frame(self)
        # IO data scrolled text window
        self.st_trm = ScrolledText(self.f_cont, height=10, state=DISABLED, wrap=NONE)
        self.st_trm.pack(expand=1,fill=BOTH)
        self.st_trm['font'] = dfl_font
        self.st_trm.tag_config('E', foreground="blue")
        self.st_trm.tag_config('M', foreground="magenta")

        tframe = Frame(self.f_cont)
        tframe.pack(expand = 0, fill = X)
        self.cmdVar = StringVar()
        self.ent_trm = Entry(tframe, textvariable=self.cmdVar, font=dfl_font)
        self.ent_trm.pack(side=LEFT, expand =1, fill = X)
        self.ent_trm.bind("<Control-l>", self.__log_handler__)
        self.ent_trm.bind("<Control-e>", self.__echo_handler__)
        self.ent_trm.bind("<Control-o>", self.__connect_handler__)
        self.ent_trm.bind("<Control-c>", self.clear_screen)
        self.ent_trm.bind("<Control-x>", self.show_hide_cont)
        self.ent_trm.bind("<KeyPress>", self.__input_handler__)

        self.b_hello = Button(tframe, text="hello", font=dfl_font)
        self.b_hello['command'] = self.do_hello
        self.b_hello.pack(side=LEFT)

        self.gui_elements = [ self.b_sh,
                              self.b_cls,
                              self.al_echo,
                              self.al_log,
                              self.al_connect,
                              self.l_title,
                              self.st_trm,
                              self.ent_trm,
                              self.b_hello ]
        self.show_cont()

    def _configure_(self,**args):
        print args
        for e in self.gui_elements:
            e.configure(args)


    def __input_handler__(self, *args):
        for i in args:
            #print self.device.name, len(i.char), i.keycode, "[%s]" %i.char
            if len(i.char):
                if i.char == "\r":
                    self.device.write("\r\n")
                    self.cmdVar.set("")
                else:
                    self.device.write(i.char)

    def __echo_handler__(self, *args):
        if self.localEcho:
            self.localEcho = False
            self.al_echo['relief'] = RAISED
            self.message("Local Echo OFF")
        else:
            self.localEcho = True
            self.al_echo['relief'] = SUNKEN
            self.message("Local Echo ON")

    def __log_handler__(self, *args):
        try:
            do_open = self.logFile.closed
            logname = self.logFile.name
        except:
            do_open = True
            logname = ""
        if do_open:
            fd = FileDialog(self)
            logname = fd.go(logname)
            try:
                self.logFile = open(logname,"a")
                self.al_log['relief'] = SUNKEN
                self.message("Logging ON: %s" % self.logFile.name)
            except:
                self.message("Error open logfile: %s" % logname)

        else:
            self.logFile.close()
            self.al_log['relief'] = RAISED
            self.message("Logging OFF: %s" % self.logFile.name)

    def __connect_handler__(self, *args):
        print self.device
        if self.device.isConnected:
            self.device.disconnect()
            self.al_connect['relief'] = RAISED
            self.message("Disconnected")
        else:
            self.device.connect()
            self.al_connect['relief'] = SUNKEN
            self.message("Connected")

    def clear_screen(self, *args):
        self.st_trm['state'] = NORMAL
        self.st_trm.delete("0.0",END)
        self.st_trm['state'] = DISABLED

    def set_device(self,device):
        if self.rxThread:
            self.rxThread.join()
        self.device = device
        self.rxThread = threading.Thread(target = self.__thrd_reader__)
        self.rxThread.setDaemon(1) # if a thread is not a daemon, the program needs to join all
        self.rxThread.setName("GUI_RX_%s" % self.device.name)
        self.rxThread.start()
        self.update_title(self.device.name)

    def update_title(self, title):
        self.tVar.set(title)

    def show_cont(self):
        self.shVar.set("X")
        self.f_cont.pack(expand=1,fill=BOTH)

    def hide_cont(self):
        self.shVar.set("+")
        self.f_cont.pack_forget()

    def show_hide_cont(self, *args):
        if self.shVar.get() == "X":
            self.hide_cont()
        else:
            self.show_cont()

    def do_hello(self):
        cmd = "hello %d\n" % 42
        #self.cmdVar.set(cmd)
        self.device.write(cmd)

    def write(self, data):
        self.display(data)

    def message(self, text, tag='M'):
        msg = "[%s:%s:%s]\n" % (time.asctime(),self.device.name, text)
        if self.st_trm.index(AtInsert()).find(".0")  < 1:
            msg = "\n" + msg
        self.display(msg, tag)

    def display(self, msg, tag = None):
        self.st_trm['state'] = NORMAL
        here =  self.st_trm.index(AtInsert())
        for d in re.split("([\r\v\t\n])", msg):
            if len(d):
                if d == '\r':
                    self.st_trm.insert(END,"\n")
                else:
                    self.st_trm.insert(END, d)
        if tag:
            self.st_trm.tag_add(tag, here, AtInsert())
        self.st_trm.see(END)
        self.st_trm['state'] = DISABLED
        try:
            self.logFile.write(msg)
            self.logFile.flush()
        except:
            pass

##
# Terminal
class TermMain(Frame):
    def __init__(self,master = None, **cnf):
        apply(Frame.__init__, (self, master), cnf)

##
# Terminal Status Line
class TermStatus(Frame):
    def __init__(self,master = None, **cnf):
        apply(Frame.__init__, (self, master), cnf)
        self.create_widgets()

    def create_widgets(self):
        b_quit = Button(self, text="quit")
        b_quit['command']  = self.do_quit
        b_quit.pack()

    def do_quit(self):
        print "Quit"
        sys.exit(0)

# === Functions ========================================================

##
# GUI initialisation
#
root = None

def sterm_init(cfg):
    # parse arguments
    global root, mw
    root = Tk()
    mw = []
    for d in cfg.get("CONFIG","device_list"):
        m = TermWindow(root)
        m.configure(FRAMEBORDER1)
        m.pack(expand=1,fill=BOTH)
        mw.append(m)
        m.set_device(d)
        try:
            f = cfg.get("CONFIG", "font")
            m._configure_(font = f)
        except:
            pass


    f_esc = TermStatus(root)
    f_esc.pack()
    root.mainloop()

def configure_sterm(cfg):
    global mw
    for m in mw:
        try:
            f = cfg.get("CONFIG", "font")
            m._configure_(font = f)
        except:
            pass
##
# start gui in a thread,
# @todo quiting is a problem
def start_gui_threaded(cfg):
    t = threading.Thread(target=sterm_init, args=(cfg,))
    t.setDaemon(1)
    t.setName("GUI")
    t.start()

def configure_devices(cfg):
    ret = []
    devlist = cfg.get("CONFIG","devices").split()
    devbyname = {}
    for i in devlist:
        devcfg = dict(cfg.items(i))
        if devcfg['type'] == 'fifo':
            dev = FifoIo(devcfg['name'])
            exec("global %s; %s = dev" % (devcfg['name'],devcfg['name']))
            ret.append(dev)
        elif devcfg['type'] == 'serial':
            dev = SerialIo(devcfg['name'])
            exec("global %s; %s = dev" % (devcfg['name'],devcfg['name']))
            ret.append(dev)
        else:
            print "Unknown Type", devcfg['type']
    for i in devlist:
        devcfg = dict(cfg.items(i))
        d = eval(devcfg['name'])
        d.configure(devcfg)
    return ret

if __name__ == "__main__":
    cfg = ConfigParser()
    cfg.read("sterm.cfg")
    d = configure_devices(cfg)
    cfg.set("CONFIG","device_list" , d)
    start_gui_threaded(cfg)
