#!/usr/bin/python
# -*- coding: utf-8 -*-
#
#    Dwarf: A PICKit 2 GUI for Linux
#    Copyright (C) 2009  Nathan Dumont (hairymnstr@gmail.com)
#
#    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 3 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, see <http://www.gnu.org/licenses/>.
#

import pygtk
pygtk.require("2.0")
import gtk, gobject
import os, sys, subprocess

VERSION = "0.1"

class GUI_Controller:
    """ The GUI class is the controller for our application """
    def __init__(self,verbosity):
        self.verbosity = verbosity
        # setup the main window
        self.root = gtk.Window(type=gtk.WINDOW_TOPLEVEL)
        self.root.set_title("Dwarf %s" % VERSION)

        self.root.set_icon_from_file("data/icon.png")
        self.root.connect("destroy", self.destroy_cb)
        self.layout=gtk.VBox()

        self.bar_layout = gtk.HBox()
        self.button_layout = gtk.HBox()

        # build file selector
        self.source_select = gtk.FileChooserButton("Source File")
        self.source_select.set_current_folder(os.path.expanduser("~/"))
        self.source_select.set_size_request(150,-1)
        self.filter_asm = gtk.FileFilter()
        self.filter_asm.set_name("Assembly Files")
        self.filter_asm.add_pattern("*.asm")
        self.source_select.add_filter(self.filter_asm)
        self.filter_hex = gtk.FileFilter()
        self.filter_hex.set_name("HEX Files")
        self.filter_hex.add_pattern("*.hex")
        self.source_select.add_filter(self.filter_hex)
        self.filter_all = gtk.FileFilter()
        self.filter_all.set_name("All Files")
        self.filter_all.add_pattern("*")
        self.source_select.add_filter(self.filter_all)
        self.source_select.set_filter(self.filter_asm)
        self.source_select.connect("file-set", self.file_choose_cb)
        self.bar_layout.pack_start(self.source_select)
        self.source_file = ""

        # add a model selector
        self.model = gtk.TextBuffer()
        self.model.set_text("(none)")
        self.model_display = gtk.TextView(self.model)
        self.bar_layout.pack_start(self.model_display)

        # assemble a set of image buttons
        # Auto-detect PIC button
        self.auto_pb = gtk.Image()
        self.auto_pb.set_from_file("data/auto.png")
        self.auto_btn = gtk.Button()
        self.auto_btn.set_image(self.auto_pb)
        self.auto_btn.connect("clicked", self.auto_cb)
        self.button_layout.pack_start(self.auto_btn)

        # Manual connect button
        self.manual_pb = gtk.Image()
        self.manual_pb.set_from_file("data/manual.png")
        self.manual_btn = gtk.Button()
        self.manual_btn.set_image(self.manual_pb)
        self.manual_btn.connect("clicked", self.manual_cb)
        self.button_layout.pack_start(self.manual_btn)

        # compile
        self.compile_pb = gtk.Image()
        self.compile_pb.set_from_file("data/compile.png")
        self.compile_btn = gtk.Button()
        self.compile_btn.set_image(self.compile_pb)
        self.compile_btn.connect("clicked", self.compile_cb)
        self.button_layout.pack_start(self.compile_btn)

        # set compile to disabled to start with, enable after file
        self.compile_btn.set_sensitive(False)
        
        # program
        self.program_pb = gtk.Image()
        self.program_pb.set_from_file("data/program.png")
        self.program_btn = gtk.Button()
        self.program_btn.set_image(self.program_pb)
        self.program_btn.connect("clicked", self.program_cb)
        self.button_layout.pack_start(self.program_btn)

        # run
        self.run_pb = gtk.Image()
        self.run_pb.set_from_file("data/run.png")
        self.run_btn = gtk.Button()
        self.run_btn.set_image(self.run_pb)
        self.run_btn.connect("clicked", self.run_cb)
        self.button_layout.pack_start(self.run_btn)

        # stop
        self.stop_pb = gtk.Image()
        self.stop_pb.set_from_file("data/stop.png")
        self.stop_btn = gtk.Button()
        self.stop_btn.set_image(self.stop_pb)
        self.stop_btn.connect("clicked", self.stop_cb)
        self.button_layout.pack_start(self.stop_btn)

        # exit
        self.exit_pb = gtk.Image()
        self.exit_pb.set_from_file("data/exit.png")
        self.exit_btn = gtk.Button()
        self.exit_btn.set_image(self.exit_pb)
        self.exit_btn.connect("clicked", self.destroy_cb)
        self.button_layout.pack_start(self.exit_btn)

        # set the root layout
        self.bar_layout.pack_end(self.button_layout, expand=False)
        self.layout.pack_start(self.bar_layout)
        
        # add a status label
        self.status_label = gtk.Label("Status: OK")
        self.status_bg = gtk.EventBox()
        self.status_bg.add(self.status_label)
        self.status_msg = ""    # the detailed message presented when details is clicked.

        self.status_btn = gtk.Button("Details")
        self.status_btn.set_size_request(50,-1)
        self.status_layout = gtk.HBox()
        self.status_layout.pack_start(self.status_bg)
        self.status_layout.pack_start(self.status_btn, expand=False)
        self.status_btn.set_sensitive(False)
        self.status_btn.connect("clicked", self.status_popup_cb)

        self.layout.pack_start(self.status_layout, expand=False)

        self.root.add(self.layout)
        self.root.show_all()

        # make a list of colours for the status label
        self.colours = {}
        self.colours["red"] = gtk.gdk.Color(65535,0,0)
        self.colours["green"] = gtk.gdk.Color(0,65535,0)
        self.colours["yellow"] = gtk.gdk.Color(65535,65535,0)

        # finally: initialise communications
        self.connect_pk2()
        return

    def destroy_cb(self, *kw):
        """ Destroy callback to shutdown the app """
        gtk.main_quit()
        return

    def run(self, *kw):
        """ run is called to set off the GTK mainloop """
        gtk.main()
        return

    def compile_cb(self, *kw):
        """ compile current project """
        if self.source_file and os.path.isfile(self.source_file):
            # no point running compile if there isn't a file.
            pr = subprocess.Popen("gpasm " + self.source_file.replace(" ","\\ "), shell=True, stdout=subprocess.PIPE)
            pr.wait()
            output = pr.stdout.read()

            errors = []
            warnings = []
            # now we have run the compiler, need to parse the output and display warnings/errors
            for l in output.splitlines():
                if not l == "":
                    # first see whether it is an error or a warning
                    if l.find("Error") > -1:
                        errors.append(l)
                    elif l.find("Warning") > -1:
                        warnings.append(l)
                    else:
                        print "Unhandled output type:", l

            self.status_msg = "-"*80 + "\n" + output + "-"*80 + "\nWarnings: %d, Errors: %d\n" % (len(warnings), len(errors)) + "-"*80 
            # now present the information with the status label
            if len(warnings) + len(errors) == 0:
                self.status_label.set_text("Compiled OK")
                self.status_bg.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(0,65535,0))
                self.status_btn.set_sensitive(False)
            elif len(warnings) > 0 and len(errors) == 0:
                self.status_label.set_text("%d Warnings, 0 Errors, click for details" % len(warnings))
                self.status_bg.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(65535,65535,0))
                self.status_btn.set_sensitive(True)
            elif len(errors) > 0:
                self.status_label.set_text("%d Warnings, %d Errors, click for details" % (len(warnings), len(errors)))
                self.status_bg.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(65535,0,0))
                self.status_btn.set_sensitive(True)
            if self.verbosity:
                print "gpasm " + self.source_file.replace(" ","\\ ")
                print "-"*80
                print output
                print "-"*80
                print "Warnings:", len(warnings), "Errors:", len(errors)
                print "-"*80
        else:
            self.status_label.set_text("Select valid source file.")
            self.status_bg.modify_bg(gtk.STATE_NORMAL,gtk.gdk.Color(65535,0,0))
            self.status_btn.set_sensitive(False)
        return

    def auto_cb(self, *kw):
        """ auto-connect to the pickit and detect the attached PIC """
        self.connect_pk2()
        return

    def manual_cb(self, *kw):
        """ manually connect the pickit with the selected device model """
        self.connect_pk2(False)
        return

    def program_cb(self, *kw):
        """ download the current binary file to a PIC """
        # first, check if there's a binary file ready (and that it's newer than the source
        # determine what the hexfile name should be:
        hex_name = os.path.splitext(self.source_file)[0] + ".hex"
        if not os.path.isfile(hex_name):
            self.status_label.set_text("Hex file not found")
            self.status_bg.modify_bg(gtk.STATE_NORMAL,gtk.gdk.Color(65535,0,0))
            self.status_btn.set_sensitive(False)
            return
        if not (hex_name == self.source_file):
            # if the selected file is the binary file don't worry about compiler warning
            hex_age = os.path.getmtime(hex_name)
            src_age = os.path.getmtime(self.source_file)
            if src_age > hex_age:
                # the source has been editted and not compiled, emit a warning
                msg_box = gtk.MessageDialog(type=gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_OK_CANCEL, message_format="Source is newer than binary,\nprogram without compiling?")
                response = msg_box.run()
                msg_box.destroy()
                if response == gtk.RESPONSE_CANCEL:
                    return

        # now do the actual programming bit
        if self.verbosity:
            print "pk2cmd -P%s -F%s -M" % (self.model.get_text(self.model.get_start_iter(), self.model.get_end_iter()), hex_name)
        pr = subprocess.Popen("pk2cmd -P%s -F%s -M" % (self.model.get_text(self.model.get_start_iter(), self.model.get_end_iter()),hex_name.replace(" ","\\ ")), shell=True, stdout = subprocess.PIPE)
        pr.wait()
        output = pr.stdout.read()
        self.status_msg = output
        if output.find("Program Succeeded") > -1:
            self.status_label.set_text("Program Succeeded")
            self.status_btn.set_sensitive(True)
            self.status_bg.modify_bg(gtk.STATE_NORMAL,gtk.gdk.Color(0,65535,0))
        else:
            self.status_label.set_text("Program Failed")
            self.status_btn.set_sensitive(True)
            self.status_bg.modify_bg(gtk.STATE_NORMAL,gtk.gdk.Color(65535,0,0))
        if self.verbosity:
            print output
        return

    def run_cb(self, *kw):
        """ Turn power on and MCLR off to let the project run powered by the
              PicKit """
        pr = subprocess.Popen("pk2cmd -P" + self.model.get_text(self.model.get_start_iter(), self.model.get_end_iter()) + " -R -T", shell=True, stdout=subprocess.PIPE)
        pr.wait()
        if self.verbosity:
            print "pk2cmd -P" + self.model.get_text(self.model.get_start_iter(), self.model.get_end_iter()) + " -R -T"
        return

    def stop_cb(self, *kw):
        """ Pause the PIC again with MCLR asserted and power off. """
        pr = subprocess.Popen("pk2cmd -P" + self.model.get_text(self.model.get_start_iter(), self.model.get_end_iter()), shell=True, stdout=subprocess.PIPE)
        pr.wait()
        if self.verbosity:
            print "pk2cmd -P" + self.model.get_text(self.model.get_start_iter(), self.model.get_end_iter())
        return

    def file_choose_cb(self, *kw):
        """ Update the local variable containing the filename. """
        self.source_file = self.source_select.get_filename()
        self.root.set_title("Dwarf %s: " % VERSION + self.source_file.split("/")[-1])
        if self.source_select.get_filename()[-4:] == ".hex":
            self.compile_btn.set_sensitive(False)
            # do status
            self.show_status("OK, selected a binary file, compile disabled", "green", False)
        else:
            self.compile_btn.set_sensitive(True)
            self.show_status("OK, selected source file.", "green", False)
        if self.verbosity:
            print "File chosen:", self.source_select.get_filename()
        return

    def status_popup_cb(self, *kw):
        """ Display a popup window with some more detailed info about the status """
        self.popup = Status_popup()
        self.popup.set_status_msg(self.status_msg)
        return

    def connect_pk2(self, auto=True):
        """ Try to connect to the pic-kit and identify the device attached. """
        if auto:
            cmd = "pk2cmd -P"
        else:
            if self.model.get_text(self.model.get_start_iter(), self.model.get_end_iter()) == "(none)":
                self.status_label.set_text("Can't manually connect without device specified.")
                self.status_bg.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(65535,0,0))
                self.status_btn.set_sensitive(False)
                return
            elif self.model.get_text(self.model.get_start_iter(), self.model.get_end_iter()).find(" ") > -1:
                self.show_status("Not a valid chip model, don't use SPACE","red",False)
                return
            cmd = "pk2cmd -P" + self.model.get_text(self.model.get_start_iter(),self.model.get_end_iter()) + " -I"
        pr = subprocess.Popen(cmd, shell=True, stdout = subprocess.PIPE)
        pr.wait()
        
        output = pr.stdout.read()
        
        if output.find("No PICkit 2 found.") > -1:
            # the PICKit wasn't found so de-activate the related functions
            self.program_btn.set_sensitive(False)
            self.run_btn.set_sensitive(False)
            self.stop_btn.set_sensitive(False)
            
            # and let the user know what's going on
            self.status_label.set_text("PICKit 2 Not Found")
            self.status_bg.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(65535,65535,0))
            self.status_btn.set_sensitive(False)
        elif auto:
            if output.find("No known part found") > -1:
                # Auto detect failed, even though PK2 is plugged into PC
                # set the model to none and disable programming
                self.model.set_text("(none)")
                self.program_btn.set_sensitive(False)
                self.run_btn.set_sensitive(False)
                self.stop_btn.set_sensitive(False)

                # then let the user know what's going on
                self.status_label.set_text("Auto Detect Failed")
                self.status_bg.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(65535,65535,0))
                self.status_btn.set_sensitive(False)
            else:
                # find the string from the description
                self.model.set_text(output[output.find("Found part ")+len("Found part "):output.find(".",output.find("Found part"))])
                self.program_btn.set_sensitive(True)
                self.run_btn.set_sensitive(True)
                self.stop_btn.set_sensitive(True)

                # do the status label
                self.status_label.set_text("Detected a " + output[output.find("Found part ")+len("Found part "):output.find(".",output.find("Found part"))])
                self.status_bg.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(0,65535,0))
                self.status_btn.set_sensitive(False)
        else:
            # manual detect
            # find the device name
            dev = output[output.find("Device Name = ") + len("Device Name = "):output.find("\n",output.find("Device Name"))]
            if output.find("Could not find device") > -1:
                # there was no device found
                self.program_btn.set_sensitive(False)
                self.run_btn.set_sensitive(False)
                self.stop_btn.set_sensitive(False)

                # set status
                self.show_status("No device found.","yellow",False)
            elif dev == self.model.get_text(self.model.get_start_iter(),self.model.get_end_iter()):
                # the found target matches the manually specified device.
                # enable programming functions
                self.program_btn.set_sensitive(True)
                self.run_btn.set_sensitive(True)
                self.stop_btn.set_sensitive(True)

                # then setup the status label
                self.status_label.set_text("Detected a " + dev)
                self.status_bg.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(0,65535,0))
                self.status_btn.set_sensitive(False)
            else:
                # the found target is a different device, disabel and display found type
                self.program_btn.set_sensitive(False)
                self.run_btn.set_sensitive(False)
                self.stop_btn.set_sensitive(False)

                # status label
                self.status_label.set_text("Warning: " + dev + " found.")
                self.status_bg.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(65535,65535,0))
                self.status_btn.set_sensitive(False)
        return

    def show_status(self,msg,col,detail):
        """ helper function to set the status bar message etc. """
        self.status_label.set_text(msg)
        self.status_bg.modify_bg(gtk.STATE_NORMAL, self.colours[col])
        self.status_btn.set_sensitive(detail)
        return

class Status_popup():
    def __init__(self):
        self.win = gtk.Window()
        self.win.set_title("Dwarf: Status Messages")
        self.win.set_icon_from_file("data/icon.png")
        self.win.connect("destroy", self.destroy_cb)

        self.layout = gtk.VBox()
        self.text_view = gtk.Label()
        self.layout.pack_start(self.text_view)

        self.close_btn = gtk.Button("Close")
        self.close_btn.connect("clicked", self.close_btn_cb)
        self.close_layout = gtk.HBox()
        self.close_layout.pack_end(self.close_btn, expand=False)
        self.layout.pack_start(self.close_layout, expand=False)
        self.win.add(self.layout)
        self.win.show_all()
        return

    def destroy_cb(self, *kw):
        del(self)

    def close_btn_cb(self,*kw):
        self.win.destroy()

    def set_status_msg(self, msg):
        self.text_view.set_text(msg)
        return

if __name__ == "__main__":
    verbosity = False
    if len(sys.argv) > 1:
        if sys.argv[1] == "-v":
            verbosity = True
    mygui = GUI_Controller(verbosity)
    mygui.run()
    