'''
Started 10 March 2016
Python script with GUI, to complement Accurev CLI ...
... mainly for sorting results and selecting items to process.
This was created out of the need to mitigate the slowness of AccuRev GUI ...
... by leveraging the faster and detailed CLI responses.
'''

# to do? http://stackoverflow.com/questions/6548837/how-do-i-get-an-event-callback-when-a-tkinter-entry-widget-is-modified link to activate custom when field changed manually?
# to do, update button?

import Tkinter
import subprocess

import tkFont
import ttk

import re
import xml.etree.ElementTree as ET

import socket
import os

import sys #
import traceback #

from datetime import datetime

window=None
commandField1=None
commandField2=None
resultListView=None

defaultCommandOptionStr="List modified elements"
defaultCommandOptionValStr='accurev stat -f x -m'

commandOptionsDict = {defaultCommandOptionStr:defaultCommandOptionValStr,
                      "List kept":'accurev stat -f x -k',
                      "List pending":'accurev stat -f x -p',
                      "List overlapping":'accurev stat -f x -o',
                      "List external":'accurev stat -f x -x',
                      "List missing":'accurev stat -f x -M',
					  "List files status for the folder":'accurev stat -f x -s <stream like Auth_BLR_Pepper_InternalEvents_Lane> <path like db\appdbs\auth30\cdata\*>',
                      "List default group":'accurev stat -f x -d',
                      "List default group, Parent Stream":'accurev stat -f x -d -b',
                      "List default group, Custom Stream":'accurev stat -f x -d -s <stream>',
                      "Update preview":'accurev update -i -f x', #response doesn't return status... maybe one of the reason GUI is slow... it fetches status one by one?
                      "Custom command":'accurev <cmd>'}

defaultCommandOptionStr2="Keep selected"
defaultCommandOptionValStr2='accurev keep -c ""'

commandOptionsDict2 = {defaultCommandOptionStr2:defaultCommandOptionValStr2,
                      "Diff against backed":"accurev diff -b -G",
                      "Diff against backed (custom stream)":"accurev diff -b -v <stream> -G",
                      "Revert selected to backed":'accurev purge',
                      "Promote selected":'accurev promote -c ""',
                      "Update":"accurev update",
                      "Show status of selected (console o/p)": "accurev stat -f x",
                      "Custom command":'accurev <cmd>'}                   
                      
def main():
    global window
    global commandField1
    global commandField2
    global resultListView
    global defaultCommandOptionStr
    global commandOptionsDict
    global defaultCommandOptionStr2
    global commandOptionsDict2
    
    #using accurev, detect the workspace folder for the system and cd to that folder
    changeToWspaceDir()
    
    window=Tkinter.Tk()
    window.title('AccuRev Assistant')
    window.lift()
    window.attributes('-topmost', True)
    window.attributes('-topmost', False)
    
    label1Text = 'Select a command from list, or type in the field...\nThen press enter or the button to enlist elements...'
    label1 = ttk.Label(window, wraplength="4i", justify="left", anchor="n", padding=(10, 2, 10, 6), text=label1Text)#, background="DarkSeaGreen2" , font="default 9 bold")
    label1.pack(fill='x')
    
    #>>>
    container1=ttk.Frame(window)
    container1.pack()
    
    defaultCommandOption=Tkinter.StringVar(container1)
    defaultCommandOption.set(defaultCommandOptionStr)
    commandOptions=commandOptionsDict.keys()
    commandMenu=Tkinter.OptionMenu(container1, defaultCommandOption, *commandOptions, command=fillCommandField1)
    commandMenu.config(bg="DarkSeaGreen2")
    commandMenu.pack(side=Tkinter.LEFT, padx=10)
    
    commandField1=Tkinter.Entry(container1, bg="DarkSeaGreen1")
    commandField1.bind('<Return>', inputEntry1Action)
    commandField1.pack(side=Tkinter.LEFT, padx=10)
    
    button1=Tkinter.Button(container1, text="Execute", command=inputEntry1Action, bg="SeaGreen3")
    button1.pack(side=Tkinter.LEFT, padx=10)
    #<<<
        
    resultListView=MultiColumnListbox()
    
    label2Text = 'Select elements and apply operation...\nWhere required, insert comments inside the quotes...'
    label2 = ttk.Label(window, wraplength="4i", justify="left", anchor="n", padding=(10, 2, 10, 6), text=label2Text)
    label2.pack(fill='x')
    
    #>>>
    container2=ttk.Frame(window)
    container2.pack()
    
    defaultCommandOption2=Tkinter.StringVar(container2)
    defaultCommandOption2.set(defaultCommandOptionStr2)
    commandOptions2=commandOptionsDict2.keys()
    commandMenu2=Tkinter.OptionMenu(container2, defaultCommandOption2, *commandOptions2, command=fillCommandField2)
    commandMenu2.config(bg="DarkSlateGray2")
    commandMenu2.pack(side=Tkinter.LEFT, padx=10)
    
    commandField2=Tkinter.Entry(container2, bg="DarkSlateGray1")
    #commandField2.bind('<Return>', button2Action)
    #disabled Enter key binding to avoid rash commands for the final step
    commandField2.pack(side=Tkinter.LEFT, padx=10)
    
    button2=Tkinter.Button(container2, text="Confirm and Execute", command=button2Action, bg="RoyalBlue1")
    button2.pack(side=Tkinter.LEFT, padx=10)
    #<<<
    
    fillCommandField1(defaultCommandOptionStr)
    fillCommandField2(defaultCommandOptionStr2)
    Tkinter.mainloop()

def changeToWspaceDir():    
    print '\nTrying to detect and change to your workspace directory...'
    getWspaceCmd='accurev show wspaces -f x'
    try:
        #funny how the absence of the second parameter blanks out e.output in Exception
        wspaceResult=subprocess.check_output(getWspaceCmd, stderr=subprocess.STDOUT)
    except Exception as e:
        print 'Error...'
        print e.output
        if(re.search(r'Not authenticated', e.output)!=None) or (re.search(r'Expired', e.output)!=None):
            print 'Login...'
            subprocess.call('accurev login')
            wspaceResult=subprocess.check_output(getWspaceCmd)
        else:
            print 'Quitting'
            quit(1)
    
    print wspaceResult
    wspaceDir='.'
    
    treeRoot=ET.fromstring(wspaceResult)
    for child in treeRoot:
        if child.tag.lower() == 'element':
            print "child.attrib['Host'].lower()::"+child.attrib['Host'].lower()
            print 'socket.getfqdn().lower()::'+socket.getfqdn().lower()
            if(re.search(socket.getfqdn().lower(),child.attrib['Host'].lower())!=None) or (re.search(child.attrib['Host'].lower(),socket.getfqdn().lower())!=None): #changed from equals comparison after accurev update (child.attrib['Host'].lower()==socket.getfqdn().lower())
                wspaceDir=child.attrib['Storage']
                break
    
    print wspaceDir
    os.chdir(wspaceDir)

def inputEntry1Action(event=None):
    global commandField1

    print '>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>'
    print 'Command 1 Started...'
    print '>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>'
    command1=commandField1.get()
    ###Maybe check and just use .call() for commands not requiring parsing?
    commandOutputStr=subprocess.check_output(command1)
    print commandOutputStr
    print '<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<'
    print 'Command 1 Complete'
    ###Remove below call if using .call()
    if(re.search(r'<AcResponse',commandOutputStr, re.IGNORECASE)!=None):
        parseAndFillList(commandOutputStr)
    print '<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<';

def button2Action(event=None):
    global commandField2
    
    print '>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>'
    print 'Command 2 Started...'
    print '>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>'
    command2=commandField2.get()
    getSelectedAndCall(command2)
    print '<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<'
    print 'Command 2 Complete'
    print '<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<'
    
def fillCommandField1(option):
    global commandField1
    global commandOptionsDict
    
    commandField1.delete(0,Tkinter.END)
    commandField1.insert(0,commandOptionsDict[option])
    
def fillCommandField2(option):
    global commandField2
    global commandOptionsDict2
    
    commandField2.delete(0,Tkinter.END)
    commandField2.insert(0,commandOptionsDict2[option])
    
# the test data ...
my_headers = ['location', 'modTime', 'status', 'namedVersion']
my_list = [
('row1', 'you') ,
('row2', 'can') ,
('row3', 'sort'),
('row4', 'me')
]

###todo
#pass the headings as well as the items to populate the new list
def parseAndFillList(dataListStr):
    global resultListView
    global my_headers
    
    print 'Parsing Result List...'
    list=[]
    headers=my_headers
    timeFormat="%d %B %Y %H:%M:%S"
    messageStr='Not Available'
    
    try:
        treeRoot=ET.fromstring(dataListStr)
        for child in treeRoot:
            if child.tag.lower() == 'message': # for update preview
                messageStr=child.text
                
            elif child.tag.lower() == 'element':
            
                filePath=matchKeyAndGetVal(child, 'location') #child.attrib['location']
                
                temp=matchKeyAndGetVal(child, 'modTime')
                
                try:
                    temp=float(temp)
                except ValueError:
                    print "Not a float. modTime N/A."
                    temp=None
                    
                if(temp):
                    time = str(temp)+' = '+datetime.fromtimestamp( temp ).strftime(timeFormat)
                else:
                    time='N/A'
                
                status=matchKeyAndGetVal(child, 'status') #child.attrib['status']
                
                if status == 'N/A':
                    status=messageStr # for update preview
                
                namedVersion=matchKeyAndGetVal(child, 'namedVersion') #child.attrib['namedVersion']
                
                list.append( (filePath, time, status, namedVersion) )
        
        #print list
        #need change here to call specific code to change the columns only instead of the below costlier call. P.S. doesn't work well either - creates extra box, column re-linking doesn't happen
        #resultListView._setup_widgets(titles=headers)
        resultListView._build_tree(titles=headers, items=list)
        
        print 'Parsing Done'
        
    except Exception as e:
        print "Error. Couldn't finish parsing..."
        print e
        traceback.print_exc() #
        #sys.exc_info()

def matchKeyAndGetVal(dict, keyPattern):
    for key in dict.keys():
        if(re.search(keyPattern, key, re.IGNORECASE)!=None):
            return dict.attrib[key]
    return 'N/A'
        
def getSelectedAndCall(commandStr):
    global resultListView
    print 'Appending Selected Items to the Command...'
    appendStr=commandStr
    for resultItem in resultListView.tree.selection():
        #resultItem is a coded identifier for the whole row
        temp = resultListView.tree.set(resultItem)
        #print temp[temp.keys()[0]] # using first column as the one contaning file name
        #appendStr=appendStr+' '+temp[temp.keys()[0]]
        print temp['location']
        appendStr=appendStr+' \"'+temp['location']+'\"'
    print appendStr
    subprocess.call(appendStr)
    print 'Executing Command...'
    
### Ref. http://stackoverflow.com/questions/5286093/display-listbox-with-columns-using-tkinter >>>
"""use a ttk.TreeView as a multicolumn ListBox"""
class MultiColumnListbox(object):

    def __init__(self):
        self.tree = None
        self._setup_widgets()
        #disabled loading of test data
        #self._build_tree()

    def _setup_widgets(self, titles=my_headers):
        global window
        
        container = ttk.Frame(window, width=1600, height=500) #ref. .grid_propagate(0) http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/ttk-Frame.html
        container.grid_propagate(0)
        container.pack(fill='both', expand=True)
        # create a treeview with dual scrollbars
        self.tree = ttk.Treeview(columns=titles, show="headings")
        vsb = ttk.Scrollbar(orient="vertical",
            command=self.tree.yview)
        hsb = ttk.Scrollbar(orient="horizontal",
            command=self.tree.xview)
        self.tree.configure(yscrollcommand=vsb.set,
            xscrollcommand=hsb.set)
        self.tree.grid(column=0, row=0, sticky='nsew', in_=container)
        vsb.grid(column=1, row=0, sticky='ns', in_=container)
        hsb.grid(column=0, row=1, sticky='ew', in_=container)
        container.grid_columnconfigure(0, weight=1)
        container.grid_rowconfigure(0, weight=1)

    def _build_tree(self, titles=my_headers, items=my_list):
        self.tree.delete(*(self.tree.get_children()))
        
        max_width=729
        
        for col in titles:
            self.tree.heading(col, text=col.title(),
                command=lambda c=col: sortby(self.tree, c, 0))
            # adjust the column's width to the header string
            self.tree.column(col,
                width=tkFont.Font().measure(col.title()))

        for item in items:
            self.tree.insert('', 'end', values=item)
            # adjust column's width if necessary to fit each value
            for ix, val in enumerate(item):
                col_w = tkFont.Font().measure(val)
                if col_w>max_width: # limit width
                    col_w=max_width
                if self.tree.column(titles[ix],width=None)<col_w:
                    self.tree.column(titles[ix], width=col_w)
        
        sortby(self.tree, titles[1], 1)

def sortby(tree, col, descending):
    """sort tree contents when a column header is clicked on"""
    # grab values to sort
    data = [(tree.set(child, col), child) \
        for child in tree.get_children('')]
    # if the data to be sorted is numeric change to float
    #data =  change_numeric(data)
    # now sort the data in place
    data.sort(reverse=descending)
    for ix, item in enumerate(data):
        tree.move(item[1], '', ix)
    # switch the heading so it will sort in the opposite direction
    tree.heading(col, command=lambda col=col: sortby(tree, col, \
        int(not descending)))

### <<<
        
main()