#!/usr/bin/env python3

import os
import re
import sys
from collections import Counter
from time import sleep
import threading

try:
        # Python 2.x imports
        import Tkinter as tk
        import ttk
        import tkFont
except ImportError:
        # Python 3.x imports
        import tkinter as tk
        from tkinter import ttk
        import tkinter.font as tkFont
                        
from pykms_Format import MsgMap, unshell_message, unformat_message

#------------------------------------------------------------------------------------------------------------------------------------------------------------

# https://stackoverflow.com/questions/3221956/how-do-i-display-tooltips-in-tkinter
class ToolTip(object):
        """ Create a tooltip for a given widget """
        def __init__(self, widget, bg = '#FFFFEA', pad = (5, 3, 5, 3), text = 'widget info', waittime = 400, wraplength = 250):
                self.waittime = waittime  # ms
                self.wraplength = wraplength  # pixels
                self.widget = widget
                self.text = text
                self.widget.bind("<Enter>", self.onEnter)
                self.widget.bind("<Leave>", self.onLeave)
                self.widget.bind("<ButtonPress>", self.onLeave)
                self.bg = bg
                self.pad = pad
                self.id = None
                self.tw = None
        
        def onEnter(self, event = None):
                self.schedule() 

        def onLeave(self, event = None):
                self.unschedule()
                self.hide()

        def schedule(self):
                self.unschedule()
                self.id = self.widget.after(self.waittime, self.show)

        def unschedule(self):
                id_ = self.id
                self.id = None
                if id_:
                        self.widget.after_cancel(id_)
        
        def show(self):
                def tip_pos_calculator(widget, label, tip_delta = (10, 5), pad = (5, 3, 5, 3)):
                    w = widget
                    s_width, s_height = w.winfo_screenwidth(), w.winfo_screenheight()
                    width, height = (pad[0] + label.winfo_reqwidth() + pad[2],
                                     pad[1] + label.winfo_reqheight() + pad[3])
                    mouse_x, mouse_y = w.winfo_pointerxy()
                    x1, y1 = mouse_x + tip_delta[0], mouse_y + tip_delta[1]
                    x2, y2 = x1 + width, y1 + height

                    x_delta = x2 - s_width
                    if x_delta < 0:
                            x_delta = 0
                    y_delta = y2 - s_height
                    if y_delta < 0:
                            y_delta = 0

                    offscreen = (x_delta, y_delta) != (0, 0)

                    if offscreen:
                        if x_delta:
                                x1 = mouse_x - tip_delta[0] - width
                        if y_delta:
                                y1 = mouse_y - tip_delta[1] - height

                    offscreen_again = y1 < 0  # out on the top

                    if offscreen_again:
                        # No further checks will be done.

                        # TIP:
                        # A further mod might automagically augment the
                        # wraplength when the tooltip is too high to be
                        # kept inside the screen.
                        y1 = 0

                    return x1, y1

                bg = self.bg
                pad = self.pad
                widget = self.widget

                # creates a toplevel window
                self.tw = tk.Toplevel(widget)

                # leaves only the label and removes the app window
                self.tw.wm_overrideredirect(True)

                win = tk.Frame(self.tw, background = bg, borderwidth = 0)
                label = ttk.Label(win, text = self.text, justify = tk.LEFT, background = bg, relief = tk.SOLID, borderwidth = 0,
                                  wraplength = self.wraplength)
                label.grid(padx = (pad[0], pad[2]), pady = (pad[1], pad[3]), sticky=tk.NSEW)
                win.grid()

                x, y = tip_pos_calculator(widget, label)

                self.tw.wm_geometry("+%d+%d" % (x, y))
                
        def hide(self):
                tw = self.tw
                if tw:
                    tw.destroy()
                self.tw = None

##-----------------------------------------------------------------------------------------------------------------------------------------------------------

class TextRedirect(object):
        class Pretty(object):
                grpmsg = unformat_message([MsgMap[1], MsgMap[7], MsgMap[12], MsgMap[20]])
                arrows = [ item[0] for item in grpmsg  ]
                clt_msg_nonewline = [ item[1] for item in grpmsg ]
                arrows = list(set(arrows))
                lenarrow = len(arrows[0])
                srv_msg_nonewline = [ item[0] for item in unformat_message([MsgMap[2], MsgMap[5], MsgMap[13], MsgMap[18]]) ]
                msg_align = [ msg[0].replace('\t', '').replace('\n', '') for msg in unformat_message([MsgMap[-2], MsgMap[-4]]) ]

                def __init__(self, srv_text_space, clt_text_space, customcolors):
                        self.srv_text_space = srv_text_space
                        self.clt_text_space = clt_text_space
                        self.customcolors = customcolors

                def textbox_write(self, tag, message, color, extras):
                        widget = self.textbox_choose(message)
                        self.w_maxpix, self.h_maxpix = widget.winfo_width(), widget.winfo_height()
                        self.xfont = tkFont.Font(font = widget['font'])
                        widget.configure(state = 'normal')
                        widget.insert('end', self.textbox_format(message), tag)
                        self.textbox_color(tag, widget, color, self.customcolors['black'], extras)
                        widget.after(100, widget.see('end'))
                        widget.configure(state = 'disabled')

                def textbox_choose(self, message):
                        if any(item.startswith('logsrv') for item in [message, self.str_to_print]):
                                self.srv_text_space.focus_set()
                                self.where = "srv"
                                return self.srv_text_space
                        elif any(item.startswith('logclt') for item in [message, self.str_to_print]):
                                self.clt_text_space.focus_set()
                                self.where = "clt"
                                return self.clt_text_space

                def textbox_color(self, tag, widget, forecolor = 'white', backcolor = 'black', extras = []):
                        for extra in extras:
                                if extra == 'bold':
                                        self.xfont.configure(weight = "bold")
                                elif extra == 'italic':
                                        self.xfont.configure(slant = "italic")
                                elif extra == 'underlined':
                                        self.xfont.text_font.configure(underline = True)
                                elif extra == 'strike':
                                        self.xfont.configure(overstrike = True)
                                elif extra == 'reverse':
                                        forecolor, backcolor = backcolor, forecolor

                        widget.tag_configure(tag, foreground = forecolor, background = backcolor, font = self.xfont)
                        widget.tag_add(tag, "insert linestart", "insert lineend")

                def textbox_newline(self, message):
                        if not message.endswith('\n'):
                                return message + '\n'
                        else:
                                return message

                def textbox_format(self, message):
                        # vertical align.
                        self.w_maxpix = self.w_maxpix - 5 # pixel reduction for distance from border.
                        w_fontpix, h_fontpix = (self.xfont.measure('0'), self.xfont.metrics('linespace'))
                        msg_unformat = message.replace('\t', '').replace('\n', '')
                        lenfixed_chars = int((self.w_maxpix / w_fontpix) - len(msg_unformat))

                        if message in self.srv_msg_nonewline + self.clt_msg_nonewline:
                                lung = lenfixed_chars - self.lenarrow
                                if message in self.clt_msg_nonewline:
                                        message = self.textbox_newline(message)
                        else:
                                lung = lenfixed_chars
                                if (self.where == "srv") or (self.where == "clt" and message not in self.arrows):
                                         message = self.textbox_newline(message)
                                # horizontal align.
                                if msg_unformat in self.msg_align:
                                        msg_strip = message.lstrip('\n')
                                        message = '\n' * (len(message) - len(msg_strip) + TextRedirect.Pretty.newlinecut[0]) + msg_strip
                                        TextRedirect.Pretty.newlinecut.pop(0)

                        count = Counter(message)
                        countab = (count['\t'] if count['\t'] != 0 else 1)
                        message = message.replace('\t' * countab, ' ' * lung)
                        return message

                def textbox_do(self):
                        msgs, TextRedirect.Pretty.tag_num = unshell_message(self.str_to_print, TextRedirect.Pretty.tag_num)
                        for tag in msgs:
                                self.textbox_write(tag, msgs[tag]['text'], self.customcolors[msgs[tag]['color']], msgs[tag]['extra'])

                def flush(self):
                        pass

                def write(self, string):
                        if string != '\n':
                                self.str_to_print = string
                                self.textbox_do()

        class Stderr(Pretty):
                def __init__(self, srv_text_space, clt_text_space, customcolors, side):
                        self.srv_text_space = srv_text_space
                        self.clt_text_space = clt_text_space
                        self.customcolors = customcolors
                        self.side = side
                        self.tag_err = 'STDERR'
                        self.xfont = tkFont.Font(font = self.srv_text_space['font'])

                def textbox_choose(self, message):
                        if self.side == "srv":
                                return self.srv_text_space
                        elif self.side == "clt":
                                return self.clt_text_space
                                                
                def write(self, string):
                        widget = self.textbox_choose(string)
                        self.textbox_color(self.tag_err, widget, self.customcolors['red'], self.customcolors['black'])
                        self.srv_text_space.configure(state = 'normal')
                        self.srv_text_space.insert('end', string, self.tag_err)
                        self.srv_text_space.see('end')
                        self.srv_text_space.configure(state = 'disabled')

        class Log(Pretty):
                def textbox_format(self, message):
                        if message.startswith('logsrv'):
                                message = message.replace('logsrv ', '')
                        if message.startswith('logclt'):
                                message = message.replace('logclt ', '')
                        return message + '\n'
                
##-----------------------------------------------------------------------------------------------------------------------------------------------------------
class TextDoubleScroll(tk.Frame): 
        def __init__(self, master, **kwargs):
                """ Initialize.
                        - horizontal scrollbar
                        - vertical scrollbar
                        - text widget
                """
                tk.Frame.__init__(self, master)
                self.master = master
                
                self.textbox = tk.Text(self.master, **kwargs)
                self.sizegrip = ttk.Sizegrip(self.master)
                self.hs = ttk.Scrollbar(self.master, orient = "horizontal", command = self.on_scrollbar_x)
                self.vs = ttk.Scrollbar(self.master, orient = "vertical", command = self.on_scrollbar_y)
                self.textbox.configure(yscrollcommand = self.on_textscroll, xscrollcommand = self.hs.set)

        def on_scrollbar_x(self, *args):
                """ Horizontally scrolls text widget. """
                self.textbox.xview(*args)

        def on_scrollbar_y(self, *args):
                """ Vertically scrolls text widget. """
                self.textbox.yview(*args)
        
        def on_textscroll(self, *args):
                """ Moves the scrollbar and scrolls text widget when the mousewheel is moved on a text widget. """
                self.vs.set(*args)
                self.on_scrollbar_y('moveto', args[0])
        
        def put(self, **kwargs):
                """ Grid the scrollbars and textbox correctly. """
                self.textbox.grid(row = 0, column = 0, padx = 3, pady = 3, sticky = "nsew")
                self.vs.grid(row = 0, column = 1, sticky = "ns")
                self.hs.grid(row = 1, column = 0, sticky = "we")
                self.sizegrip.grid(row = 1, column = 1, sticky = "news")
                
        def get(self):
                """ Return the "frame" useful to place inner controls. """
                return self.textbox

##-----------------------------------------------------------------------------------------------------------------------------------------------------------
def custom_background(window):
        # first level canvas.
        allwidgets = window.grid_slaves(0,0)[0].grid_slaves() + window.grid_slaves(0,0)[0].place_slaves()
        widgets_alphalow = [ widget for widget in allwidgets if widget.winfo_class() == 'Canvas']
        widgets_alphahigh = []
        # sub-level canvas.
        for side in ["Srv", "Clt"]:
                widgets_alphahigh.append(window.pagewidgets[side]["BtnWin"])
                for position in ["Left", "Right"]:
                        widgets_alphahigh.append(window.pagewidgets[side]["AniWin"][position])
                for pagename in window.pagewidgets[side]["PageWin"].keys():
                        widgets_alphalow.append(window.pagewidgets[side]["PageWin"][pagename])
        
        try:
                from PIL import Image, ImageTk

                # Open Image.
                img = Image.open(os.path.dirname(os.path.abspath( __file__ )) + "/graphics/pykms_Keys.gif")
                img = img.convert('RGBA')
                # Resize image.
                img.resize((window.winfo_width(), window.winfo_height()), Image.ANTIALIAS)
                # Put semi-transparent background chunks.
                window.backcrops_alphalow, window.backcrops_alphahigh = ([] for _ in range(2))

                def cutter(master, image, widgets, crops, alpha):
                        for widget in widgets:
                                x, y, w, h = master.get_position(widget)
                                cropped = image.crop((x, y, x + w, y + h))
                                cropped.putalpha(alpha)
                                crops.append(ImageTk.PhotoImage(cropped))
                        # Not in same loop to prevent reference garbage.
                        for crop, widget in zip(crops, widgets):
                                widget.create_image(1, 1, image = crop, anchor = 'nw')

                cutter(window, img, widgets_alphalow, window.backcrops_alphalow, 36)
                cutter(window, img, widgets_alphahigh, window.backcrops_alphahigh, 96)
                        
                # Put semi-transparent background overall.
                img.putalpha(128)
                window.backimg = ImageTk.PhotoImage(img)
                window.masterwin.create_image(1, 1, image = window.backimg, anchor = 'nw')
                
        except ImportError:
                for widget in widgets_alphalow + widgets_alphahigh:
                        widget.configure(background = window.customcolors['lavender'])

        # Hide client.
        window.clt_on_show(force_remove = True)
        # Show Gui.
        window.deiconify()

##-----------------------------------------------------------------------------------------------------------------------------------------------------------
class Animation(object):
        def __init__(self, gifpath, master, widget, loop = False):
                from PIL import Image, ImageTk, ImageSequence

                self.master = master
                self.widget = widget
                self.loop = loop
                self.cancelid = None
                self.flagstop = False
                self.index = 0
                self.frames = []

                img = Image.open(gifpath)
                size = img.size
                for frame in ImageSequence.Iterator(img):
                        static_img = ImageTk.PhotoImage(frame.convert('RGBA'))
                        try:
                                static_img.delay = int(frame.info['duration'])
                        except KeyError:
                                static_img.delay = 100
                        self.frames.append(static_img)

                self.widget.configure(width = size[0], height = size[1])
                self.initialize()

        def initialize(self):
                self.widget.configure(image = self.frames[0])
                self.widget.image = self.frames[0]

        def deanimate(self):
                while not self.flagstop:
                        pass
                self.flagstop = False
                self.index = 0
                self.widget.configure(relief = "raised")

        def animate(self):
                frame = self.frames[self.index]
                self.widget.configure(image = frame, relief = "sunken")
                self.index += 1
                self.cancelid = self.master.after(frame.delay, self.animate)
                if self.index == len(self.frames):
                        if self.loop:
                                self.index = 0
                        else:
                                self.stop()

        def start(self, event = None):
                if str(self.widget['state']) != 'disabled':
                        if self.cancelid is None:
                                if not self.loop:
                                        self.btnani_thread = threading.Thread(target = self.deanimate, name = "Thread-BtnAni")
                                        self.btnani_thread.setDaemon(True)
                                        self.btnani_thread.start()
                                self.cancelid = self.master.after(self.frames[0].delay, self.animate)

        def stop(self, event = None):
                if self.cancelid:
                        self.master.after_cancel(self.cancelid)
                        self.cancelid = None
                        self.flagstop = True
                        self.initialize()


def custom_pages(window, side):
        buttons = window.pagewidgets[side]["BtnAni"]
        labels = window.pagewidgets[side]["LblAni"]
        
        for position in buttons.keys():
                buttons[position].config(anchor = "center",
                                         font = window.btnwinfont,
                                         background = window.customcolors['white'],
                                         activebackground = window.customcolors['white'],
                                         borderwidth = 2)

                try:
                        anibtn = Animation(os.path.dirname(os.path.abspath( __file__ )) + "/graphics/pykms_Keyhole_%s.gif" %position,
                                           window, buttons[position], loop = False)
                        anilbl = Animation(os.path.dirname(os.path.abspath( __file__ )) + "/graphics/pykms_Arrow_%s.gif" %position,
                                           window, labels[position], loop = True)

                        def animationwait(master, button, btn_animation, lbl_animation):
                                while btn_animation.cancelid:
                                        pass
                                sleep(1)
                                x, y = master.winfo_pointerxy()
                                if master.winfo_containing(x, y) == button:
                                        lbl_animation.start()

                        def animationcombo(master, button, btn_animation, lbl_animation):
                                wait_thread = threading.Thread(target = animationwait,
                                                               args = (master, button, btn_animation, lbl_animation),
                                                               name = "Thread-WaitAni")
                                wait_thread.setDaemon(True)
                                wait_thread.start()
                                lbl_animation.stop()
                                btn_animation.start()

                        buttons[position].bind("<ButtonPress>", lambda event, anim1 = anibtn, anim2 = anilbl,
                                               bt = buttons[position], win = window:
                                               animationcombo(win, bt, anim1, anim2))
                        buttons[position].bind("<Enter>", anilbl.start)
                        buttons[position].bind("<Leave>", anilbl.stop)

                except ImportError:
                        buttons[position].config(activebackground = window.customcolors['blue'],
                                                 foreground = window.customcolors['blue'])
                        labels[position].config(background = window.customcolors['lavender'])

                        if position == "Left":
                                buttons[position].config(text = '<<')
                        elif position == "Right":
                                buttons[position].config(text = '>>')

##-----------------------------------------------------------------------------------------------------------------------------------------------------------
class ListboxOfRadiobuttons(tk.Frame):
        def __init__(self, master, radios, font, changed, **kwargs):
                tk.Frame.__init__(self, master)

                self.master = master
                self.radios = radios
                self.font = font
                self.changed = changed

                self.scrollv = tk.Scrollbar(self, orient = "vertical")
                self.textbox = tk.Text(self, yscrollcommand = self.scrollv.set, **kwargs)
                self.scrollv.config(command = self.textbox.yview)
                # layout.
                self.scrollv.pack(side = "right", fill = "y")
                self.textbox.pack(side = "left", fill = "both", expand = True)
                # create radiobuttons.
                self.radiovar = tk.StringVar()
                self.radiovar.set('FILE')
                self.create()

        def create(self):
                self.rdbtns = []
                for n, nameradio in enumerate(self.radios):
                        rdbtn = tk.Radiobutton(self, text = nameradio, value = nameradio, variable = self.radiovar,
                                               font = self.font, indicatoron = 0, width = 15,
                                               borderwidth = 3, selectcolor = 'yellow', command = self.change)
                        self.textbox.window_create("end", window = rdbtn)
                        # to force one checkbox per line
                        if n != len(self.radios) - 1:
                                self.textbox.insert("end", "\n")
                        self.rdbtns.append(rdbtn)
                self.textbox.configure(state = "disabled")

        def change(self):
                st = self.state()
                for widget, default in self.changed:
                        wclass = widget.winfo_class()
                        if st in ['STDOUT', 'FILEOFF']:
                                if wclass == 'Entry':
                                        widget.delete(0, 'end')
                                        widget.configure(state = "disabled")
                                elif wclass == 'TCombobox':
                                        if st == 'STDOUT':
                                                widget.set(default)
                                                widget.configure(state = "readonly")
                                        elif st == 'FILEOFF':
                                                widget.set('')
                                                widget.configure(state = "disabled")
                        elif st in ['FILE', 'FILESTDOUT', 'STDOUTOFF']:
                                if wclass == 'Entry':
                                        widget.configure(state = "normal")
                                        widget.delete(0, 'end')
                                        widget.insert('end', default)
                                        widget.xview_moveto(1)
                                elif wclass == 'TCombobox':
                                        widget.configure(state = "readonly")
                                        widget.set(default)
                                elif wclass == 'Button':
                                        widget.configure(state = "normal")

        def configure(self, state):
                for rb in self.rdbtns:
                        rb.configure(state = state)

        def state(self):
                return self.radiovar.get()