"""
#! 
ACESui.py

This provides the Graphical User Interface (GUI) for the ACES catalog.  It runs in
the System tray (Notification Area) and includes an XMLRPC Server that listens for
commands to popup on the screen.  This software only runs on Windows systems.

Normally it takes NO arguments.  You can run it directly (in debug mode) with the command:

ACESui -d ???????????

"""

try:
    import gtk
except RuntimeError:
    print ("Runtime Error: Unable to initialize graphical environment.")
    raise SystemExit
import os
import webbrowser
import socket
import BaseHTTPServer
from socket import error
from SimpleXMLRPCServer import SimpleXMLRPCServer
import gobject
from urllib import urlencode
from time import sleep
import simpleConfig
import win32gui
import wmi
import admin
import getpass
import platform
from subprocess import Popen, PIPE


# some constants
REVISION = "1.10.04"
TITLE_BAR = "AutoPoint ACES Catalog (rev. %s)" % REVISION
ICONS = "icons"
CLIENT_PORT= 8020
USER = getpass.getuser()
if platform.release() == "XP":
    CONFIG_FILE = r'C:\Documents and Settings\%s\aces.conf' % USER
else:
    CONFIG_FILE = r'C:\Users\%s\aces.conf' % USER
VALID_OPTS = ('port', 'server', 'browser')
# NOTE: had to hard wire this because there's no consistency
# on what is returned on different versions of Windows!
#AMADOR_PATH = os.path.join(os.environ['PROGRAMFILES'], "Amador")
AMADOR_PATH = os.path.join("C:/Program Files", "Amador")
ACES_ICON = os.path.join(AMADOR_PATH, ICONS, "acesicon.png")
#ACES_ICON = os.path.join(AMADOR_PATH, ICONS, "acesicon.ico")
ACES_PATH = os.path.join(AMADOR_PATH, "aces")
#ACES_APPDATA = 'C:/Users/AutoPoint/AppData/Local/ACES'
FIREFOX_PROFILE = "ACES"
ACESCONFIG = os.path.join(ACES_PATH, "acesconfig.py")
# NOTE: the embedded single quotes are necessary because of the blank in 'Program Files'
BROWSERS = {
    "chrome": (r'C:\Program Files\Google\Chrome\Application\chrome.exe',
                r'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe',
                r'C:\Users\%s\AppData\Local\Google\Chrome\Application\chrome.exe' % USER),
    "ie": (r'C:\Program Files (x86)\Internet Explorer\iexplore.exe',
           r'C:\Program Files\Internet Explorer\iexplore.exe'),
    "firefox": (r'C:\Program Files (x86)\Mozilla Firefox\firefox.exe',
                r'C:\Program Files\Mozilla Firefox\firefox.exe')
}

# used this window for error messages if needed 
winlist = []

def enum_windows(hwnd, wlist):
    wlist.append( (hwnd, win32gui.GetWindowText(hwnd)) )


##### patch for slow reponse ############
def _bare_address_string(self):
    host, port = self.client_address[:2]
    return '%s' % host

BaseHTTPServer.BaseHTTPRequestHandler.address_string = _bare_address_string
# end patch #############################




class RPCFunctions(object):

    def __init__(self, link, browser_path):
        self.link = link
        # have to add quotes because of blanks in path
        #self.browser_path = '"' + browser_path + '"'
        self.browser_path = browser_path
        self.browser_name = self.get_browser_name(browser_path)

    def _listMethods(self):
        return ['show_window']

    def get_browser_name(self, browser_path):
        # return general browser name
        if browser_path.find("chrome") != -1:
            return "chrome"
        if browser_path.find("firefox") != -1:
            return "firefox"
        if browser_path.find("iexplore") != -1:
            return "ie"
        return "unknown"

    def show_window(self, branch, termno, custno, custname, phone=""):
        #print ("opening browser...")
        #browser = webbrowser.get(self.browser_path + " %s")
        if self.browser_name == "chrome":
            appdir = 'C:/Users/%s/AppData/Local/ACES' % USER
            #print "appdir is", appdir
            cmd = '"' + self.browser_path + '" %s --user-data-dir=' + '"%s"' % appdir
            #cmd = '"' + self.browser_path + '" %s --user-data-dir=' + ACES_APPDATA
        #elif self.browser_name == "firefox":
        #    cmd = '"' + self.browser_path + '" -P ' + FIREFOX_PROFILE + " %s"
        #    cmd = '"' + self.browser_path + '" -no-remote %s'
        #elif self.browser_name == "ie":
        #    cmd = '"' + self.browser_path + '" %s'
        else:
            cmd = '"' + self.browser_path + '" %s'
        browser = webbrowser.get(cmd)
        self.min_max_window("SmarTerm", 2) # SW_SHOWMINIMIZED
        query = {'branch': branch, 'termno': termno, 'custno': custno,
                 'custname': custname, 'phone': phone}
        query = urlencode(query)
        #print "browser is type", type(browser)
        #print "calling browser.open(%s%s%s)" % (self.link, '/?', query)
        result = browser.open(self.link + '/?' + query)
        #if self.browser_path.endswith('iexplore.exe"'):
        if self.browser_name == "ie":
            # STUPID IE always returns False so force to True
            result = True
        if not result:
            error_msg("Call to Browser failed", gtk.Window(gtk.WINDOW_TOPLEVEL))
        #print ("back from browser open call...maximizing SmarTerm window...")
        #self.min_max_window("SmarTerm Office", 1) # SW_SHOW
        self.min_max_window("SmarTerm", 1) # SW_SHOW
        #print ("exiting show_window().")

    def min_max_window(self, wtitle, SW_arg):
        winlist = []
        win32gui.EnumWindows(enum_windows, winlist)
        #print "Window names:"
        #for window in winlist:
        #    if window[1]:
        #        print window
        #print ("looking for window %s" % wtitle)
        window = [ (hwnd, title) for hwnd, title in winlist if title.startswith(wtitle) ]
        #print ("matching window is: %s" % window)
        #print "Window titles:"
        #for hwnd, title in winlist:
        #    print "title is", title
        #print ("SW_arg is: %s" % SW_arg)
        if window:
            win32gui.ShowWindow(window[0][0], SW_arg)
        #else:
        #    print ("No matching windows found!")
        #print ("leaving min_max_window()")



class TrayIcon(object):

    def __init__(self):
        self.staticon = gtk.StatusIcon()
        self.staticon.set_from_file(ACES_ICON)
        self.staticon.set_visible(True)
        self.popup_menu = gtk.Menu()
        menu_item = gtk.ImageMenuItem("About ACES Catalog")
        menu_item.set_image(gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU))
        menu_item.connect("activate", self.on_about)
        self.popup_menu.append(menu_item)

        menu_item = gtk.ImageMenuItem("Edit Configuration")
        menu_item.set_image(gtk.image_new_from_stock(gtk.STOCK_EDIT, gtk.ICON_SIZE_MENU))
        menu_item.connect("activate", self.on_edit)
        self.popup_menu.append(menu_item)

        menu_item = gtk.ImageMenuItem("Halt ACES Catalog")
        menu_item.set_image(gtk.image_new_from_stock(gtk.STOCK_QUIT, gtk.ICON_SIZE_MENU))
        menu_item.connect("activate", self.on_quit)
        self.popup_menu.append(menu_item)

        self.staticon.connect("popup-menu", self.on_popup, self.popup_menu)
        self.staticon.set_tooltip("AutoPoint's ACES Catalog Interface")

    def on_popup(self, widget, button, time, menu):
        menu.show_all()
        menu.popup(None, None, None, 3, time)

    def on_about(self, widget):
        dialog = gtk.AboutDialog()
        dialog.set_name("ACES Catalog - rev: ")
        dialog.set_version(REVISION)
        dialog.set_comments("AutoPoint's ACES Catalog Interface for part lookup.")
        dialog.run()
        dialog.destroy()

    def on_quit(self, widget):
        dialog = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, 
                    gtk.BUTTONS_YES_NO,
                    "Are you sure you want to halt the ACES catalog Interface?")
        # for some reason windows defaults to yes...i want NO
        dialog.set_default_response(gtk.RESPONSE_NO)
        resp = dialog.run()
        dialog.destroy()
        if resp == gtk.RESPONSE_YES:
            for i in range(gtk.main_level()):
                gtk.main_quit()

    def on_edit(self, widget):
        proc = Popen([r'C:\Python27\pythonw.exe', ACESCONFIG], stdout=PIPE, stderr=PIPE)
        stdout, stderr = proc.communicate()
        if proc.wait() != 0:
            dialog = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, 
                    gtk.BUTTONS_OK,
                    "Sorry, unable to edit configuration: %s" % stdout + stderr)
            dialog.run()
            dialog.destroy()




class ProgressWindow(object):

    def __init__(self, title, message):
        """
        Create a popup window with:
            label
            progress bar
            separator
            Hbutton box with cancel button
        """
        pwin = gtk.Window(gtk.WINDOW_TOPLEVEL)
        pwin.set_title(title)
        pwin.set_border_width(10)
        vbox = gtk.VBox(spacing=4)
        self.label = gtk.Label(message)
        #self.label.set_justify(gtk.JUSTIFY_LEFT)
        self.pbar = gtk.ProgressBar()
        vbox.pack_start(self.label, False, False, 4)
        ###vbox.pack_start(self.label, True, True, 4)
        vbox.pack_start(self.pbar, False, False, 4)
        vbox.pack_start(gtk.HSeparator(), False, False, 2)
        hbuttonbox = gtk.HButtonBox()
        self.cancelbutton = gtk.Button(stock=gtk.STOCK_CANCEL)
        hbuttonbox.pack_end(self.cancelbutton, False, False, 0)
        hbuttonbox.set_layout(gtk.BUTTONBOX_END)
        vbox.pack_start(hbuttonbox, False, False, 0)
        pwin.connect("destroy-event", gtk.main_quit)
        pwin.connect("delete-event", gtk.main_quit)
        self.cancelbutton.connect("clicked", self.cancelbutton_clicked)
        pwin.add(vbox)
        self.pwin = pwin
        self.cancelled = False
        self.visible = False

    def set_task(self, function):
        # this steals idle cycles from the gtk main execution loop so that
        # your task can do some work in the background and update the progress bar
        # NOTE: this function must be a generator function (i.e. contain a 'yield' statement)
        gobject.idle_add(function.next)

    def run(self):
        self.pwin.show_all()
        gtk.main()

    def cancelbutton_clicked(self, widget):
        self.cancelled = True
        if gtk.main_level() == 0:
            raise SystemExit
        gtk.main_quit()

    def close(self, delay=1000, func=None):
        """
        Optional(?) routine to execute before closing popup window
        """
        if self.cancelled:
            self.cancelled = False
            # force to not execute any closeout function
            func = None
        self.cancelbutton.set_sensitive(False)
        if func:
            # set timeout for gtk.main() loop to exit
            gobject.timeout_add(delay, self.timer_callback)
            # automatically passes itself to function
            func(self)
            gtk.main()
        else:
            # destroy yourself!
            gobject.timeout_add(250, self.timer_callback)
            #gobject.timeout_add(delay, self.timer_callback)
            #self.pwin.destroy()
            gtk.main()

    def timer_callback(self):
        self.pwin.destroy()
        gtk.main_quit()

    def splash(self, delay=3000):
        if delay > 0:
            gobject.timeout_add(delay, self.timer_callback)
        self.pbar.set_fraction(1.00)
        self.pwin.show_all()
        gtk.main()

    def fraction(self, frac, msg=None):
        if not self.visible:
            self.pwin.show_all()
            self.visible = True
        if msg:
            self.label.set_text(msg)
        if frac is not None:
            self.pbar.set_fraction(frac)
        while gtk.events_pending():
            gtk.main_iteration(False)




class DummyProgressWindow:
    """
    If running in nongraphical environment, use this object.
    """
    def __init__(self, junk, junk1):
        pass

    def set_task(self, junk):
        pass

    def run(self):
        pass

    def timer_callback(self):
        pass

    def splash(self, junk):
        pass

    def fraction(self, junk, msg=None):
        if msg:
            print (msg)

    def close(self, junk, junk1=None):
        pass




class ACES_XMLRPCServer( SimpleXMLRPCServer ):

    def __init__(self, addr):
        SimpleXMLRPCServer.__init__(self, addr, allow_none=True)
        gobject.io_add_watch(self.socket, gobject.IO_IN, self.do_request)

    def do_request(self, source, condition):
        # had to add this stupid kludge because of issue in windows sockets
        # see: http://bugs.python.org/issue9090
        # This prevents error in SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.do_post()
        # at the call to 'self.rfile.read(chunk_size)':
        #    "A non-blocking socket operation could not be completed immediately"
        sleep(0.5)
        #print ("calling handle_request()...")
        self.handle_request()
        #print ("back from handle request.")
        return True





class ProcessFinder(object):

    def __init__(self):
        pass

    def running(self, wtitle):
        # unfortunately the only window title I could get to work was "pythonw.exe"
        # i never could set title either!
        winlist = []
        win32gui.EnumWindows(enum_windows, winlist)
        #fd = open("C:/temp/debug.txt", 'a')
        #fd.write("window list:\n")
        #for window in winlist:
        #    #if window[1]:
        #    #    fd.write("%s\n" % window[1])
        #window = [ (hwnd, title) for hwnd, title in winlist if title.startswith(wtitle) ]
        window = [ (hwnd, title) for hwnd, title in winlist if title == wtitle ]
        #fd.write("window[] is %s" % window)
        #fd.write("len(window) is %d" % len(window))
        #fd.close()
        #return len(window) > 1
        return len(window)



class FirewallError(Exception):
    pass

class Firewall(object):

    def __init__(self):
        self.windows_release = platform.release()

    def state(self, profile="all"):
        if self.windows_release == "XP":
            cmdline = "netsh firewall show state"
            line_beginning = "Operational mode"
        else:
            cmdline = "netsh advfirewall show %s state" % profile
            line_beginning = "State"
        proc = Popen(cmdline, stdout=PIPE, stderr=PIPE)
        stdout, stderr = proc.communicate()
        if proc.wait() != 0:
            raise FirewallError(stdout + stderr)
        status = []
        for line in stdout.splitlines():
            #print "looking at line", line
            if line.startswith(line_beginning):
                if self.windows_release == "XP":
                    fields = line.split('=')
                    status.append("JUNK")
                    if fields[1].strip() == "Enable":
                        status.append("ON")
                    else:
                        status.append("OFF")
                    break
                else:
                    fields = line.split()
                    status.append(fields[1])
        #print "Firewall state returning", status
        return status

    def get_rules(self, rule_name, profile, direction):
        if self.windows_release == "XP":
            return []
        proc = Popen("netsh.exe advfirewall firewall show rule name=%s dir=%s profile=%s" % \
                     (rule_name, direction, profile), stdout=PIPE, stderr=PIPE)
        stdout, stderr = proc.communicate()
        if proc.wait() != 0:
            #print "show rule error", stdout, stderr
            #raise FirewallError(stdout + stderr)
            return []
        rules = []
        fwfilter = {}
        #print "stdout is", stdout, stderr
        for line in stdout.splitlines():
            if not line.strip(): continue
            if line.startswith('------'): continue
            if line.startswith("Rule Name:"):
                if fwfilter:
                    rules.append(fwfilter)
                    fwfilter = {}
            else:
                fields = line.split()
                if len(fields) == 2:
                    key, value = fields
                    fwfilter[key[:-1]] = value
        if fwfilter:
            rules.append(fwfilter)
        return rules

    def add_rule(self, name, direct, action, program, protocol, profile):
        if self.windows_release == "XP":
            status = admin.runAsAdmin(["netsh.exe", "firewall", "set", "opmode", "disable"])
        else:
            status = admin.runAsAdmin(["netsh.exe", "advfirewall", "firewall", "add", "rule",
                           "name=%s" % name, "dir=%s" % direct, "action=%s" % action,
                           "program=%s" % program, "protocol=%s" % protocol,
                           "profile=%s" % profile])
        return status

    def set_rule(self, name, direct, action, program, protocol, profile):
        status = admin.runAsAdmin(["netsh.exe", "advfirewall", "firewall", "set", "rule", "name=%s" % name,
                             "dir=%s" % direct, "program=%s" % program, "protocol=%s" % protocol,
                             "profile=%s" % profile, "new",  "action=%s" % action,])
        return status



def get_link(conf):
    ip = conf.get("server")
    if not ip:
        raise ValueError("server IP missing from config file: %s" % CONFIG_FILE)
    port = conf.get("port")
    if not port:
        raise ValueError("Missing port number from config file: %s" % CONFIG_FILE)
    return "http://%s:%s/aces_webui" % (ip, port)


def error_msg( message, parent, mtype=gtk.MESSAGE_ERROR,
                buttons=gtk.BUTTONS_OK, cancelButton=None ):
    """
    Display a dialog box on the screen with the given 'message'
    """
    dialog = gtk.MessageDialog( parent,
                                gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
                                mtype, buttons, message)
    if cancelButton:
        dialog.add_button( cancelButton, gtk.RESPONSE_CANCEL )
    dialog.run()
    dialog.destroy()


def splash_screen(delay):
    splash_screen = ProgressWindow(" ACES Catalog Client Startup ",
                             "\n  Bringing up ACES Catalog Client in the backgroud...  \n")
    splash_screen.pbar.set_text("Revision %s" % REVISION)
    splash_screen.splash(delay)


def find_browser(conf):
    browser = conf.get("browser")
    if not browser:
        browser = "chrome"
    browser = browser.lower()
    if browser not in BROWSERS.keys():
        # assume it's a full path to browser
        if not os.path.exists(browser):
            browser = None
    else:
        for path in BROWSERS[browser]:
            if os.path.exists(path):
                browser = path
                break
        else:
            browser = None

    if browser is None:
        raise ValueError("Unable to find browser: %s" % conf["browser"])
    return browser



if __name__ == "__main__":
    # initialize GUI side and its callbacks
    try:
        #if ProcessFinder().running("ACESui.pyw - "):
        if ProcessFinder().running("pythonw.exe"):
            raise RuntimeError("ACES Client is already running.")
        if not os.path.exists(CONFIG_FILE):
            proc = Popen([r'C:\Python27\pythonw.exe', ACESCONFIG], stdout=PIPE, stderr=PIPE)
            stdout, stderr = proc.communicate()
            if proc.wait() != 0:
                raise RuntimeError("Unable to run: %s Reason: %s" % (ACESCONFIG, stdout + stderr))
        conf = simpleConfig.options(CONFIG_FILE, VALID_OPTS)
        browser_path = find_browser(conf)
        # some versions return '127.0.0.1' which won't work!
        client_ip = socket.gethostbyname(socket.gethostname())
        link = get_link(conf)
        splash_screen(3000)
        fw = Firewall()
        status = fw.state()
        # just look at private profile
        if status[1] == "ON":
            rules = fw.get_rules("pythonw", "private", "in")
            if not rules:
                status = fw.add_rule("pythonw", "in", "allow", "C:\python27\pythonw.exe",
                                     "TCP", "private")
                if status != 0:
                    raise FirewallError("Unable to change Firewall settings.")
            else:
                for rule in rules:
                    #print rule
                    if rule["Enabled"] != "Yes" or rule["Action"] != "Allow":
                        status = fw.set_rule("pythonw", "in", "allow", 
                                        "C:\python27\pythonw.exe", "TCP", "private")
                        if status != 0:
                            raise FirewallError("Unable to set firewall rule: %s" % "pythonw")
        server = ACES_XMLRPCServer((client_ip, CLIENT_PORT))
        server.register_introspection_functions()
        server.register_instance(RPCFunctions(link, browser_path))
    except (error, simpleConfig.Error, RuntimeError, ValueError, FirewallError) as err:
        error_msg(str(err), gtk.Window(gtk.WINDOW_TOPLEVEL))
        raise SystemExit
    # create tray icon for this program
    # NOTE: you can comment out next 2 lines for debugging
    TrayIcon()
    gtk.main()
