#!/usr/bin/python

# Copyright 2008 Stephen Warren
#
# This file is part of congruity.
#
# congruity 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.
#
# congruity 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 congruity.  If not, see <http://www.gnu.org/licenses/>.

from ctypes import *
import os
import os.path
import sys
import thread
import time
import traceback
import wx
import wx.lib.dialogs

version = "9"

try:
    import libconcord
    # Fix typo in libconcord 0.20 Python bindings
    try:
        libconcord.delete_blob
    except:
        libconcord.delete_blob = libconcord.delete_block
except:
    app = wx.PySimpleApp()
    dlg = wx.MessageDialog(
        None,
        "Could not load libconcord; please ensure it, and the Python "
        "bindings, are installed and in the relevant search paths.",
        "congruity: Dependency Error",
        wx.OK | wx.ICON_ERROR
    )
    dlg.ShowModal()
    os._exit(1)

def counter():
    i=0
    while True:
        yield i
        i += 1

def program_callback_imp(count, current, total, context):
    if not context:
        return

    try:
        (f, fcontext) = context
        percent = (current * 100) / total
        f(False, percent, fcontext)
    except:
        print
        traceback.print_exc()

class CmdLineException(Exception):
    pass

def exception_message():
    msg = ''
    if type(sys.exc_value) == libconcord.LibConcordException:
        try:
            msg += '%s\n    (libconcord function %s error %d)\n\n' % (
                sys.exc_value.result_str,
                sys.exc_value.func,
                sys.exc_value.result
            )
        except:
            pass
    if type(sys.exc_value) == CmdLineException:
        try:
            msg += '%s\n\n' % (
                str(sys.exc_value)
            )
        except:
            pass
    msg += traceback.format_exc()
    return msg

def worker_body_connect(
    resources,
    on_progress,
    cb_context,
    cancel_check,
    after_reset
):
    program_callback = libconcord.callback_type(program_callback_imp)

    max_attempts = after_reset and 180 or 60
    for attempt in range(max_attempts):
        on_progress(
            False,
            (attempt * 100) / max_attempts,
            cb_context
        )
        try:
            libconcord.init_concord()
            resources.SetConnected(True)
            try:
                libconcord.get_identity(
                    program_callback,
                    None
                )
            except:
                ignore = False
                if type(sys.exc_value) == libconcord.LibConcordException:
                    # FIXME: Expose these constants in libconcord.py
                    ignore = sys.exc_value.result == 16 #LC_ERROR_INVALID_CONFIG
                if not ignore:
                    raise
            break
        except:
            if cancel_check() or (attempt == max_attempts - 1):
                raise
        time.sleep(1)
    on_progress(
        True,
        100,
        cb_context
    )

def show_modal_scrolled_msgbox(parent, title, text):
    size = parent.GetClientSizeTuple()
    size = (size[0] * 90 / 100, size[1] * 90 / 100)
    wx.lib.dialogs.ScrolledMessageDialog(
        parent,
        text,
        title,
        (-1, -1),
        size
    ).ShowModal()

ALIGN_LTA = wx.ALIGN_LEFT | wx.ALIGN_TOP             | wx.ALL
ALIGN_LCA = wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL | wx.ALL 
ALIGN_LBA = wx.ALIGN_LEFT | wx.ALIGN_BOTTOM          | wx.ALL

class WrappedStaticText(wx.StaticText):
    def __init__(self, parent):
        self.parent = parent
        wx.StaticText.__init__(self, parent, -1, "")

    def UpdateText(self, new_label):
        cur_width = self.parent.GetSize().GetWidth()
        self.SetLabel(new_label)           
        self.Wrap(cur_width)
        self.parent.Layout()

class DecoratedContainer(wx.Panel):
    def __init__(self, parent, resources):
        self.parent = parent
        self.resources = resources

        wx.Panel.__init__(self, parent)

        self.sizer = wx.GridBagSizer(5, 5)
        self.sizer.AddGrowableCol(2)
        self.SetSizer(self.sizer)

        self.last_updated_dg = None

    def _DgStart(self, dg):
        self._OnProgressGauge(False, 0, dg)

    def _DgUpdate(self, is_done, percent, dg):
        self._OnProgressGauge(is_done, percent, dg)

    def _DgEnd(self, dg):
        self._OnProgressGauge(True, 100, dg)

    def _DgFailure(self):
        if self.last_updated_dg:
            self.last_updated_dg.SetBitmap(self.resources.icon_failed)

    def _OnProgressGauge(self, is_done, percent, dg):
        if is_done:
            new_bitmap = self.resources.icon_complete
        else:
            new_bitmap = self.resources.icon_in_progress
        dg.SetBitmap(new_bitmap)
        dg.gauge.SetValue(percent)
        self.last_updated_dg = dg

class DecoratedContainerThreadMixin(object):
    def __init__(self, dc):
        self.dc = dc

    def _DgStart(self, dg):
        wx.CallAfter(self.dc._DgStart, dg)

    def _DgUpdate(self, is_done, percent, dg):
        wx.CallAfter(self.dc._DgUpdate, is_done, percent, dg)

    def _DgEnd(self, dg):
        wx.CallAfter(self.dc._DgEnd, dg)

    def _DgFailure(self):
        wx.CallAfter(self.dc._DgFailure)

class DecoratedGauge(object):
    def __init__(self, parent, caption, vpos):
        self.current_bitmap = parent.resources.icon_unstarted
        self.bitmap = wx.StaticBitmap(
            parent,
            -1,
            self.current_bitmap,
            None,
            parent.resources.iwh
        )
        self.text = wx.StaticText(parent, -1, caption)
        self.gauge = wx.Gauge(
            parent,
            -1,
            100,
            None,
            (250, parent.resources.iwh[1])
        )
        parent.sizer.Add(self.bitmap, (vpos, 0), (1, 1), ALIGN_LBA, 5)
        parent.sizer.Add(self.text,   (vpos, 1), (1, 1), ALIGN_LCA, 5)
        parent.sizer.Add(self.gauge,  (vpos, 2), (1, 1), ALIGN_LBA, 5)

    def SetBitmap(self, new_bitmap):
        if self.current_bitmap == new_bitmap:
            return
        self.current_bitmap = new_bitmap
        self.bitmap.SetBitmap(self.current_bitmap)

class MessagePanelBase(wx.Panel):
    def __init__(self, parent, resources):
        self.parent = parent
        self.resources = resources

        wx.Panel.__init__(self, parent)

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.text_message = WrappedStaticText(self)
        self.sizer.Add(self.text_message, 0, ALIGN_LTA, 5)
        self.SetSizerAndFit(self.sizer)

class WelcomePanel(MessagePanelBase):
    _msg_welcome = (
        "Welcome to congruity; a programming application " + 
        "for Logitech Harmony remote controls.\n\n"
    )

    _msg_progress_parsing = (
        "Please wait while the configuration file is parsed."
    )

    _msg_status_ok = (
        "Please ensure the remote control is connected " +
        "before proceeding.\n\n" + 
        "Click next to begin operation."
    )

    _msg_status_failure = (
        "A problem occurred. Click next for details."
    )

    _msg_failure_explanation = (
        "The configuration file cannot be read, or parsing failed.\n\n" +
        "Operation cannot continue."
    )

    _msg_failure_details_unknown_op = (
        "Unrecognized file type '%d' returned by libconcord"
    )

    def __init__(self, parent, resources):
        self.parent = parent
        self.resources = resources

        MessagePanelBase.__init__(self, parent, resources)

        self.next = None
        self.initial_exception = None

    def _WorkerFunction(self):
        try:
            wx.CallAfter(
                self.text_message.UpdateText,
                self._msg_welcome + self._msg_progress_parsing
            )

            if self.initial_exception:
                wx.CallAfter(
                    self._OnStatusFailure,
                    *self.initial_exception
                )
                return

            self.next = self.resources.page_connect

            xml = POINTER(c_ubyte)()
            xml_size = c_uint()
            libconcord.read_file(
                self.resources.ezhex_filename,
                byref(xml),
                byref(xml_size)
            )
            self.resources.SetXmlData(xml, xml_size)

            type = c_int()
            libconcord.identify_file(xml, xml_size, byref(type))

            (next_page, type_text) = {
                libconcord.LC_FILE_TYPE_CONNECTIVITY:
                    (
                        self.resources.page_check_connectivity,
                        "Connectivity Check"
                    ),
                libconcord.LC_FILE_TYPE_CONFIGURATION:
                    (
                        self.resources.page_write_configuration,
                        "Update Configuration"
                    ),
                libconcord.LC_FILE_TYPE_FIRMWARE:
                    (
                        self.resources.page_update_firmware,
                        "Update Firmware"
                    )
            }.get(type.value, (None, None))

            if not next_page:
                wx.CallAfter(
                    self._OnStatusFailure,
                    self._msg_failure_explanation,
                    self._msg_failure_details_unknown_op % type.value
                )
                return

            wx.CallAfter(self._OnStatusOk, next_page, type_text)
        except:
            wx.CallAfter(
                self._OnStatusFailure,
                self._msg_failure_explanation,
                exception_message()
            )

    def _OnStatusOk(self, next_page, type_text):
        self.resources.page_connect.SetNext(next_page)
        self._OnStatusCommon(self._msg_status_ok)

    def _OnStatusFailure(self, failure_msg, details):
        self.resources.page_failure.SetMessages(
            failure_msg,
            details
        )
        self.next = self.resources.page_failure
        self._OnStatusCommon(self._msg_status_failure)

    def _OnStatusCommon(self, message):
        self.text_message.UpdateText(self._msg_welcome + message)
        self.parent.ReenableCancel()
        self.parent.ReenableNext()

    def SetInitialException(self, initial_exception):
        self.initial_exception = initial_exception

    def OnActivated(self):
        thread.start_new_thread(self._WorkerFunction, ())

    def OnCancel(self):
        self.parent.OnExit(1)

    def GetTitle(self):
        return "Welcome"

    def IsTerminal(self):
        return False

    def IsNextInitiallyDisabled(self):
        return True

    def IsCancelInitiallyDisabled(self):
        return False

    def GetNext(self):
        return self.next

class ConnectPanel(wx.Panel, DecoratedContainerThreadMixin):
    _msg_ensure_connected = (
        "Please ensure your remote is correctly connected to your computer."
    )

    _msg_status_ok = (
        "Successfully connected to a remote:\n%s"
    )

    _msg_status_failure = (
        "A problem occurred. Click next for details."
    )

    _msg_failure_explanation = (
        "No remote could be found."
    )

    def __init__(self, parent, resources):
        self.parent = parent
        self.resources = resources

        wx.Panel.__init__(self, parent)

        self.sizer = wx.BoxSizer(wx.VERTICAL)

        self.text_help = WrappedStaticText(self)
        self.sizer.Add(self.text_help, 0, wx.ALIGN_LEFT | wx.ALL, 5)

        self.dc = DecoratedContainer(self, resources)
        DecoratedContainerThreadMixin.__init__(self, self.dc)
        self.dg_connect = DecoratedGauge(self.dc, "Detect Remote", 0)
        self.sizer.Add(self.dc, 0, wx.ALIGN_LEFT | wx.ALL, 5)

        self.text_info = WrappedStaticText(self)
        self.sizer.Add(self.text_info, 0, wx.ALIGN_LEFT | wx.ALL, 5)

        self.SetSizerAndFit(self.sizer)

        self.next = None
        self.finished = False
        self.cancelled = False

        self.lock = thread.allocate_lock()

    def _WorkerFunction(self):
        try:
            wx.CallAfter(
                self.text_help.UpdateText,
                self._msg_ensure_connected
            )

            self._DgStart(self.dg_connect)
            worker_body_connect(
                self.resources,
                self._DgUpdate,
                self.dg_connect,
                lambda: self.cancelled,
                False
            )
            self._DgEnd(self.dg_connect)

            mfg = libconcord.get_mfg()
            model = libconcord.get_model()

            mfg_model = mfg + " " + model
            wx.CallAfter(self._OnStatusOk, mfg_model)
        except:
            wx.CallAfter(
                self._OnStatusFailure,
                self._msg_failure_explanation,
                exception_message()
            )

        self.lock.acquire()
        self.finished = True
        if self.cancelled:
            wx.CallAfter(self.OnCancel)
        self.lock.release()

    def _OnStatusOk(self, mfg_model):
        self.btn_details = wx.Button(self, -1, "&Details...")
        self.sizer.Add(self.btn_details, 0, wx.ALIGN_LEFT | wx.ALL, 5)
        self.Bind(wx.EVT_BUTTON, self._OnDetails, self.btn_details)
        self._OnStatusCommon(self._msg_status_ok % mfg_model)

    def _OnStatusFailure(self, failure_msg, details):
        self.dc._DgFailure()
        self.resources.page_failure.SetMessages(
            failure_msg,
            details
        )
        self.SetNext(self.resources.page_failure)
        self._OnStatusCommon(self._msg_status_failure)

    def _OnStatusCommon(self, message):
        self.text_info.UpdateText(message)
        self.parent.ReenableNext()

    def _OnDetails(self, event):
        try:
            msg = ""

            mfg = libconcord.get_mfg()
            model = libconcord.get_model()
            codename = libconcord.get_codename()
            msg += "Model: %s %s (%s)\n" % (msg, model, codename)

            hid_mfg = libconcord.get_hid_mfg_str()
            hid_prod = libconcord.get_hid_prod_str()
            msg += "USB HID Model: %s %s\n" % (hid_mfg, hid_prod)

            ser_1 = libconcord.get_serial(libconcord.SERIAL_COMPONENT_1)
            ser_2 = libconcord.get_serial(libconcord.SERIAL_COMPONENT_2)
            ser_3 = libconcord.get_serial(libconcord.SERIAL_COMPONENT_3)
            msg += "Serial:\n    %s\n    %s\n    %s\n" % (ser_1, ser_2, ser_3)

            arch = libconcord.get_arch()
            proto = libconcord.get_proto()
            skin = libconcord.get_skin()
            msg += "Arch:%d Proto:%d Skin:%d\n" % (arch, proto, skin)

            fw_type = libconcord.get_fw_type()
            fw_ver_maj = libconcord.get_fw_ver_maj()
            fw_ver_min = libconcord.get_fw_ver_min()
            msg += "Firmware type:%d, version %d.%d\n" % (
                fw_type, fw_ver_maj, fw_ver_min
            )

            hw_ver_maj = libconcord.get_hw_ver_maj()
            hw_ver_min = libconcord.get_hw_ver_min()
            msg += "HW version %d.%d\n" % (hw_ver_maj, hw_ver_min)

            flash_mfg = libconcord.get_flash_mfg()
            flash_id = libconcord.get_flash_id()
            flash_part_num = libconcord.get_flash_part_num()
            flash_size = libconcord.get_flash_size()
            msg += "Flash Manufacturer:%d ID:%d Part:%s Size:%dK\n" % (
                flash_mfg, flash_id, flash_part_num, flash_size
            )

            hid_irl = libconcord.get_hid_irl()
            hid_orl = libconcord.get_hid_orl()
            hid_frl = libconcord.get_hid_frl()
            msg += "USB HID Irl:%d Orl:%d Frl:%d\n" % (hid_irl, hid_orl, hid_frl)

            usb_vid = libconcord.get_usb_vid()
            usb_pid = libconcord.get_usb_pid()
            usb_bcd = libconcord.get_usb_bcd()
            msg += "USB VID:%04x PID:%04x BCD:%04x\n" % (usb_vid, usb_pid, usb_bcd)

            config_bytes_used = libconcord.get_config_bytes_used()
            config_bytes_total = libconcord.get_config_bytes_total()
            config_pct_used = (config_bytes_used * 100) / config_bytes_total
            msg += "Config used %d / total %d = %d%%\n" % (
                config_bytes_used, config_bytes_total, config_pct_used
            )

            fw_nondirect = libconcord.is_fw_update_supported(0) == 0
            fw_direct = libconcord.is_fw_update_supported(1) == 0
            if fw_nondirect or fw_direct:
                config_safe = libconcord.is_config_safe_after_fw() == 0
                msg += "Firmware updates: Supported (%s), config%ssafe\n" % (
                    fw_direct and "Direct" or "Not direct",
                    config_safe and " " or " NOT "
                )
            else:
                msg += "Firmware updates: NOT supported\n"
        except:
            msg = (
                "Error retrieving remote information:\n" +
                exception_message()
            )

        show_modal_scrolled_msgbox(self.parent, "Remote Information", msg)

    def SetNext(self, next):
        self.next = next

    def OnActivated(self):
        thread.start_new_thread(self._WorkerFunction, ())

    def OnCancel(self):
        self.lock.acquire()
        if self.finished:
            self.lock.release()
            self.parent.OnExit(1)
        else:
            self.cancelled = True
        self.lock.release()

    def GetTitle(self):
        return "Connecting"

    def IsTerminal(self):
        return False

    def IsNextInitiallyDisabled(self):
        return True

    def IsCancelInitiallyDisabled(self):
        return False

    def GetNext(self):
        return self.next

class ProgramRemotePanelBase(wx.Panel, DecoratedContainerThreadMixin):
    def __init__(self, parent, resources, file_type):
        self.parent = parent
        self.resources = resources
        self.file_type = file_type

        wx.Panel.__init__(self, parent)

        self.sizer = wx.BoxSizer(wx.VERTICAL)

        self.dc = DecoratedContainer(self, resources)
        DecoratedContainerThreadMixin.__init__(self, self.dc)
        self._AddWidgets()
        self.sizer.Add(self.dc, 0, wx.ALIGN_LEFT | wx.ALL, 5)

        self.SetSizerAndFit(self.sizer)

        self.next = None
        self.finished = False

    def _WorkerFunction(self):
        try:
            try:
                self._WorkerFunctionBody()
                wx.CallAfter(self._OnStatusOk)
                return
            except:
                wx.CallAfter(
                    self._OnStatusFailure,
                    "Operation Failed",
                    exception_message()
                )
        finally:
            try:
                if self.resources.connected:
                    self.resources.SetConnected(False)
                    libconcord.deinit_concord()
            except:
                pass

    def _OnStatusOk(self):
        self.next = self.resources.page_success
        self._OnStatusCommon()

    def _OnStatusFailure(self, failure_msg, details):
        self.dc._DgFailure()
        self.next = self.resources.page_failure
        self.resources.page_failure.SetMessages(
            failure_msg,
            details
        )
        self._OnStatusCommon()

    def _OnStatusCommon(self):
        self.finished = True
        self.parent.ReenableCancel()
        self.parent.ReenableNext()

    def OnActivated(self):
        thread.start_new_thread(self._WorkerFunction, ())

    def OnCancel(self):
        if self.finished:
            self.parent.OnExit(1)
        show_modal_scrolled_msgbox(
            self.parent,
            "Cannot Cancel",
            "Cancel is disabled during programming operations, " +
            "to prevent placing the remote into a state that will " +
            "potentially be difficult to recover from."
        )

    def IsTerminal(self):
        return False

    def IsNextInitiallyDisabled(self):
        return True

    def IsCancelInitiallyDisabled(self):
        return True

    def GetNext(self):
        return self.next

class CheckConnectivityPanel(ProgramRemotePanelBase):
    def __init__(self, parent, resources):
        ProgramRemotePanelBase.__init__(
            self,
            parent,
            resources,
            libconcord.LC_FILE_TYPE_CONNECTIVITY
        )

    def _AddWidgets(self):
        vpos = counter()
        self.dg_notify_website = DecoratedGauge(self.dc, "Notify Website", vpos.next())

    def _WorkerFunctionBody(self):
        self._DgUpdate(
            False,
            0,
            self.dg_notify_website
        )
        libconcord.post_connect_test_success(
            self.resources.xml,
            self.resources.xml_size
        )
        self._DgUpdate(
            True,
            100,
            self.dg_notify_website
        )

    def GetTitle(self):
        return "Checking Connectivity"

class WriteConfigurationPanel(ProgramRemotePanelBase):
    def __init__(self, parent, resources):
        ProgramRemotePanelBase.__init__(
            self,
            parent,
            resources,
            libconcord.LC_FILE_TYPE_CONFIGURATION
        )

    def _AddWidgets(self):
        vpos = counter()
        self.dg_check_website = DecoratedGauge(self.dc, "Check Website", vpos.next())
        self.dg_prepare = DecoratedGauge(self.dc, "Prepare Remote", vpos.next())
        self.dg_erase = DecoratedGauge(self.dc, "Erase Flash", vpos.next())
        self.dg_write = DecoratedGauge(self.dc, "Write Configuration", vpos.next())
        self.dg_verify = DecoratedGauge(self.dc, "Verify Upgrade", vpos.next())
        self.dg_reconnect = DecoratedGauge(self.dc, "Reconnect to Remote", vpos.next())
        self.dg_set_time = DecoratedGauge(self.dc, "Set Time", vpos.next())
        self.dg_notify_website = DecoratedGauge(self.dc, "Notify Website", vpos.next())

    def _WorkerFunctionBody(self):
        program_callback = libconcord.callback_type(program_callback_imp)

        self._DgStart(self.dg_check_website)
        libconcord.post_preconfig(self.resources.xml, self.resources.xml_size)
        self._DgEnd(self.dg_check_website)

        self._DgStart(self.dg_prepare)
        bin_data = POINTER(c_ubyte)()
        bin_size = c_uint()
        libconcord.find_config_binary(
            self.resources.xml,
            self.resources.xml_size,
            byref(bin_data),
            byref(bin_size)
        )
        self._DgUpdate(False, 50, self.dg_prepare)
        libconcord.invalidate_flash()
        self._DgEnd(self.dg_prepare)

        self._DgStart(self.dg_erase)
        libconcord.erase_config(
            bin_size,
            program_callback,
            py_object((self._DgUpdate, self.dg_erase))
        )
        self._DgEnd(self.dg_erase)

        self._DgStart(self.dg_write)
        libconcord.write_config_to_remote(
            bin_data,
            bin_size,
            program_callback,
            py_object((self._DgUpdate, self.dg_write))
        )
        self._DgEnd(self.dg_write)

        self._DgStart(self.dg_verify)
        libconcord.verify_remote_config(
            bin_data,
            bin_size,
            program_callback,
            py_object((self._DgUpdate, self.dg_verify))
        )
        self._DgEnd(self.dg_verify)

        self._DgStart(self.dg_reconnect)
        libconcord.reset_remote()
        worker_body_connect(
            self.resources,
            self._DgUpdate,
            self.dg_reconnect,
            lambda: False,
            True
        )

        self._DgStart(self.dg_set_time)
        libconcord.set_time()
        self._DgEnd(self.dg_set_time)

        self._DgStart(self.dg_notify_website)
        libconcord.post_postconfig(self.resources.xml, self.resources.xml_size)
        self._DgEnd(self.dg_notify_website)

    def GetTitle(self):
        return "Updating Configuration"

class UpdateFirmwarePanel(ProgramRemotePanelBase):
    def __init__(self, parent, resources):
        ProgramRemotePanelBase.__init__(
            self,
            parent,
            resources,
            libconcord.LC_FILE_TYPE_FIRMWARE
        )

    def _AddWidgets(self):
        vpos = counter()
        self.dg_prepare = DecoratedGauge(self.dc, "Prepare Remote", vpos.next())
        self.dg_erase = DecoratedGauge(self.dc, "Erase Flash", vpos.next())
        self.dg_write = DecoratedGauge(self.dc, "Write Firmware", vpos.next())
        self.dg_finalize = DecoratedGauge(self.dc, "Finalize Programming", vpos.next())
        self.dg_reconnect = DecoratedGauge(self.dc, "Reconnect to Remote", vpos.next())
        self.dg_set_time = DecoratedGauge(self.dc, "Set Time", vpos.next())
        self.dg_notify_website = DecoratedGauge(self.dc, "Notify Website", vpos.next())

    def _WorkerFunctionBody(self):
        program_callback = libconcord.callback_type(program_callback_imp)

        self._DgStart(self.dg_prepare)

        # is_fw_update_supported returns error code; 0 OK, otherwise failure
        if libconcord.is_fw_update_supported(0) == 0:
            is_direct = False
        elif libconcord.is_fw_update_supported(1) == 0:
            is_direct = True
        else:
            raise Exception(
                "Sorry, congruity doesn't yet support firmware update " +
                "on this remote model."
            )

        self._DgUpdate(False, 25, self.dg_prepare)

        bin_data = POINTER(c_ubyte)()
        bin_size = c_uint()
        libconcord.extract_firmware_binary(
            self.resources.xml,
            self.resources.xml_size,
            byref(bin_data),
            byref(bin_size)
        )

        self._DgUpdate(False, 50, self.dg_prepare)

        if not is_direct:
            libconcord.prep_firmware()

        self._DgUpdate(False, 75, self.dg_prepare)

        libconcord.invalidate_flash()

        self._DgEnd(self.dg_prepare)

        self._DgStart(self.dg_erase)
        libconcord.erase_firmware(
            is_direct,
            program_callback,
            py_object((self._DgUpdate, self.dg_erase))
        )
        self._DgEnd(self.dg_erase)

        self._DgStart(self.dg_write)
        libconcord.write_firmware_to_remote(
            bin_data,
            bin_size,
            c_int(is_direct and 1 or 0),
            program_callback,
            py_object((self._DgUpdate, self.dg_write))
        )
        self._DgEnd(self.dg_write)

        self._DgStart(self.dg_finalize)
        if not is_direct:
            libconcord.finish_firmware()
        self._DgEnd(self.dg_finalize)

        self._DgStart(self.dg_reconnect)
        libconcord.reset_remote()
        worker_body_connect(
            self.resources,
            self._DgUpdate,
            self.dg_reconnect,
            lambda: False,
            True
        )

        self._DgStart(self.dg_set_time)
        libconcord.set_time()
        self._DgEnd(self.dg_set_time)

        self._DgStart(self.dg_notify_website)
        libconcord.post_postfirmware(self.resources.xml, self.resources.xml_size)
        self._DgEnd(self.dg_notify_website)

    def GetTitle(self):
        return "Updating Firmware"

class SuccessPanel(MessagePanelBase):
    def __init__(self, parent, resources):
        MessagePanelBase.__init__(
            self,
            parent,
            resources
        )

    def OnActivated(self):
        self.text_message.UpdateText("Operation has completed successfully.")

    def OnCancel(self):
        self.parent.OnExit(0)

    def GetTitle(self):
        return "Success"

    def IsTerminal(self):
        return True

    def GetExitCode(self):
        return 0

    def IsNextInitiallyDisabled(self):
        return False

    def IsCancelInitiallyDisabled(self):
        return True

class FailurePanel(wx.Panel):
    def __init__(self, parent, resources):
        self.parent = parent
        self.resources = resources

        wx.Panel.__init__(self, parent)

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.text_message = WrappedStaticText(self)
        self.sizer.Add(self.text_message, 0, wx.ALIGN_LEFT | wx.ALL, 5)
        self.btn_details = wx.Button(self, -1, "&Details...")
        self.Bind(wx.EVT_BUTTON, self._OnDetails, self.btn_details)
        self.sizer.Add(self.btn_details, 0, wx.ALIGN_LEFT | wx.ALL, 5)
        self.SetSizerAndFit(self.sizer)

        self.message = ""
        self.log_text = ""

    def _OnDetails(self, event):
        show_modal_scrolled_msgbox(self.parent, "Error Log", self.log_text)
        self.parent.ReenableNext()

    def SetMessages(self, message, traceback):
        self.message = message
        if traceback:
            self.message += "\n\nSee below for details."
        self.log_text = traceback

    def OnActivated(self):
        if self.log_text:
            self.btn_details.SetFocus()
        else:
            self.btn_details.Hide()
        self.text_message.UpdateText(self.message)

    def OnCancel(self):
        self.parent.OnExit(1)

    def GetTitle(self):
        return "Failure"

    def IsTerminal(self):
        return True

    def GetExitCode(self):
        return 1

    def IsNextInitiallyDisabled(self):
        return False

    def IsCancelInitiallyDisabled(self):
        return True

class Wizard(wx.Dialog):
    def __init__(
        self,
        resources,
        app_finalizer,
        min_page_width = 450,
        min_page_height = None
    ):
        self.app_finalizer = app_finalizer

        self.min_page_width = min_page_width
        self.min_page_height = min_page_height

        wx.Dialog.__init__(self, None, -1, 'Congruity version ' + version)

        sizer_main = wx.BoxSizer(wx.VERTICAL)

        sizer_top = wx.BoxSizer(wx.HORIZONTAL)
        bitmap = wx.StaticBitmap(self, -1, resources.img_remote)
        sizer_top.Add(bitmap, 0, wx.EXPAND | wx.ALL, 5)

        self.sizer_top_right = wx.BoxSizer(wx.VERTICAL)
        self.title = wx.StaticText(self, -1, "Title")
        font = wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD)
        self.title.SetFont(font)
        self.sizer_top_right.Add(self.title, 0, wx.EXPAND)
        divider_top_right = wx.StaticLine(self, -1, None, None, wx.LI_HORIZONTAL)
        self.sizer_top_right.Add(divider_top_right, 0, wx.EXPAND)
        spacer = wx.StaticText(self, -1, "")
        self.sizer_top_right.Add(spacer, 0, wx.EXPAND)

        sizer_top.Add(self.sizer_top_right, 1, wx.EXPAND | wx.ALL, 5)
        sizer_main.Add(sizer_top, 1, wx.EXPAND | wx.ALL, 5)

        divider_main = wx.StaticLine(self, -1, None, None, wx.LI_HORIZONTAL)
        sizer_main.Add(divider_main, 0, wx.EXPAND | wx.ALL, 5)

        sizer_buttons = wx.BoxSizer(wx.HORIZONTAL)
        panel_btn_dummy = wx.Panel(self)
        sizer_buttons.Add(panel_btn_dummy, 1, wx.EXPAND | wx.ALL, 5)
        self.btn_next = wx.Button(self, -1, "&Next >")
        self.Bind(wx.EVT_BUTTON, self._OnNext, self.btn_next)
        sizer_buttons.Add(self.btn_next, 0, wx.EXPAND | wx.ALL, 5)
        self.btn_cancel = wx.Button(self, -1, "&Cancel")
        self.Bind(wx.EVT_BUTTON, self._OnCancel, self.btn_cancel)
        self.Bind(wx.EVT_CLOSE, self._OnCancel)
        sizer_buttons.Add(self.btn_cancel, 0, wx.EXPAND | wx.ALL, 5)
        sizer_main.Add(sizer_buttons, 0, wx.EXPAND | wx.ALL, 5)

        self.SetSizerAndFit(sizer_main)

        self.cur_page = None

    def SetPages(self, pages):
        def tuple_max(a, b):
            return (max(a[0], b[0]), max(a[1], b[1]))

        self.pages = pages

        for page in self.pages:
            page.Hide()

        size_wiz = self.GetSizeTuple()
        for page in self.pages:
            page.Show()
            self.sizer_top_right.Add(page, 1, wx.EXPAND)
            self.Fit()
            size_page = self.GetSizeTuple()
            size_wiz = tuple_max(size_wiz, size_page)
            page.Hide()
            self.sizer_top_right.Remove(page)

        if self.min_page_width and (size_wiz[0] < self.min_page_width):
            size_wiz = (self.min_page_width, size_wiz[1])

        if self.min_page_height and (size_wiz[1] < self.min_page_height):
            size_wiz = (size_wiz[0], self.min_page_height )

        self.SetSize(size_wiz)

    def SetInitialPage(self, page):
        if self.cur_page:
            raise Exception("Current page already set")
        self._SetPage(page)

    def OnExit(self, retcode):
        if self.app_finalizer:
            self.app_finalizer()
        os._exit(retcode)

    def _ReenableButton(self, button):
        button.Enable(True)
        button.Hide()
        button.Show()
        button.SetFocus()

    def ReenableNext(self):
        self._ReenableButton(self.btn_next)

    def ReenableCancel(self):
        self._ReenableButton(self.btn_cancel)

    def _OnNext(self, event):
        if self.cur_page.IsTerminal():
            self.OnExit(self.cur_page.GetExitCode())
        next_page = self.cur_page.GetNext()
        self._SetPage(next_page)

    def _OnCancel(self, event):
        self.cur_page.OnCancel()

    def _SetPage(self, page):
        if not page in self.pages:
            raise Exception("Invalid page")

        if self.cur_page:
            self.cur_page.Hide()
            self.sizer_top_right.Remove(self.cur_page)

        self.cur_page = page

        self.cur_page.Show()
        self.sizer_top_right.Add(self.cur_page, 1, wx.EXPAND)

        self.title.SetLabel(self.cur_page.GetTitle())

        self.Layout()

        is_terminal = self.cur_page.IsTerminal()
        if is_terminal:
            self.btn_next.SetLabel("&Finish")
        else:
            self.btn_next.SetLabel("&Next >")

        self.btn_next.Enable(not self.cur_page.IsNextInitiallyDisabled())
        self.btn_cancel.Enable(
            (not is_terminal)
            and
            (not self.cur_page.IsCancelInitiallyDisabled())
        )
        self.btn_next.SetFocus()

        self.cur_page.OnActivated()

class Resources(object):
    def __init__(self, appdir):
        self.appdir = appdir

        self.ezhex_filename = None
        self.xml = None
        self.xml_size = None
        self.connected = False

    def LoadImages(self):
        def load(filename, appdir = self.appdir):
            dirs = ['/usr/share/congruity', appdir, '.']
            for dir in dirs:
                fpath = os.path.join(dir, filename)
                if not os.path.isfile(fpath):
                    continue
                return wx.Image(fpath, wx.BITMAP_TYPE_PNG).ConvertToBitmap()
            raise Exception("Can't load " + filename)

        self.img_remote       = load("remote.png")
        self.icon_unstarted   = load("icon-unstarted.png")
        self.icon_in_progress = load("icon-in-progress.png")
        self.icon_complete    = load("icon-complete.png")
        self.icon_failed      = load("icon-failed.png")

        iw = max(
            self.icon_in_progress.GetWidth(),
            self.icon_complete.GetWidth(),
            self.icon_failed.GetWidth()
        )
        ih = max(
            self.icon_in_progress.GetHeight(),
            self.icon_complete.GetHeight(),
            self.icon_failed.GetHeight()
        )
        self.iwh = (iw, ih)

    def CreatePages(self, wizard):
        self.page_welcome = WelcomePanel(wizard, self)
        self.page_connect = ConnectPanel(wizard, self)
        self.page_check_connectivity = CheckConnectivityPanel(wizard, self)
        self.page_write_configuration = WriteConfigurationPanel(wizard, self)
        self.page_update_firmware = UpdateFirmwarePanel(wizard, self)
        self.page_success = SuccessPanel(wizard, self)
        self.page_failure = FailurePanel(wizard, self)

    def SetEzHexFilename(self, ezhex_filename):
        self.ezhex_filename = ezhex_filename

    def SetXmlData(self, xml, xml_size):
        self.xml = xml
        self.xml_size = xml_size

    def SetConnected(self, connected):
        self.connected = connected

class Finalizer(object):
    def __init__(self, resources):
        self.resources = resources

    def __call__(self):
        if self.resources.xml:
            try:
                xml = self.resources.xml
                self.resources.xml = None
                self.resources.xml_size = None
                libconcord.delete_blob(xml)
            except:
                pass

        if self.resources.connected:
            try:
                self.resources.SetConnected(False)
                libconcord.deinit_concord()
            except:
                pass

def main(argv):
    app = argv.pop(0)
    appdir = os.path.dirname(app)

    try:
        while len(argv) and argv[0].startswith('-'):
            arg = argv.pop(0)
            if arg == '--version':
                print version
                return
            else:
                raise CmdLineException("ERROR: Option '%s' not recognized" % arg)
                sys.exit(0)
        if len(argv) != 1:
            raise CmdLineException("ERROR: Precisely one filename argument is required")
        ezhex_filename = argv.pop(0)
        initial_exception = None
    except:
        ezhex_filename = None
        initial_exception = ("Command-line error", exception_message())
        
    app = wx.PySimpleApp()
    wx.InitAllImageHandlers()

    resources = Resources(appdir)
    resources.LoadImages()
    resources.SetEzHexFilename(ezhex_filename)

    wizard = Wizard(resources, Finalizer(resources))

    resources.CreatePages(wizard)
    if initial_exception:
        resources.page_welcome.SetInitialException(initial_exception)
    wizard.SetPages([
        resources.page_welcome,
        resources.page_connect,
        resources.page_check_connectivity,
        resources.page_write_configuration,
        resources.page_update_firmware,
        resources.page_success,
        resources.page_failure
    ])
    wizard.SetInitialPage(resources.page_welcome)

    wizard.Show()

    app.MainLoop()

if __name__ == "__main__":
    main(sys.argv)

