#!/usr/bin/python

""" Tkinter interface made for Live Coding with Python syntax highlighting """  

# Tkinter Interface
from Tkinter import *
import ttk
import tkFont
import tkFileDialog

# Custom app modules
from Format import *
from AppFunctions import *
from Console import console
from Undo import UndoStack
from Prompt import TextPrompt
from BracketHandler import BracketHandler
from TextBox import ThreadedText
from LineNumbers import LineNumbers

from ..Settings import FONT, FOXDOT_ICON
import os
import re

# Code execution
from ..Code import execute
 
# App object

class workspace:

    default_font = FONT
    namespace = {}

    def __init__(self, CodeClass):

        # Configure FoxDot's namespace to include the editor

        CodeClass.namespace['FoxDot'] = self
        #CodeClass.namespace['Player'].widget = self
        #CodeClass.namespace['Ghost'].widget = self

        # Used for docstring prompt
        
        self.namespace = CodeClass.namespace

        # Set up master widget  

        self.root = Tk()
        self.root.title("FoxDot - Live Coding with Python and SuperCollider")
        self.root.columnconfigure(1, weight=1)
        self.root.rowconfigure(0, weight=1)
        self.root.rowconfigure(1, weight=0)
        self.root.grid_columnconfigure(0, weight=0)
        self.root.protocol("WM_DELETE_WINDOW", self.kill )

        # Set FoxDot icon
        
        try:

            # Use .ico file by default
            self.root.iconbitmap(FOXDOT_ICON)
            
        except:

            # Use .gif if necessary
            self.root.tk.call('wm', 'iconphoto', self.root._w, PhotoImage(file=FOXDOT_ICON))

        # set font

        self.font = tkFont.Font(font=(self.default_font, 12), name="CodeFont")
        self.font.configure(**tkFont.nametofont("CodeFont").configure())

        # Create menu

        self.menu_visible = False

        self.menu = Menu(self.root)
        self.menu.config(font="CodeFont",
                         bg=colour_map['background'],
                         fg=colour_map['plaintext'])

        # self.root.config(menu=self.menu)
        
        self.menu.add_command(label="Open",       command=self.openfile)
        self.menu.add_command(label="Save",       command=self.save)
        self.menu.add_command(label="Save As",    command=self.save)
        self.menu.add_command(label="Settings",   command=lambda: None)
        self.menu.add_command(label="Help",       command=lambda: None)
       
        # Create Y scrollbar

        self.Yscroll = Scrollbar(self.root)
        self.Yscroll.grid(row=0, column=2, sticky='nsew')

        # Create text box for code

        self.text = ThreadedText(self.root,
                                 padx=5, pady=5,
                                 bg=colour_map['background'],
                                 fg=colour_map['plaintext'],
                                 insertbackground="White",
                                 font = "CodeFont",
                                 yscrollcommand=self.Yscroll.set,
                                 width=100, height=20, bd=0,
                                 maxundo=50 )

        self.text.grid(row=0, column=1, sticky="nsew")
        self.Yscroll.config(command=self.text.yview)
        self.text.focus_set()

        # Create box for line numbers

        self.linenumbers = LineNumbers(self.text, width=25,
                                       bg=colour_map['background'],
                                       bd=0, highlightthickness=0 )
        
        self.linenumbers.grid(row=0, column=0, sticky='nsew')
        

        # Docstring prompt label

        self.prompt = TextPrompt(self.text)        

        # Key bindings
        
        self.text.bind("<Return>",          self.newline)
        self.text.bind("<BackSpace>",       self.delete)
        self.text.bind("<Delete>",          self.delete2)
        self.text.bind("<Tab>",             self.tab)
        self.text.bind("<Key>",             self.keypress)
        
        # Use command key on Mac (Temporary)
        
        ctrl = "Command" if SYSTEM == MAC_OS else "Control"
            
        self.text.bind("<{}-Return>".format(ctrl),          self.exec_block)
        self.text.bind("<Alt-Return>",                      self.exec_line)
        self.text.bind("<Alt_L>",                           lambda event: "break")
        self.text.bind("<{}-a>".format(ctrl),               self.selectall)
        self.text.bind("<{}-period>".format(ctrl),          self.killall)
        self.text.bind("<Alt-period>".format(ctrl),         self.releaseNodes)
        self.text.bind("<{}-v>".format(ctrl),               self.paste)
        self.text.bind("<{}-bracketright>".format(ctrl),    self.indent)
        self.text.bind("<{}-bracketleft>".format(ctrl),     self.unindent)
        self.text.bind("<{}-equal>".format(ctrl),           self.zoom_in)
        self.text.bind("<{}-minus>".format(ctrl),           self.zoom_out)
        self.text.bind("<{}-z>".format(ctrl),               self.undo)
        self.text.bind("<{}-s>".format(ctrl),               self.save)
        self.text.bind("<{}-o>".format(ctrl),               self.openfile)
        self.text.bind("<{}-m>".format(ctrl),               self.toggleMenu)

        # Change ctrl+h on Mac (is used to close)

        if SYSTEM == MAC_OS:
                       
            self.text.bind("<{}-k>".format(ctrl), self.help)
            self.help_key = "K"

        else:

            self.text.bind("<{}-h>".format(ctrl), self.help)
            self.help_key = "H"

        # Toggle console button keybind

        try:
            
            self.text.bind("<{}-#>".format(ctrl), self.toggle_console)
            self.toggle_key = "#"
            
        except:
            
            self.text.bind("<{}-t>".format(ctrl), self.toggle_console)
            self.toggle_key = "T" 

        # Save feature variabes

        self.saved    = False
        self.file     = None
        self.filename = None

        self.bracketHandler = BracketHandler(self)

        # Set tag names and config for specific colours

        for tier in tag_weights:

            for tag_name in tier:

                self.text.tag_config(tag_name, foreground=colour_map[tag_name])

        # Create undo-stack

        self.undo_stack = UndoStack(self.text)

        # Create lable for console

        self.console = console(self.root, self.default_font)
        self.console_visible = True
        sys.stdout = self.console

        # Store original location of cursor
        self.origin = "origin"
        self.text.mark_set(self.origin, INSERT)
        self.text.mark_gravity(self.origin, LEFT)

        # Say Hello to the user

        print "Welcome to FoxDot! Press Ctrl+{} for help.".format(self.help_key)
        print "-----------------------------------------"

    def run(self):
        """ Starts the Tk mainloop for the master widget """
        try:
            self.root.mainloop()
        except (KeyboardInterrupt, SystemExit):
            execute("Clock.stop()")
            execute("Server.quit()")
        return

    def read(self):
        return self.text.get("1.0", END)

    """

        Key press commands
        ------------------

    """

    def keypress(self, event=None):
        """ Handles any keypress """

        # For non string characters, return normally

        if not event.char or isHex(event.char):

            self.inbrackets = False

            self.update_prompt()

            return

        # Add character to text box
        
        else:

            self.delete_selection()

            index = self.text.index(INSERT)

            self.text.insert(index, event.char)

            self.undo_stack.append_keystroke(index)

            self.update(event)

        return "break"

    """

        Getting blocks / lines

    """

    def exec_line(self, event=None, insert=INSERT):
        """ Highlights a single line and executes """
        line, column = index(self.text.index(insert))
        
        a, b = "%d.0" % line, "%d.end" % line

        self.highlight(a, b, "red")

        try:

            execute( self.text.get(a, b) )
            execute.update_line_numbers(self.text, a, b)

        except:

            pass

        self.root.after(200, self.unhighlight)

        return "break"

    def exec_block(self, event=None, insert=INSERT):
        """ Method to highlight block of code and execute """

        # Get start and end of the buffer
        start, end = "1.0", self.text.index(END)
        lastline   = int(end.split('.')[0]) + 1

        # Indicies of block to execute
        block = [0,0]        
        
        # 1. Get position of cursor
        #cursor = self.text.index(insert)
        #cur_x, cur_y   = (int(a) for a in cursor.split('.'))
        cur_x, cur_y = index(self.text.index(insert))
        
        # 2. Go through line by line (back) and see what it's value is
        
        for line in range(cur_x, 0, -1):
            if not self.text.get("%d.0" % line, "%d.end" % line).strip():
                break

        block[0] = line

        # 3. Iterate forwards until we get two \n\n or index==END
        for line in range(cur_x, lastline):
            if not self.text.get("%d.0" % line, "%d.end" % line).strip():
                break

        block[1] = line

        # Now we have the lines of code!

        a, b = block
        
        if a == b: b += 1

        for line in range(a, b):
            start = "%d.0" % line
            end   = "%d.end" % line

            # Highlight text only to last character, not whole line

            self.highlight(start, end)

        # Convert line numbers to Tkinter indices

        a, b = ("%d.0" % n for n in (a, b))

        # Execute the python code

        try:

            execute( self.text.get( a , b ) )
            execute.update_line_numbers(self.text, a, b)

        except:

            pass

        # Unhighlight the line of text

        self.root.after(200, self.unhighlight)

        return "break"

    # Scheduling tasks
    # ----------------
    def addTask(self, target, args=(), kwargs={}):
        self.text.queue.put((target, args, kwargs))
        return

    # Undo action: Ctrl+Z
    #--------------------

    def undo(self, event=None):
        """ Ctrl+z: Remove the last character pressed """

        if len(self.undo_stack) > 0:

            self.undo_stack.pop().action()
            self.update(event)
            
        return

    # Help feature: Ctrl+H
    #---------------------

    def help(self, event=None):

        if SYSTEM == MAC_OS:
            ctrl = "Cmd"
        else:
            ctrl = "Ctrl"
            
        print "FoxDot Help:"
        print "--------------------------------------------"
        print "{}+Return  : Execute code".format(ctrl)
        print "{}+.       : Stop all sound".format(ctrl)
        print "{}+=       : Increase font size".format(ctrl)
        print "{}+-       : Decrease font size".format(ctrl)
        print "{}+S       : Save your work".format(ctrl)
        print "{}+O       : Open a file".format(ctrl)
        print "{}+M       : Toggle the menu".format(ctrl)
        print "{}+{}       : Toggle console window".format(ctrl, self.toggle_key)
        print "--------------------------------------------"
        print "Please visit foxdot.org for more information"
        print "--------------------------------------------"
        return "break"

    # Save the current text: Ctrl+s
    #------------------------------

    def save(self, event=None):
        """ Saves the contents of the text editor """
        text = self.text.get("0.0",END)
        if not self.saved:
            self.filename = tkFileDialog.asksaveasfilename(defaultextension=".py")
        if self.filename is not None:
            with open(self.filename, 'w') as f:
                f.write("# {}\n".format(self.filename))
                f.write("from __future__ import division\n")
                f.write("from FoxDot import *\n\n")
                f.write(text)
                f.close()
                self.saved = True
                print "Save successful!"
        return

    # Open save

    def saveAs(self,event=None):
        text = self.text.get("0.0",END)
        self.filename = tkFileDialog.asksaveasfilename(defaultextension=".py")
        if self.filename is not None:
            with open(self.filename, 'w') as f:
                f.write(text)
                f.close()
                self.saved = True
                print "Save successful!"
        return

    # Open a file: Ctrl+o
    #--------------------

    def openfile(self, event=None):
        f = tkFileDialog.askopenfile()
        if f is None:
            return
        else:
            text = f.read()
            f.close()
            self.set_all(text)
        return

    # Toggle console: Ctrl+#
    #-----------------------------
    def toggle_console(self,event=None):
        if self.console_visible:
            self.console.hide()
            self.text.config(height=self.text.cget('height')+self.console.height)
            self.console_visible = False
        else:
            self.console.show()
            self.text.config(height=self.text.cget('height')-self.console.height)
            self.console_visible = True
        return

    def toggleMenu(self, event=None):
        self.root.config(menu=self.menu if not self.menu_visible else 0)
        self.menu_visible = not self.menu_visible
        return "break"
    
    def paste(self, event=None):
        """ Ctrl-V: Pastes any text and updates the IDE """
        # Insert the data from the clipboard            
        self.text.insert(self.text.index(INSERT), self.root.clipboard_get())

        # Update the IDE colours
        self.update_all()
        
        return "break"

    # Newline
    #--------

    def newline(self, event=None, insert=INSERT):
        """ Adds whitespace to newlines where necessary """

        # Remove any highlighted text

        self.delete_selection()

        # Get the text from this line

        i, j = index(self.text.index(insert))
        line = self.text.get("%d.0" % i, "%d.end" % i)

        # Add newline

        self.text.insert(index(i, j), "\n")

        # Update player line numbers

        execute.update_line_numbers(self.text)

        pos = 0 # amount of whitespace to add

        while True:

            # Case 1. Unindented or indented but empty

            if line.strip() == "": break

            # Case 2. Open Bracket

            pos = open_bracket(line)

            if pos: break

            # Case 2. Keyword with ending ':'

            pos = function(line)

            if pos: break

            # Case 3. Indented line

            pos = indented(line)

            if pos: break

            # Any other possibilities

            pos = 0
            
            break

        # Add the necessary whitespace

        self.text.insert(index(i+1,0), " " * pos )

        # Update the IDE colours

        self.update(event)
        
        return "break"

    # Tab
    #----

    def tab(self, event=None, insert=INSERT):
        """ Move selected text forward 4 spaces """
        try: # Move any selected lines forwards
            a, b = (index(a)[0] for a in (self.text.index(SEL_FIRST), self.text.index(SEL_LAST)))
            if a < b:
                self.indent(event)
                return "break"
            else:
                self.delete(event)
        except: 
            pass
        
        # Insert white space

        line, column = index(self.text.index(insert))
        
        self.text.insert(index(line, column), self.tabspace())

        # Update IDE

        self.update(event)
            
        return "break"

    # Indent: Ctrl+]
    #---------------

    def indent(self, event=None, insert=INSERT):
        """ Indent the current line or selected text """
        try:

            if not self.text_selected():
                a = index(self.text.index(INSERT))
                a = a[0],a[1]-1
                b = a[0],a[1]+1
                self.text.tag_add(SEL, index(*a), index(*b))

            sel_a = index(index(self.text.index(SEL_FIRST))[0],0)
            sel_b = index(index(self.text.index(SEL_LAST))[0],'end')
                
            start, end = (index(a) for a in (self.text.index(SEL_FIRST), self.text.index(SEL_LAST)))
            for row in range(start[0], end[0]+1):
                # Add intentation
                self.text.insert(index(row,0), self.tabspace())

            self.text.tag_add(SEL, sel_a, sel_b)

        except:

            pass
            
        return "break"

    # Un-inden: Ctrl+[
    #-----------------

    def unindent(self, event):
        """ Moves the current row or selected text back by 4 spaces """
        if not self.text_selected():
            a = index(self.text.index(INSERT))
            a = a[0],a[1]-1
            b = a[0],a[1]+1
            self.text.tag_add(SEL, index(*a), index(*b))

        sel_a = index(index(self.text.index(SEL_FIRST))[0],0)
        sel_b = index(index(self.text.index(SEL_LAST))[0],'end')
            
        start, end = (index(a) for a in (self.text.index(SEL_FIRST), self.text.index(SEL_LAST)))
        for row in range(start[0], end[0]+1):
            # Unindent
            line = self.text.get(index(row,0), index(row,'end'))
            for n, char in enumerate(line[:tabsize]):
                if char != " ":
                    break
            self.text.delete(index(row,0),index(row,n+1))
            #self.text.insert(index(row,0), self.tabspace())

        self.text.tag_add(SEL, sel_a, sel_b)    
        return "break"

    # Deletion
    #---------

    def delete(self, event=None, insert=INSERT):
        """ Deletes a character or selected area """
        # If there is a selected area, delete that

        if self.delete_selection():

            # Update player line numbers

            execute.update_line_numbers(self.text)

            return "break"

        # Handle delete in brackets

        if self.bracketHandler.delete():

            return "break"
    
        # Else, work out if there is a tab to delete
        
        line, column = index(self.text.index(insert))

        # If we are at the start of a line, delete that

        if column == 0:

            self.update(event)

            # Update player line numbers

            execute.update_line_numbers(self.text, start="%d.0" % (i-1), remove=int(i!=1))

            self.text.delete(index(line-1, END), insert)

        else:

            tab = index(line, column-tabsize)
        
            # Check if there's a tab
            
            if self.text.get(tab, insert) == self.tabspace():

                self.text.delete(tab, insert)

            else:

                self.text.delete(index(line, column-1), insert)
                
        # Update the IDE
        self.update(event)

        return "break"

    def delete2(self, event=None, insert=INSERT):
        """ Delete the next character """

        if not self.delete_selection():

            self.text.delete(self.text.index(insert))
            
        self.update(event)

        execute.update_line_numbers(self.text)

        return "break"

    def delete_selection(self):
        """ If an area is selected, it is deleted and returns True """
        try:
            text = self.text.get(SEL_FIRST, SEL_LAST)
            a, b = self.text.index(SEL_FIRST), self.text.index(SEL_LAST)
            self.text.delete(SEL_FIRST, SEL_LAST)
            self.undo_stack.append_delete(text, a, b)
            return True        
        except:
            return False

    def text_selected(self):
        """ Returns True if text is selected """
        try:
            self.text.index(SEL_FIRST)
            return True
        except:
            return False

    """

        Keyboard Shortcuts
        ==================

    """

    # Select all: Ctrl+a
    #-------------------

    def selectall(self, event):
        """ Ctrl-a: Select the contents of the IDE """
        self.text.tag_add(SEL, "1.0", END)
        self.text.mark_set(INSERT, "1.0")
        self.text.see(INSERT)
        return 'break'

    # Kill all: Ctrl+.
    #-----------------

    def killall(self, event):
        """ Stops all player objects """
        execute("Clock.clear()")
        return "break"

    # Zoom in: Ctrl+=
    #----------------

    def zoom_in(self, event):
        """ Ctrl+= increases text size """
        self.root.grid_propagate(False)
        font = tkFont.nametofont("CodeFont")
        size = font.actual()["size"]+2
        font.configure(size=size)
        # Increase size of line number
        self.linenumbers.config(width=self.linenumbers.winfo_width() + 3)
        return 'break'

    # Zoom out: Ctrl+-
    #-----------------

    def zoom_out(self, event):
        """ Ctrl+- decreases text size (minimum of 8) """
        self.root.grid_propagate(False)
        font = tkFont.nametofont("CodeFont")
        size = font.actual()["size"]-2
        if size >= 8:
            font.configure(size=size)
            self.linenumbers.config(width=self.linenumbers.winfo_width() - 3)
        return  'break'
    

    def submit(self, code_str):
        """ Runs the chunk of code through FoxDot processing and execute """
        try:
                
            execute( code_str )

        except Exception as e:

            print e

    def highlight(self, start, end, colour="Red"):
        """ Highlights an area of text """

        # Label block (start and end are the lines before and after the code itself)

        self.text.tag_add("code", start, end)

        # Highlight
        
        self.text.tag_config("code", background=colour, foreground="White")

        return

    def unhighlight(self):
        """ Creates thread to wait 0.2 seconds before removing any highlights from the text """

        # Remove labels

        self.text.tag_delete("code")

        return

    """

        Methods that view the FoxDot namespace
        --------------------------------------

    """
        

    def update_prompt(self):
        return

    def update_prompt2(self):        

        if self.inbrackets:

            # Show prompt

            line, column = index(self.text.index(INSERT))
            text = self.text.get(index(line, 0), index(line, column))

            x = self.font.measure(text)
            y = self.font.metrics("linespace") * line

            self.prompt.move(x, y)

            # If cursor is in between brackets that follow a type word

            try:

                self.check_namespace()

            except:

                pass

        else:

            # Hide prompt

            self.prompt.hide()

    def check_namespace(self):
        """ Sets the label """

        obj = self.namespace[self.last_word]

        if obj:

            if obj.__doc__ is not None:

                self.prompt.value.set(obj.__doc__)

            else:

                self.prompt.hide()

    """

        Methods that update the contents of the IDE
        -------------------------------------------

    """

    def update(self, event=None, insert=INSERT):
        """ Updates the the colours of the IDE """

        # Move the window to view the current line

        self.text.see(INSERT)

        # 1. Get the contents of the current line

        cur = self.text.index(insert)
        line, column = index(cur)

        self.colour_line(line)

        self.update_prompt()

        return "break"

    def update_all(self):
        """ Updates every line in the IDE """

        row, col = index(self.text.index(END))
        lines = row + 1

        for line in range(lines):

            self.colour_line(line)

        return

    def colour_line(self, line):
        """ Checks a line for any tags that match regex and updates IDE colours """

        start, end = index(line,0), index(line,"end")

        thisline = self.text.get(start, end)

        try:

            # Remove tags at current point

            for tag_name in self.text.tag_names():

                self.text.tag_remove(tag_name, start, end)

            # Re-apply tags

            for tag_name, start, end in findstyles(thisline):
                
                self.text.tag_add(tag_name, index(line, start), index(line, end))

        except Exception as e:

            print e
                    

    def get_last_word(self):

        line, column = index(self.text.index(INSERT))

        string = ""

        while True:

            char = self.text.get(index(line, column-1))

            if char.isalpha():

                string = char + string

            else:

                break

            column -= 1

            if column == 0:

                break

        self.last_word = string

    """
        Generic functions
        -----------------
        - Correct exiting
        - Tabspace (todo: customise)
        

    """

    def kill(self):
        """ Proper exit function """

        self.terminate()

        self.root.destroy()

        return
    
    def tabspace(self):
        return " " * tabsize

    def terminate(self):
        """ Called on window close. Ends Clock thread process """
        execute("Clock.stop()")
        execute("Server.quit()")
        return

    def releaseNodes(self, event=None):
        execute("Server.freeAllNodes()")
        return

    def replace(self, line, old, new):
        """ Replaces text on a specified line and updates the IDE """
        try:

            # Store cursor
            origin = self.text.index(INSERT)
            # Get contents of the line
            a = index(line, 0)
            b = index(line, END)
            contents = self.text.get(a, b)
            # Find the part to replace

            i = contents.index(old)
            j = i + len(old)
            a = index(line, i)
            b = index(line, j)
            
            self.text.delete(a, b)
            self.text.insert(a, new)

            self.update(insert=a)

            self.text.mark_set(INSERT, origin)

        except:

            return

    def replace_re(self, line, new=""):
        """ Replaces text on a specified line and updates the IDE """
        try:

            # Store cursor
            origin = self.text.index(INSERT)
            # Get contents of the line
            a = index(line, 0)
            b = index(line, END)
            contents = self.text.get(a, b)

            # Search using RegEx
            match = re_pat(contents)

            if match:

                # Find the part to replace
                i = match.start()
                j = match.end()
                a = index(line, i)
                b = index(line, j)
                
                self.text.delete(a, b)
                self.text.insert(a, new)

                self.update(insert=a)

                self.text.mark_set(INSERT, origin)

        except Exception as e:

            print e

            return

    def set_all(self, text):
        self.text.delete("1.0", END)
        self.text.insert("1.0", text.strip())
        self.update_all()
        return

    def get_all(self):
        return self.text.get("1.0", END)

    
