# Tk UI
# Copyright (C) 2002, 2003 John Goerzen
# <jgoerzen@complete.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., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA

from __future__ import nested_scopes

from Tkinter import *
import tkFont
from threading import *
import thread, traceback, time, threading
from StringIO import StringIO
from ScrolledText import ScrolledText
from offlineimap import threadutil, version
from Queue import Queue
from UIBase import UIBase
from offlineimap.ui.Blinkenlights import BlinkenBase

usabletest = None

class PasswordDialog:
    def __init__(self, accountname, config, master=None, errmsg = None):
        self.top = Toplevel(master)
        self.top.title(version.productname + " Password Entry")
        text = ''
        if errmsg:
            text = '%s: %s\n' % (accountname, errmsg)
        text += "%s: Enter password: " % accountname
        self.label = Label(self.top, text = text)
        self.label.pack()

        self.entry = Entry(self.top, show='*')
        self.entry.bind("<Return>", self.ok)
        self.entry.pack()
        self.entry.focus_force()

        self.button = Button(self.top, text = "OK", command=self.ok)
        self.button.pack()

        self.entry.focus_force()
        self.top.wait_window(self.label)

    def ok(self, args = None):
        self.password = self.entry.get()
        self.top.destroy()

    def getpassword(self):
        return self.password

class TextOKDialog:
    def __init__(self, title, message, blocking = 1, master = None):
        if not master:
            self.top = Tk()
        else:
            self.top = Toplevel(master)
        self.top.title(title)
        self.text = ScrolledText(self.top, font = "Courier 10")
        self.text.pack()
        self.text.insert(END, message)
        self.text['state'] = DISABLED
        self.button = Button(self.top, text = "OK", command=self.ok)
        self.button.pack()

        if blocking:
            self.top.wait_window(self.button)

    def ok(self):
        self.top.destroy()
        
                                 

class ThreadFrame(Frame):
    def __init__(self, master=None):
        self.threadextraframe = None
        self.thread = currentThread()
        self.threadid = thread.get_ident()
        Frame.__init__(self, master, relief = RIDGE, borderwidth = 2)
        self.pack(fill = 'x')
        self.threadlabel = Label(self, foreground = '#FF0000',
                                 text ="Thread %d (%s)" % (self.threadid,
                                                     self.thread.getName()))
        self.threadlabel.pack()
        self.setthread(currentThread())

        self.account = "Unknown"
        self.mailbox = "Unknown"
        self.loclabel = Label(self,
                              text = "Account/mailbox information unknown")
        #self.loclabel.pack()

        self.updateloclabel()

        self.message = Label(self, text="Messages will appear here.\n",
                             foreground = '#0000FF')
        self.message.pack(fill = 'x')

    def setthread(self, newthread):
        if newthread:
            self.threadlabel['text'] = newthread.getName()
        else:
            self.threadlabel['text'] = "No thread"
        self.destroythreadextraframe()

    def destroythreadextraframe(self):
        if self.threadextraframe:
            self.threadextraframe.destroy()
            self.threadextraframe = None

    def getthreadextraframe(self):
        if self.threadextraframe:
            return self.threadextraframe
        self.threadextraframe = Frame(self)
        self.threadextraframe.pack(fill = 'x')
        return self.threadextraframe

    def setaccount(self, account):
        self.account = account
        self.mailbox = "Unknown"
        self.updateloclabel()

    def setmailbox(self, mailbox):
        self.mailbox = mailbox
        self.updateloclabel()

    def updateloclabel(self):
        self.loclabel['text'] = "Processing %s: %s" % (self.account,
                                                       self.mailbox)
    
    def appendmessage(self, newtext):
        self.message['text'] += "\n" + newtext

    def setmessage(self, newtext):
        self.message['text'] = newtext
        

class VerboseUI(UIBase):
    def isusable(s):
        global usabletest
        if usabletest != None:
            return usabletest

        try:
            Tk().destroy()
            usabletest = 1
        except TclError:
            usabletest = 0
        return usabletest
    
    def _createTopWindow(self, doidlevac = 1):
        self.notdeleted = 1
        self.created = threading.Event()

        self.af = {}
        self.aflock = Lock()

        t = threadutil.ExitNotifyThread(target = self._runmainloop,
                                        name = "Tk Mainloop")
        t.setDaemon(1)
        t.start()

        self.created.wait()
        del self.created

        if doidlevac:
            t = threadutil.ExitNotifyThread(target = self.idlevacuum,
                                            name = "Tk idle vacuum")
            t.setDaemon(1)
            t.start()

    def _runmainloop(s):
        s.top = Tk()
        s.top.title(version.productname + " " + version.versionstr)
        s.top.after_idle(s.created.set)
        s.top.mainloop()
        s.notdeleted = 0

    def getaccountframe(s):
        accountname = s.getthreadaccount()
        s.aflock.acquire()
        try:
            if accountname in s.af:
                return s.af[accountname]

            s.af[accountname] = LEDAccountFrame(s.top, accountname,
                                                s.fontfamily, s.fontsize)
        finally:
            s.aflock.release()
        return s.af[accountname]
    
    def getpass(s, accountname, config, errmsg = None):
        pd = PasswordDialog(accountname, config, errmsg = errmsg)
        return pd.getpassword()

    def gettf(s, newtype=ThreadFrame, master = None):
        if master == None:
            master = s.top
        threadid = thread.get_ident()
        s.tflock.acquire()
        try:
            if threadid in s.threadframes:
                return s.threadframes[threadid]
            if len(s.availablethreadframes):
                tf = s.availablethreadframes.pop(0)
                tf.setthread(currentThread())
            else:
                tf = newtype(master)
            s.threadframes[threadid] = tf
            return tf
        finally:
            s.tflock.release()

    def _display(s, msg):
        s.gettf().setmessage(msg)

    def threadExited(s, thread):
        threadid = thread.threadid
        s.tflock.acquire()
        if threadid in s.threadframes:
            tf = s.threadframes[threadid]
            tf.setthread(None)
            tf.setaccount("Unknown")
            tf.setmessage("Idle")
            s.availablethreadframes.append(tf)
            del s.threadframes[threadid]
        s.tflock.release()
        UIBase.threadExited(s, thread)

    def idlevacuum(s):
        while s.notdeleted:
            time.sleep(10)
            s.tflock.acquire()
            while len(s.availablethreadframes):
                tf = s.availablethreadframes.pop()
                tf.destroy()
            s.tflock.release()

    def terminate(s, exitstatus = 0, errortitle = None, errormsg = None):
        if errormsg <> None:
            if errortitle == None:
                errortitle = "Error"
            TextOKDialog(errortitle, errormsg)
        UIBase.terminate(s, exitstatus = exitstatus, errortitle = None, errormsg = None)
            
    def threadException(s, thread):
        exceptionstr = s.getThreadExceptionString(thread)
        print exceptionstr
    
        s.top.destroy()
        s.top = None
        TextOKDialog("Thread Exception", exceptionstr)
        s.delThreadDebugLog(thread)
        s.terminate(100)

    def mainException(s):
        exceptionstr = s.getMainExceptionString()
        print exceptionstr

        s.top.destroy()
        s.top = None
        TextOKDialog("Main Program Exception", exceptionstr)

    def warn(s, msg, minor = 0):
        if minor:
            # Just let the default handler catch it
            UIBase.warn(s, msg, minor)
        else:
            TextOKDialog("OfflineIMAP Warning", msg)

    def showlicense(s):
        TextOKDialog(version.productname + " License",
                     version.bigcopyright + "\n" +
                     version.homepage + "\n\n" + version.license,
                     blocking = 0, master = s.top)


    def init_banner(s):
        s.threadframes = {}
        s.availablethreadframes = []
        s.tflock = Lock()
        s._createTopWindow()
        s._msg(version.productname + " " + version.versionstr + ", " +\
               version.copyright)
        tf = s.gettf().getthreadextraframe()

        b = Button(tf, text = "About", command = s.showlicense)
        b.pack(side = LEFT)
        
        b = Button(tf, text = "Exit", command = s.terminate)
        b.pack(side = RIGHT)
        s.sleeping_abort = {}

    def deletingmessages(s, uidlist, destlist):
        ds = s.folderlist(destlist)
        s._msg("Deleting %d messages in %s" % (len(uidlist), ds))

    def _sleep_cancel(s, args = None):
        s.sleeping_abort[thread.get_ident()] = 1

    def sleep(s, sleepsecs):
        threadid = thread.get_ident()
        s.sleeping_abort[threadid] = 0
        tf = s.gettf().getthreadextraframe()

        def sleep_cancel():
            s.sleeping_abort[threadid] = 1

        sleepbut = Button(tf, text = 'Sync immediately',
                          command = sleep_cancel)
        sleepbut.pack()
        UIBase.sleep(s, sleepsecs)
        
    def sleeping(s, sleepsecs, remainingsecs):
        retval = s.sleeping_abort[thread.get_ident()]
        if remainingsecs:
            s._msg("Next sync in %d:%02d" % (remainingsecs / 60,
                                             remainingsecs % 60))
        else:
            s._msg("Wait done; synchronizing now.")
            s.gettf().destroythreadextraframe()
            del s.sleeping_abort[thread.get_ident()]
        time.sleep(sleepsecs)
        return retval

TkUI = VerboseUI

################################################## Blinkenlights

class LEDAccountFrame:
    def __init__(self, top, accountname, fontfamily, fontsize):
        self.top = top
        self.accountname = accountname
        self.fontfamily = fontfamily
        self.fontsize = fontsize
        self.frame = Frame(self.top, background = 'black')
        self.frame.pack(side = BOTTOM, expand = 1, fill = X)
        self._createcanvas(self.frame)

        self.label = Label(self.frame, text = accountname,
                           background = "black", foreground = "blue",
                           font = (self.fontfamily, self.fontsize))
        self.label.grid(sticky = E, row = 0, column = 1)

    def getnewthreadframe(s):
        return LEDThreadFrame(s.canvas)

    def _createcanvas(self, parent):
        c = LEDFrame(parent)
        self.canvas = c
        c.grid(sticky = E, row = 0, column = 0)
        parent.grid_columnconfigure(1, weight = 1)
        #c.pack(side = LEFT, expand = 0, fill = X)

    def startsleep(s, sleepsecs):
        s.sleeping_abort = 0
        s.button = Button(s.frame, text = "Sync now", command = s.syncnow,
                          background = "black", activebackground = "black",
                          activeforeground = "white",
                          foreground = "blue", highlightthickness = 0,
                          padx = 0, pady = 0,
                          font = (s.fontfamily, s.fontsize), borderwidth = 0,
                          relief = 'solid')
        s.button.grid(sticky = E, row = 0, column = 2)

    def syncnow(s):
        s.sleeping_abort = 1

    def sleeping(s, sleepsecs, remainingsecs):
        if remainingsecs:
            s.button.config(text = 'Sync now (%d:%02d remain)' % \
                            (remainingsecs / 60, remainingsecs % 60))
            time.sleep(sleepsecs)
        else:
            s.button.destroy()
            del s.button
        return s.sleeping_abort

class LEDFrame(Frame):
    """This holds the different lights."""
    def getnewobj(self):
        retval = Canvas(self, background = 'black', height = 20, bd = 0,
                        highlightthickness = 0, width = 10)
        retval.pack(side = LEFT, padx = 0, pady = 0, ipadx = 0, ipady = 0)
        return retval

class LEDThreadFrame:
    """There is one of these for each little light."""
    def __init__(self, master):
        self.canvas = master.getnewobj()
        self.color = ''
        self.ovalid = self.canvas.create_oval(4, 4, 9,
                                              9, fill = 'gray',
                                              outline = '#303030')

    def setcolor(self, newcolor):
        if newcolor != self.color:
            self.canvas.itemconfigure(self.ovalid, fill = newcolor)
            self.color = newcolor

    def getcolor(self):
        return self.color

    def setthread(self, newthread):
        if newthread:
            self.setcolor('gray')
        else:
            self.setcolor('black')


class Blinkenlights(BlinkenBase, VerboseUI):
    def __init__(s, config, verbose = 0):
        VerboseUI.__init__(s, config, verbose)
        s.fontfamily = 'Helvetica'
        s.fontsize = 8
        if config.has_option('ui.Tk.Blinkenlights', 'fontfamily'):
            s.fontfamily = config.get('ui.Tk.Blinkenlights', 'fontfamily')
        if config.has_option('ui.Tk.Blinkenlights', 'fontsize'):
            s.fontsize = config.getint('ui.Tk.Blinkenlights', 'fontsize')

    def isusable(s):
        return VerboseUI.isusable(s)

    def _createTopWindow(self):
        VerboseUI._createTopWindow(self, 0)
        #self.top.resizable(width = 0, height = 0)
        self.top.configure(background = 'black', bd = 0)

        widthmetric = tkFont.Font(family = self.fontfamily, size = self.fontsize).measure("0")
        self.loglines = self.config.getdefaultint("ui.Tk.Blinkenlights",
                                                  "loglines", 5)
        self.bufferlines = self.config.getdefaultint("ui.Tk.Blinkenlights",
                                                     "bufferlines", 500)
        self.text = ScrolledText(self.top, bg = 'black', #scrollbar = 'y',
                                 font = (self.fontfamily, self.fontsize),
                                 bd = 0, highlightthickness = 0, setgrid = 0,
                                 state = DISABLED, height = self.loglines,
                                 wrap = NONE, width = 60)
        self.text.vbar.configure(background = '#000050',
                                 activebackground = 'blue',
                                 highlightbackground = 'black',
                                 troughcolor = "black", bd = 0,
                                 elementborderwidth = 2)
                                 
        self.textenabled = 0
        self.tags = []
        self.textlock = Lock()

    def init_banner(s):
        BlinkenBase.init_banner(s)
        s._createTopWindow()
        menubar = Menu(s.top, activebackground = "black",
                       activeforeground = "white",
                       activeborderwidth = 0,
                       background = "black", foreground = "blue",
                       font = (s.fontfamily, s.fontsize), bd = 0)
        menubar.add_command(label = "About", command = s.showlicense)
        menubar.add_command(label = "Show Log", command = s._togglelog)
        menubar.add_command(label = "Exit", command = s.terminate)
        s.top.config(menu = menubar)
        s.menubar = menubar
        s.text.see(END)
        if s.config.getdefaultboolean("ui.Tk.Blinkenlights", "showlog", 1):
            s._togglelog()
        s.gettf().setcolor('red')
        s.top.resizable(width = 0, height = 0)
        s._msg(version.banner)

    def _togglelog(s):
        if s.textenabled:
            s.oldtextheight = s.text.winfo_height()
            s.text.pack_forget()
            s.textenabled = 0
            s.menubar.entryconfig('Hide Log', label = 'Show Log')
            s.top.update()
            s.top.geometry("")
            s.top.update()
            s.top.resizable(width = 0, height = 0)
            s.top.update()
        
        else: 
            s.text.pack(side = TOP, expand = 1, fill = BOTH)
            s.textenabled = 1
            s.top.update()
            s.top.geometry("")
            s.menubar.entryconfig('Show Log', label = 'Hide Log')
            s._rescroll()
            s.top.resizable(width = 1, height = 1)

    def sleep(s, sleepsecs):
        s.gettf().setcolor('red')
        s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60))
        BlinkenBase.sleep(s, sleepsecs)

    def sleeping(s, sleepsecs, remainingsecs):
        return BlinkenBase.sleeping(s, sleepsecs, remainingsecs)

    def _rescroll(s):
        s.text.see(END)
        lo, hi = s.text.vbar.get()
        s.text.vbar.set(1.0 - (hi - lo), 1.0)

    def _display(s, msg):
        if "\n" in msg:
            for thisline in msg.split("\n"):
                s._msg(thisline)
            return
        #VerboseUI._msg(s, msg)
        color = s.gettf().getcolor()
        rescroll = 1
        s.textlock.acquire()
        try:
            if s.text.vbar.get()[1] != 1.0:
                rescroll = 0
            s.text.config(state = NORMAL)
            if not color in s.tags:
                s.text.tag_config(color, foreground = color)
                s.tags.append(color)
            s.text.insert(END, "\n" + msg, color)

            # Trim down.  Not quite sure why I have to say 7 instead of 5,
            # but so it is.
            while float(s.text.index(END)) > s.bufferlines + 2.0:
                s.text.delete(1.0, 2.0)

            if rescroll:
                s._rescroll()
        finally:
            s.text.config(state = DISABLED)
            s.textlock.release()

    
