#!/usr/bin/env python

# Copyright 2012-2013 Scott Talbert
#
# 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 traceback

import wxversion
wxversion.ensureMinimal('2.8')
import wx
from tempfile import NamedTemporaryFile
sys.path.append('/usr/share/congruity')
from mhmanager import MHManager
from mhmanager import MHAccountDetails

version = "17"

use_local_wsdl = False
if '--use-local-wsdl' in sys.argv:
    use_local_wsdl = True
mhMgr = MHManager(use_local_wsdl)

try:
    import libconcord
except:
    str = traceback.format_exc()
    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.\n\n" + str,
        "congruity: Dependency Error",
        wx.OK | wx.ICON_ERROR
    )
    dlg.ShowModal()
    os._exit(1)

ALIGN_LTA = wx.ALIGN_LEFT  | wx.ALIGN_TOP             | wx.ALL
ALIGN_XTA = wx.EXPAND      | wx.ALIGN_TOP             | wx.ALL
ALIGN_LCA = wx.ALIGN_LEFT  | wx.ALIGN_CENTER_VERTICAL | wx.ALL
ALIGN_RCA = wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL | wx.ALL
ALIGN_XCA = wx.EXPAND      | wx.ALIGN_CENTER_VERTICAL | wx.ALL
ALIGN_LBA = wx.ALIGN_LEFT  | wx.ALIGN_BOTTOM          | wx.ALL
ALIGN_XBA = wx.EXPAND      | wx.ALIGN_BOTTOM          | wx.ALL

def dummy_callback_imp(stage_id, count, current, total, type, context, stages):
    pass

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.Layout()
        self.parent.Layout()

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

        wx.Panel.__init__(self, parent)

    def OnActivated(self, prev_page, data):
        return (None, None)

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

    def GetTitle(self):
        return "Base"

    def IsTerminal(self):
        return False

    def IsBackInitiallyDisabled(self):
        return True

    def IsNextInitiallyDisabled(self):
        return True

    def IsCloseInitiallyDisabled(self):
        return True

    def IsCancelInitiallyDisabled(self):
        return True

    def GetExitCode(self):
        return 0

    def GetBack(self):
        return (None, None)

    def GetNext(self):
        return (None, None)

    def StartBusy(self):
        self.parent.Hide()
        self.busy = wx.BusyInfo("Please wait...")
        wx.MilliSleep(100)
        wx.Yield()

    def EndBusy(self):
        self.busy = None
        self.parent.Show()

class WelcomePanel(WizardPanelBase):
    _msg_welcome = (
        "Welcome to MHGUI: an application for accessing " +
        "Logitech's MyHarmony website.\n\n" +
        "Please enter your MyHarmony.com username and " +
        "password below.\n"
    )

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

        WizardPanelBase.__init__(self, parent, resources)

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.textMessage = WrappedStaticText(self)
        self.sizer.Add(self.textMessage, 0, ALIGN_XTA, 5)
        self.usernameLabel = wx.StaticText(self, -1, "Username:")
        self.usernameCtrl = wx.TextCtrl(self, -1, "")
        self.usernameCtrl.SetMinSize((200, 31))
        self.usernameSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.usernameSizer.Add(self.usernameLabel, 0, ALIGN_LCA, 0)
        self.usernameSizer.Add(self.usernameCtrl, 0, ALIGN_RCA, 0)
        self.sizer.Add(self.usernameSizer, 0, wx.EXPAND, 0)
        self.passwordText = wx.StaticText(self, -1, "Password:")
        self.passwordCtrl = wx.TextCtrl(self, -1, "", style=wx.TE_PASSWORD)
        self.passwordCtrl.SetMinSize((200, 31))
        self.passwordSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.passwordSizer.Add(self.passwordText, 0, ALIGN_LCA, 0)
        self.passwordSizer.Add(self.passwordCtrl, 0, ALIGN_RCA, 0)
        self.sizer.Add(self.passwordSizer, 0, wx.EXPAND, 0)
        self.sizer.AddSpacer(25)
        self.createAccountButton = wx.Button(self, label="Create Account")
        self.createAccountButton.Bind(wx.EVT_BUTTON, self.OnCreateAccount)
        self.sizer.Add(self.createAccountButton)

        self.SetSizerAndFit(self.sizer)

        self.next = None

    def _WorkerFunction(self):
        wx.CallAfter(
            self.textMessage.UpdateText,
            self._msg_welcome
        )

        self.next = self.resources.page_remote_select
        self.parent.ReenableNext()

    def OnActivated(self, prev_page, data):
        thread.start_new_thread(self._WorkerFunction, ())
        return (None, None)

    def GetTitle(self):
        return "Welcome"

    def IsCancelInitiallyDisabled(self):
        return False

    def OnNext(self):
        if mhMgr.Login(self.usernameCtrl.GetValue(),
                       self.passwordCtrl.GetValue()) is True:
            return True
        else:
            wx.MessageBox('Login failed.', 'Login failed.', wx.OK |
                          wx.ICON_WARNING)
            self.usernameCtrl.Clear()
            self.passwordCtrl.Clear()
            return False

    def GetNext(self):
        return (self.next, None)

    def OnCreateAccount(self, event):
        self.parent._SetPage(self.resources.page_create_account, None)

class RemoteSelectPanel(WizardPanelBase):
    _msg_welcome = (
        "Please select a remote control below.\n\n" +
        "Remotes (maximum of 6 per account):"
    )

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

        WizardPanelBase.__init__(self, parent, resources)

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.textMessage = WrappedStaticText(self)
        self.sizer.Add(self.textMessage, 0, ALIGN_XTA, 5)
        self.remotesListBox = wx.ListBox(self, style=wx.LB_SINGLE,
                                         size=(134, 75))
        self.sizer.Add(self.remotesListBox)
        self.sizer.AddSpacer(20)
        self.addButton = wx.Button(self, label="Add New Remote",
                                   size=(127, 33))
        self.addButton.Bind(wx.EVT_BUTTON, self.OnAddRemote)
        self.sizer.Add(self.addButton)
        self.sizer.AddSpacer(10)
        self.updateAccountButton = wx.Button(self, label="Update Account",
                                             size=(127,33))
        self.updateAccountButton.Bind(wx.EVT_BUTTON, self.OnUpdateAccount)
        self.updateAccountButton.SetToolTip(wx.ToolTip(
                "Update User Account Details (password, etc.)"
        ))
        self.sizer.Add(self.updateAccountButton)
        self.SetSizerAndFit(self.sizer)

        self.next = None

    def _WorkerFunction(self):
        wx.CallAfter(
            self.textMessage.UpdateText,
            self._msg_welcome
        )

        # Get list of remotes for this account from the web service
        wx.CallAfter(self.StartBusy)
        wx.CallAfter(self.UpdateRemotes)
        wx.CallAfter(self.EndBusy)

        wx.CallAfter(self.parent.ReenableNext)

    def UpdateRemotes(self):
        self.remotes = mhMgr.GetRemotes()
        self.remoteDisplayNames = []
        for remote in self.remotes:
            product = mhMgr.GetProduct(remote.SkinId)
            self.remoteDisplayNames.append(product.DisplayName)
        self.remotesListBox.Clear()
        self.remotesListBox.Set(self.remoteDisplayNames)
        self.Layout()

    def OnActivated(self, prev_page, data):
        thread.start_new_thread(self._WorkerFunction, ())
        return (None, None)

    def GetTitle(self):
        return "Remote Selection"

    def IsCancelInitiallyDisabled(self):
        return False

    def OnAddRemote(self, event):
        if len(self.remotes) >= 6:
            wx.MessageBox('Each account can support up to 6 remotes.',
                          'Maximum Number of Remotes Reached',
                          wx.OK | wx.ICON_WARNING)
            return            
        supportedRemotes = mhMgr.GetSupportedRemoteNames()
        msg = 'Please ensure your remote control is connected.'
        msg += '\n\nSupported models are:'
        for name in supportedRemotes:
            msg += '\n' + name
        wx.MessageBox(msg, 'Connect Remote', wx.OK)

        try:
            libconcord.init_concord()
        except:
            msg = '%s\n    (libconcord function %s error %d)\n\n' % (
                sys.exc_value.result_str,
                sys.exc_value.func,
                sys.exc_value.result
            )
            wx.MessageBox('Could not detect remote: ' + msg, 'Error',
                          wx.OK | wx.ICON_WARNING)
            return

        cb = libconcord.callback_type(dummy_callback_imp)
        try:
            libconcord.get_identity(cb, None)
        except:
            msg = '%s\n    (libconcord function %s error %d)\n\n' % (
                sys.exc_value.result_str,
                sys.exc_value.func,
                sys.exc_value.result
            )
            wx.MessageBox('Could not identify remote: ' + msg, 'Error',
                          wx.OK | wx.ICON_WARNING)
            libconcord.deinit_concord()
            return

        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)
        serialNumber = ser_1 + ser_2 + ser_3
        skinId = libconcord.get_skin()
        usbPid = hex(libconcord.get_usb_pid())
        usbVid = hex(libconcord.get_usb_vid())
        model = libconcord.get_mfg() + ' ' + libconcord.get_model()
        libconcord.deinit_concord()
        # Check whether this is a supported remote.
        supportedSkins = mhMgr.GetSupportedRemoteSkinIds()
        if skinId not in supportedSkins:
            wx.MessageBox('Sorry, this remote model (' + model + ') is '
                          + 'not supported by this software.  Please '
                          + 'use members.harmonyremote.com.',
                          'Unsupported Remote', wx.OK | wx.ICON_WARNING)
            return
        # Check whether this remote is already on this account.
        for remote in self.remotes:
            if remote.SerialNumber == serialNumber:
                wx.MessageBox('Sorry, this remote already exists in '
                              + 'your account.', 'Existing Remote',
                              wx.OK | wx.ICON_WARNING)
                return
        # Actually add the remote!
        result = mhMgr.AddRemote(serialNumber, skinId, usbPid, usbVid)
        if result is not None:
            wx.MessageBox('Remote successfully added.', 'Success',
                          wx.OK)
            self.UpdateRemotes()
            return
        else:
            wx.MessageBox('Remote addition failed.', 'Failure',
                          wx.OK, wx.ICON_WARNING)
            return

    def OnUpdateAccount(self, event):
        self.parent._SetPage(self.resources.page_create_account, True)

    def OnNext(self):
        if self.remotesListBox.GetSelection() != -1:
            self.next = self.resources.page_remote_configuration
            self.StartBusy()
            self.resources.page_remote_configuration.SetRemote(
                self.remotes[self.remotesListBox.GetSelection()])
            self.EndBusy()
            return True
        else:
            wx.MessageBox('Please make a selection.', 'No selection made.',
                          wx.OK | wx.ICON_WARNING)
            return False

    def GetNext(self):
        return (self.next, None)

class RemoteConfigurationPanel(WizardPanelBase):
    _msg_welcome = (
        "Please make a selection below.\n"
    )

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

        WizardPanelBase.__init__(self, parent, resources)

        buttonSize = (159, 33) # Size of largest button
        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.textMessage = wx.StaticText(self)
        self.sizer.Add(self.textMessage, 0, ALIGN_XTA, 5)
        self.devicesListBox = wx.ListBox(self, style=wx.LB_SINGLE)
        self.sizer.Add(self.devicesListBox, 0, 0, 0)
        self.sizer.AddSpacer(20)
        self.buttonSizer = wx.BoxSizer(wx.HORIZONTAL)

        self.leftButtonSizer = wx.BoxSizer(wx.VERTICAL)
        self.addButton = wx.Button(self, label="Add Device", size=buttonSize)
        self.addButton.Bind(wx.EVT_BUTTON, self.OnAdd)
        self.addButton.SetToolTip(wx.ToolTip(
                "Add a device (TV, cable box, etc) to this remote"
        ))
        self.leftButtonSizer.Add(self.addButton, 0, 0, 0)
        self.deleteButton = wx.Button(self, label="Delete Device",
                                      size=buttonSize)
        self.deleteButton.Bind(wx.EVT_BUTTON, self.OnDelete)
        self.leftButtonSizer.Add(self.deleteButton, 0, 0, 0)
        self.renameButton = wx.Button(self, label="Rename Device",
                                      size=buttonSize)
        self.renameButton.Bind(wx.EVT_BUTTON, self.OnRename)
        self.leftButtonSizer.Add(self.renameButton, 0, 0, 0)
        self.configureButton = wx.Button(self, label="Configure Device",
                                         size=buttonSize)
        self.configureButton.Bind(wx.EVT_BUTTON, self.OnConfigure)
        self.configureButton.SetToolTip(wx.ToolTip(
                "Adjust/fix button functionality for a device"
        ))
        self.leftButtonSizer.Add(self.configureButton, 0, 0, 0)
        self.buttonSizer.Add(self.leftButtonSizer, 0, 0, 0)

        self.rightButtonSizer = wx.BoxSizer(wx.VERTICAL)
        self.favoriteChannelsButton = wx.Button(self,
                                                label="Edit Favorite Channels",
                                                size=buttonSize)
        self.favoriteChannelsButton.Bind(wx.EVT_BUTTON, self.OnFavoriteChannels)
        self.rightButtonSizer.Add(self.favoriteChannelsButton, 0, 0, 0)
        self.syncButton = wx.Button(self, label="Sync Remote",
                                    size=buttonSize)
        self.syncButton.Bind(wx.EVT_BUTTON, self.OnSync)
        self.syncButton.SetToolTip(wx.ToolTip(
                "Update the configuration on your remote"
        ))
        self.rightButtonSizer.Add(self.syncButton, 0, 0, 0)
        self.downloadConfigButton = wx.Button(self, label="Download Config",
                                              size=buttonSize)
        self.downloadConfigButton.Bind(wx.EVT_BUTTON, self.OnDownloadConfig)
        self.downloadConfigButton.SetToolTip(wx.ToolTip(
                "Download the config for your remote and save it to a file"
        ))
        self.rightButtonSizer.Add(self.downloadConfigButton, 0, 0, 0)
        self.buttonSizer.Add(self.rightButtonSizer, 0, 0, 0)

        self.sizer.Add(self.buttonSizer, 0, 0, 0)
        self.SetSizerAndFit(self.sizer)

        self.next = None

    def SetRemote(self, remote):
        self.remote = remote
        self.product = mhMgr.GetProduct(remote.SkinId)
        self._msg_welcome = "Remote Configuration for " \
            + self.product.DisplayName + "\n\nDevices (maximum of " \
            + str(self.product.MaxDevicesPerAccount) + "):"
        self.textMessage.SetLabel(self._msg_welcome)
        self.PopulateDevicesList()
        self.Layout()

    def PopulateDevicesList(self):
        self.devices = mhMgr.GetDevices(self.remote.Id)
        self.devicesList = []
        if self.devices is not None:
            for device in self.devices:
                self.devicesList.append(device.Name)
            self.devicesListBox.Set(self.devicesList)
        else:
            self.devicesListBox.Clear()

    def OnAdd(self, event):
        if len(self.devicesList) < self.product.MaxDevicesPerAccount:
            self.parent._SetPage(self.resources.page_add_device, self.remote)
        else:
            wx.MessageBox('Remote cannot support additional devices.',
                          'Cannot Add Device.', wx.OK | wx.ICON_WARNING)

    def OnDelete(self, event):
        deviceToDelete = self.devicesListBox.GetSelections()
        if deviceToDelete != ():
            dlg = wx.MessageDialog(self.parent,
                                   'Are you sure you want to delete '
                                   + self.devicesListBox.GetStringSelection()
                                   + '?', 'Delete Confirmation',
                                   wx.YES_NO | wx.ICON_QUESTION)
            result = dlg.ShowModal() == wx.ID_YES
            dlg.Destroy()
            if result:
                mhMgr.DeleteDevice(self.devices[deviceToDelete[0]].Id)
                self.PopulateDevicesList()
                self.Layout()
        else:
            wx.MessageBox('Please select a device to delete.',
                          'No selection made.', wx.OK | wx.ICON_WARNING)

    def OnRename(self, event):
        deviceToRename = self.devicesListBox.GetSelections()
        if deviceToRename != ():
            dlg = wx.TextEntryDialog(self.parent,
                                     'Enter new name for device "'
                                     + self.devicesListBox.GetStringSelection()
                                     + '":', 'Rename Device')
            result = dlg.ShowModal() == wx.ID_OK
            newName = dlg.GetValue()
            dlg.Destroy()
            if result:
                mhMgr.RenameDevice(self.devices[deviceToRename[0]].Id, newName)
                self.PopulateDevicesList()
                self.Layout()
        else:
            wx.MessageBox('Please select a device to rename.',
                          'No selection made.', wx.OK | wx.ICON_WARNING)

    def OnConfigure(self, event):
        deviceToConfigure = self.devicesListBox.GetSelections()
        if deviceToConfigure != ():
            self.StartBusy()
            self.resources.page_configure_device.SetDevice(
                self.remote.SkinId,
                self.devices[deviceToConfigure[0]].Id,
                self.devices[deviceToConfigure[0]].Name
            )
            self.EndBusy()
            self.parent._SetPage(self.resources.page_configure_device, None)
        else:
            wx.MessageBox('Please select a device to configure.',
                          'No selection made.', wx.OK | wx.ICON_WARNING)

    def OnFavoriteChannels(self, event):
        deviceToConfigure = self.devicesListBox.GetSelections()
        if deviceToConfigure != ():
            self.StartBusy()
            self.resources.page_favorite_channels.SetDevice(
                self.remote.SkinId,
                self.devices[deviceToConfigure[0]].Id,
                self.devices[deviceToConfigure[0]].Name
            )
            self.EndBusy()
            self.parent._SetPage(self.resources.page_favorite_channels, None)
        else:
            wx.MessageBox('Please select a device for which to configure the '
                          + "favorite channels.", 'No selection made.',
                          wx.OK | wx.ICON_WARNING)

    def OnSync(self, event):
        tempFile = NamedTemporaryFile(delete=False)
        tempFile.close()
        self.StartBusy()
        mhMgr.GetConfig(self.remote, tempFile.name)
        self.EndBusy()
        self.Hide()
        os.system("./congruity --no-web " + tempFile.name)
        os.unlink(tempFile.name)
        self.Show()

    def OnDownloadConfig(self, event):
        dialog = wx.FileDialog(
            self,
            message = "Save configuration as...", 
            defaultFile = "config.zip",
            style = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT
        )
        if dialog.ShowModal() == wx.ID_OK:
            filename = dialog.GetPath()
            self.StartBusy()
            mhMgr.GetConfig(self.remote, filename)
            self.EndBusy()
            wx.MessageBox('Config file saved to ' + filename,
                          'Download complete.', wx.OK)

    def OnActivated(self, prev_page, data):
        self.parent.ReenableBack()
        return (None, None)

    def GetTitle(self):
        return "Remote Configuration"

    def IsCancelInitiallyDisabled(self):
        return False

    def OnNext(self):
        pass

    def GetNext(self):
        return (self.next, None)

    def GetBack(self):
        return (self.resources.page_remote_select, None)

class AddDevicePanel(WizardPanelBase):
    _msg_welcome = (
        "Please enter the manufacturer and model number of your device below.\n"
    )

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

        WizardPanelBase.__init__(self, parent, resources)

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.textMessage = WrappedStaticText(self)
        self.sizer.Add(self.textMessage, 0, ALIGN_XTA, 5)
        self.manufacturerLabel = wx.StaticText(self, -1, "Manufacturer:")
        self.manufacturerCtrl = wx.TextCtrl(self, -1, "")
        self.manufacturerCtrl.SetMinSize((200, 31))
        self.manufacturerSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.manufacturerSizer.Add(self.manufacturerLabel, 0, ALIGN_LCA, 0)
        self.manufacturerSizer.Add(self.manufacturerCtrl, 0, ALIGN_RCA, 0)
        self.sizer.Add(self.manufacturerSizer, 0, wx.EXPAND, 0)
        self.modelNumberText = wx.StaticText(self, -1, "Model Number:")
        self.modelNumberCtrl = wx.TextCtrl(self, -1, "")
        self.modelNumberCtrl.SetMinSize((200, 31))
        self.modelNumberSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.modelNumberSizer.Add(self.modelNumberText, 0, ALIGN_LCA, 0)
        self.modelNumberSizer.Add(self.modelNumberCtrl, 0, ALIGN_RCA, 0)
        self.sizer.Add(self.modelNumberSizer, 0, wx.EXPAND, 0)
        self.searchButton = wx.Button(self, label="Search")
        self.searchButton.Bind(wx.EVT_BUTTON, self.OnSearch)
        self.sizer.Add(self.searchButton, 0, 0, 0)

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

        self.searchResultsText = wx.StaticText(self, -1, "Search Results:")
        self.sizer.Add(self.searchResultsText, 0, 0, 0)
        self.searchResultsListBox = wx.ListBox(self, style=wx.LB_SINGLE, 
                                               size=(300, 150))
        self.sizer.Add(self.searchResultsListBox, 0, 0, 0)
        self.addButton = wx.Button(self, label="Add Device")
        self.addButton.Bind(wx.EVT_BUTTON, self.OnAdd)
        self.sizer.Add(self.addButton, 0, 0, 0)

        self.SetSizerAndFit(self.sizer)

        self.next = None

    def _WorkerFunction(self):
        wx.CallAfter(
            self.textMessage.UpdateText,
            self._msg_welcome
        )

    def OnActivated(self, prev_page, data):
        self.remote = data
        self.ClearPage()
        self.parent.ReenableBack()
        thread.start_new_thread(self._WorkerFunction, ())
        return (None, None)

    def OnSearch(self, event):
        self.searchResultsList = []
        if self.manufacturerCtrl.IsEmpty():
            wx.MessageBox('Please enter a manufacturer.',
                          'No manufacturer entered.', wx.OK | wx.ICON_WARNING)
        elif self.modelNumberCtrl.IsEmpty():
            wx.MessageBox('Please enter a model number.',
                          'No model number entered.', wx.OK | wx.ICON_WARNING)
        else:
            searchResults = mhMgr.SearchDevices(
                self.manufacturerCtrl.GetValue(),
                self.modelNumberCtrl.GetValue(),
                5)
            if searchResults.Status == "NoMatchFound":
                wx.MessageBox('Sorry, no devices were found.',
                              'No devices found.', wx.OK | wx.ICON_WARNING)
            else:
                self.matches = searchResults.Matches.PublicDeviceSearchMatch
                for match in self.matches:
                    self.searchResultsList.append(match.Manufacturer + " "
                                                  + match.DeviceModel)
        self.searchResultsListBox.Set(self.searchResultsList)

    def OnAdd(self, event):
        deviceToAdd = self.searchResultsListBox.GetSelections()
        if deviceToAdd != ():
            self.StartBusy()
            result = mhMgr.AddDevice(self.matches[deviceToAdd[0]],
                                     self.remote.Id)
            if result is None:
                self.EndBusy()
                wx.MessageBox('Device addition failed!',
                              'Failure', wx.OK | wx.ICON_WARNING)
            else:
                self.resources.page_remote_configuration.PopulateDevicesList()
                self.EndBusy()
                self.parent._SetPage(self.resources.page_remote_configuration,
                                     None)
        else:
            wx.MessageBox('Please select a device to add.',
                          'No selection made.', wx.OK | wx.ICON_WARNING)

    def ClearPage(self):
        self.manufacturerCtrl.Clear()
        self.modelNumberCtrl.Clear()
        self.searchResultsListBox.Clear()

    def GetTitle(self):
        return "Device Addition"

    def IsCancelInitiallyDisabled(self):
        return False

    def OnNext(self):
        pass

    def GetNext(self):
        return (self.next, None)

    def GetBack(self):
        return (self.resources.page_remote_configuration, None)

class CreateAccountPanel(WizardPanelBase):
    _msg_welcome_create = (
        "Please enter the information below in order to create an account.\n"
    )
    _msg_welcome_update = (
        "Please update the information below.\n"
    )

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

        WizardPanelBase.__init__(self, parent, resources)

        self.securityQuestionChoices = [
            "What was the make of your first car?",
            "What's your mother's maiden name?",
            "What's the name of your favorite sports team?",
            "What's your first pet's name?",
            "Who is your all-time favorite movie character?" ]
        self.securityQuestionTokens = [
            "QuestionCar",
            "QuestionMothersMaidenName",
            "QuestionSportsTeam",
            "QuestionFirstPet",
            "QuestionMovieCharacter" ]

        # Probably should do this in a thread or something as this makes a call
        # to the website.
        self.countryLists = mhMgr.GetCountryLists()
        self.countryCodes = self.countryLists[0]
        self.countries = self.countryLists[1]

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.textMessage = WrappedStaticText(self)
        self.sizer.Add(self.textMessage, 0, ALIGN_XTA, 5)
    
        self.lineOneSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.firstNameLabel = wx.StaticText(self, -1, "First Name:")
        self.lineOneSizer.Add(self.firstNameLabel, 0, ALIGN_LCA)
        self.firstNameCtrl = wx.TextCtrl(self, size=(120,31))
        self.firstNameCtrl.SetMaxLength(30)
        self.lineOneSizer.Add(self.firstNameCtrl)
        self.lineOneSizer.AddSpacer(25)
        self.lastNameLabel = wx.StaticText(self, -1, "Last Name:")
        self.lineOneSizer.Add(self.lastNameLabel, 0, ALIGN_LCA)
        self.lastNameCtrl = wx.TextCtrl(self, size=(120,31))
        self.lastNameCtrl.SetMaxLength(30)
        self.lineOneSizer.Add(self.lastNameCtrl)
        self.sizer.Add(self.lineOneSizer)
        self.sizer.AddSpacer(12)

        self.lineTwoSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.countryLabel = wx.StaticText(self, -1, "Country:")
        self.lineTwoSizer.Add(self.countryLabel, 0, ALIGN_LCA)
        self.countryChoice = wx.Choice(self, -1, choices=self.countries)
        self.lineTwoSizer.Add(self.countryChoice)
        self.sizer.Add(self.lineTwoSizer)
        self.sizer.AddSpacer(12)

        self.lineThreeSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.emailAddressLabel = wx.StaticText(self, -1, "Email Address:")
        self.lineThreeSizer.Add(self.emailAddressLabel, 0, ALIGN_LCA)
        self.emailAddressCtrl = wx.TextCtrl(self, size=(170,31))
        self.emailAddressCtrl.SetMaxLength(50)
        self.lineThreeSizer.Add(self.emailAddressCtrl)
        self.sizer.Add(self.lineThreeSizer)
        self.sizer.AddSpacer(12)

        self.lineFourSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.passwordLabel = wx.StaticText(self, -1, "Password:")
        self.lineFourSizer.Add(self.passwordLabel, 0, ALIGN_LCA)
        self.passwordCtrl = wx.TextCtrl(self, -1, "", style=wx.TE_PASSWORD, 
                                        size=(120, 31))
        self.passwordCtrl.SetMaxLength(30)
        self.lineFourSizer.Add(self.passwordCtrl)
        self.lineFourSizer.AddSpacer(25)
        self.retypePasswordLabel = wx.StaticText(self, -1, "Confirm Password:")
        self.lineFourSizer.Add(self.retypePasswordLabel, 0, ALIGN_LCA)
        self.retypePasswordCtrl = wx.TextCtrl(self, -1, "",
                                              style=wx.TE_PASSWORD,
                                              size=(120, 31))
        self.retypePasswordCtrl.SetMaxLength(30)
        self.lineFourSizer.Add(self.retypePasswordCtrl)
        self.sizer.Add(self.lineFourSizer)
        self.sizer.AddSpacer(12)

        self.lineFiveSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.securityQuestionLabel = wx.StaticText(self, -1,
                                                   "Security Question:")
        self.lineFiveSizer.Add(self.securityQuestionLabel, 0, ALIGN_LCA)
        self.securityQuestionChoice = wx.Choice(self, -1,
            choices=self.securityQuestionChoices)
        self.lineFiveSizer.Add(self.securityQuestionChoice)
        self.sizer.Add(self.lineFiveSizer)
        self.sizer.AddSpacer(12)

        self.lineSixSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.securityAnswerLabel = wx.StaticText(self, -1, "Security Answer:")
        self.lineSixSizer.Add(self.securityAnswerLabel, 0, ALIGN_LCA)
        self.securityAnswerCtrl = wx.TextCtrl(self, size=(170,31))
        self.securityAnswerCtrl.SetMaxLength(30)
        self.lineSixSizer.Add(self.securityAnswerCtrl)
        self.sizer.Add(self.lineSixSizer)
        self.sizer.AddSpacer(12)

        self.lineSevenSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.termsCheckBox = wx.CheckBox(self,
            label="I agree to Terms of Use and Privacy Policy")
        self.sizer.Add(self.termsCheckBox)
        self.contactCheckBox = wx.CheckBox(self,
            label="I agree to receive a survey from Logitech")
        self.sizer.Add(self.contactCheckBox)

        self.lineEightSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.termsOfUseLink = wx.HyperlinkCtrl(self, -1, "Terms of Use",
            "http://files.myharmony.com/Assets/legal/en/termsofuse.html")
        self.lineEightSizer.Add(self.termsOfUseLink)
        self.lineEightSizer.AddSpacer(10)
        self.privacyPolicyLink = wx.HyperlinkCtrl(self, -1, "Privacy Policy",
            "http://files.myharmony.com/Assets/legal/en/privacypolicy.html")
        self.lineEightSizer.Add(self.privacyPolicyLink)
        self.sizer.Add(self.lineEightSizer)

        self.SetSizerAndFit(self.sizer)

        self.next = None

    def _WorkerFunction(self):
        wx.CallAfter(
            self.textMessage.UpdateText,
            self._msg_welcome
        )
        self.parent.ReenableNext()
        self.parent.ReenableBack()

    def OnActivated(self, prev_page, data):
        if data is not None:
            self.isUpdate = True
            self.parent.title.SetLabel("Account Update")
            self._msg_welcome = self._msg_welcome_update
            self.PopulateData()
        else:
            self.isUpdate = False
            self._msg_welcome = self._msg_welcome_create
            self.ClearData()
        thread.start_new_thread(self._WorkerFunction, ())
        return (None, None)

    def PopulateData(self):
        details = mhMgr.GetAccountDetails()
        self.firstNameCtrl.SetValue(details.firstName)
        self.lastNameCtrl.SetValue(details.lastName)
        self.countryChoice.SetSelection(
            self.countryCodes.index(details.country)
        )
        self.emailAddressCtrl.SetValue(details.email)
        self.passwordCtrl.SetValue(details.password) 
        self.retypePasswordCtrl.SetValue(details.password)
        self.securityQuestionChoice.SetSelection(
            self.securityQuestionTokens.index(details.securityQuestion)
        )
        self.securityAnswerCtrl.Clear()
        self.termsCheckBox.SetValue(True)
        if str(details.keepMeInformed).lower() == "true":
            self.contactCheckBox.SetValue(True)
        else:
            self.contactCheckBox.SetValue(False)

    def ClearData(self):
        self.firstNameCtrl.Clear()
        self.lastNameCtrl.Clear()
        self.countryChoice.SetSelection(0)
        self.emailAddressCtrl.Clear()
        self.passwordCtrl.Clear()
        self.retypePasswordCtrl.Clear()
        self.securityQuestionChoice.SetSelection(0)
        self.securityAnswerCtrl.Clear()
        self.termsCheckBox.SetValue(False)
        self.contactCheckBox.SetValue(False)

    def GetTitle(self):
        return "Account Creation"

    def IsCancelInitiallyDisabled(self):
        return False

    def OnNext(self):
        if self.firstNameCtrl.IsEmpty():
            wx.MessageBox('First Name Required.',
                          'Error', wx.OK | wx.ICON_WARNING)
            return
        if self.lastNameCtrl.IsEmpty():
            wx.MessageBox('Last Name Required.',
                          'Error', wx.OK | wx.ICON_WARNING)
            return
        if self.countryChoice.GetSelection() == 0:
            wx.MessageBox('Country Selection Required.',
                          'Error', wx.OK | wx.ICON_WARNING)
            return
        if self.emailAddressCtrl.IsEmpty():
            wx.MessageBox('Email Address Required.',
                          'Error', wx.OK | wx.ICON_WARNING)
            return
        if len(self.passwordCtrl.GetValue()) < 4:
            wx.MessageBox('Passwords must be at least 4 characters.',
                          'Error', wx.OK | wx.ICON_WARNING)
            return
        if self.passwordCtrl.GetValue() != self.retypePasswordCtrl.GetValue():
            wx.MessageBox('Passwords do not match.',
                          'Error', wx.OK | wx.ICON_WARNING)
            return
        if (not self.isUpdate) and (self.securityAnswerCtrl.IsEmpty()):
            wx.MessageBox('Security Answer Required.',
                          'Error', wx.OK | wx.ICON_WARNING)
            return
        if self.termsCheckBox.GetValue() is False:
            wx.MessageBox('Must accept terms and conditions.',
                          'Error', wx.OK | wx.ICON_WARNING)
            return

        mhAccount = MHAccountDetails()
        mhAccount.firstName = self.firstNameCtrl.GetValue()
        mhAccount.lastName = self.lastNameCtrl.GetValue()
        mhAccount.country = self.countryCodes[self.countryChoice.GetSelection()]
        mhAccount.email = self.emailAddressCtrl.GetValue()
        mhAccount.password = self.passwordCtrl.GetValue()
        mhAccount.securityQuestion = \
            self.securityQuestionTokens[self.securityQuestionChoice.GetSelection()]
        mhAccount.securityAnswer = self.securityAnswerCtrl.GetValue()
        if self.contactCheckBox.GetValue() is True:
            mhAccount.keepMeInformed = "true"
        else:
            mhAccount.keepMeInformed = "false"

        if not self.isUpdate:
            result = mhMgr.CreateAccount(mhAccount)
            if result is None:
                wx.MessageBox('Account created successfully.',
                              'Success', wx.OK)
                self.parent._SetPage(self.resources.page_welcome, None)
            else:
                wx.MessageBox(result, 'Error', wx.OK | wx.ICON_WARNING)
                return
        else:
            result = mhMgr.UpdateAccountDetails(mhAccount)
            if result:
                wx.MessageBox('Account updated successfully.', 'Success',
                              wx.OK)
                self.parent._SetPage(self.resources.page_remote_select, None)
            else:
                wx.MessageBox('Account update failed.', 'Failure',
                              wx.OK | wx.ICON_WARNING)
                return

    def GetNext(self):
        return (self.next, None)

    def GetBack(self):
        if self.isUpdate:
            return (self.resources.page_remote_select, None)
        else:
            return (self.resources.page_welcome, None)

class ConfigureDevicePanel(WizardPanelBase):
    _msg_welcome = (
        "Please select a remote control button to modify on the left.\n" +
        "Then, select a device command to assign to that button on the right.\n"
        + "If there is an existing device command assigned to a button, it\n"
        + "will be selected when you select the remote control button."
    )

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

        WizardPanelBase.__init__(self, parent, resources)

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.textMessage = WrappedStaticText(self)
        self.sizer.Add(self.textMessage, 0, ALIGN_XTA, 5)

        self.hSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.leftSizer = wx.BoxSizer(wx.VERTICAL)
        self.remoteButtonsLabel = wx.StaticText(self, -1, "Remote Buttons:")
        self.leftSizer.Add(self.remoteButtonsLabel, 0, 0, 0)
        self.remoteButtonsListBox = wx.ListBox(self, style=wx.LB_SINGLE)
        self.remoteButtonsListBox.Bind(wx.EVT_LISTBOX,
                                       self.OnRemoteButtonSelection)
        self.leftSizer.Add(self.remoteButtonsListBox, 0, 0, 0)
        self.hSizer.Add(self.leftSizer, 0, 0, 0)
        self.hSizer.AddSpacer(20)
        self.rightSizer = wx.BoxSizer(wx.VERTICAL)
        self.deviceCommandsLabel = wx.StaticText(self, -1, "Device Commands:")
        self.rightSizer.Add(self.deviceCommandsLabel, 0, 0, 0)
        self.deviceCommandsListBox = wx.ListBox(self, style=wx.LB_SINGLE)
        self.rightSizer.Add(self.deviceCommandsListBox, 0, 0, 0)
        self.hSizer.Add(self.rightSizer, 0, 0, 0)
        self.sizer.Add(self.hSizer, 0, 0, 0)

        self.bottomSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.updateButton = wx.Button(self, label="Update Button")
        self.updateButton.Bind(wx.EVT_BUTTON, self.OnUpdate)
        self.updateButton.SetToolTip(wx.ToolTip(
                "Update selected button with the selected device command"
        ))
        self.bottomSizer.Add(self.updateButton, 0, 0, 0)
        self.overrideButton = wx.Button(self, label="Override Command")
        self.overrideButton.Bind(wx.EVT_BUTTON, self.OnOverride)
        self.overrideButton.SetToolTip(wx.ToolTip(
                "Learn IR command from existing remote to replace a command"
        ))
        self.bottomSizer.Add(self.overrideButton, 0, 0, 0)
        self.addButton = wx.Button(self, label="Add Command")
        self.addButton.Bind(wx.EVT_BUTTON, self.OnAdd)
        self.addButton.SetToolTip(wx.ToolTip(
                "Learn IR command from existing remote as a new command"
        ))
        self.bottomSizer.Add(self.addButton, 0, 0, 0)
        self.restoreButton = wx.Button(self, label="Restore Command")
        self.restoreButton.Bind(wx.EVT_BUTTON, self.OnRestore)
        self.restoreButton.SetToolTip(wx.ToolTip(
                "Remove overriden IR command/restore to official command"
        ))
        self.bottomSizer.Add(self.restoreButton, 0, 0, 0)
        self.sizer.Add(self.bottomSizer, 0, 0, 0)

        self.SetSizerAndFit(self.sizer)

        self.next = None

    def _WorkerFunction(self):
        wx.CallAfter(
            self.textMessage.UpdateText,
            self._msg_welcome
        )

    def OnActivated(self, prev_page, data):
        thread.start_new_thread(self._WorkerFunction, ())
        self.parent.ReenableBack()
        return (None, None)

    def SetDevice(self, skinId, deviceId, deviceName):
        self.skinId = skinId
        self.deviceId = deviceId
        self.deviceName = deviceName
        self.title = self.deviceName
        self.PopulateCommandsLists()

    def PopulateCommandsLists(self):
        self.remoteButtons = mhMgr.GetProductButtonList(self.skinId)
        self.remoteButtonsList = []
        if self.remoteButtons is not None:
            for button in self.remoteButtons:
                self.remoteButtonsList.append(button.ButtonKey)
            self.remoteButtonsListBox.Set(self.remoteButtonsList)
            
        self.deviceCommands = mhMgr.GetCommands(self.deviceId)
        self.deviceCommandsList = []
        if self.deviceCommands is not None:
            for command in self.deviceCommands:
                name = command.Name
                if command.IsLearned == "true":
                    name += "*"
                self.deviceCommandsList.append(name)
            self.deviceCommandsListBox.Set(self.deviceCommandsList)

        self.buttonMap = mhMgr.GetButtonMap(self.deviceId)

    def OnRemoteButtonSelection(self, event):
        key = self.remoteButtons[self.remoteButtonsListBox.GetSelection()] \
            .ButtonKey
        foundCommand = self.FindCommand(key)
        if foundCommand is not None:
            for i in range(len(self.deviceCommands)):
                # ButtonAssignment might not be a CommandButtonAssignment, but
                # ignore the error if it isn't.
                try:
                    if foundCommand.ButtonAssignment.CommandId.Value == \
                            self.deviceCommands[i].Id.Value:
                        self.deviceCommandsListBox.SetSelection(i)
                        return
                except AttributeError:
                    pass
        self.deviceCommandsListBox.DeselectAll()

    def FindCommand(self, buttonKey):
        for button in self.buttonMap.Buttons.AbstractButton:
            if buttonKey == button.ButtonKey:
                return button
        return None

    def OnUpdate(self, event):
        buttonSelection = self.remoteButtonsListBox.GetSelection()
        commandSelection = self.deviceCommandsListBox.GetSelection()
        if (buttonSelection == -1) or (commandSelection == -1):
            wx.MessageBox('Please select a button and command to assign.',
                          'No selection(s) made.', wx.OK | wx.ICON_WARNING)
            return
        self.StartBusy()
        button = self.remoteButtons[buttonSelection]
        command = self.deviceCommands[commandSelection]
        mhMgr.UpdateButtonMap(self.buttonMap, button, command)
        self.PopulateCommandsLists()
        self.EndBusy()

    def OnOverride(self, event):
        commandSelection = self.deviceCommandsListBox.GetSelection()
        if commandSelection == -1:
            wx.MessageBox('Please select a command to override.',
                          'No command selected.', wx.OK | wx.ICON_WARNING)
            return
        command = self.deviceCommands[commandSelection].Name
        self.UpdateIR(command)

    def OnAdd(self, event):
        dlg = wx.TextEntryDialog(None, "Enter the name of the new command:",
                                 "Add Command")
        if dlg.ShowModal() != wx.ID_OK:
            return
        command = dlg.GetValue()
        self.UpdateIR(command)

    def OnRestore(self, event):
        commandSelection = self.deviceCommandsListBox.GetSelection()
        if commandSelection == -1:
            wx.MessageBox('Please select a command to restore.',
                          'No command selected.', wx.OK | wx.ICON_WARNING)
            return
        command = self.deviceCommands[commandSelection]
        if command.IsLearned != "true":
            wx.MessageBox('Selected command is not a learned command.',
                          'Error', wx.OK | wx.ICON_WARNING)
            return
        result = mhMgr.DeleteIRCommand(command.Id, self.deviceId)
        if result is not None:
            wx.MessageBox('IR command deletion failed: ' + result, 'Error',
                          wx.OK | wx.ICON_WARNING)
        self.PopulateCommandsLists()

    def UpdateIR(self, commandName):
        msg = 'Please ensure your remote control is connected.'
        wx.MessageBox(msg, 'Connect Remote', wx.OK)
        try:
            libconcord.init_concord()
        except:
            msg = '%s\n    (libconcord function %s error %d)\n\n' % (
                sys.exc_value.result_str,
                sys.exc_value.func,
                sys.exc_value.result
            )
            wx.MessageBox('Could not detect remote: ' + msg, 'Error',
                          wx.OK | wx.ICON_WARNING)
            return

        msg = 'Please place your two remotes 3 inches (8 cm) apart.  After ' \
            + 'pressing OK, press the button on your non-Harmony remote that ' \
            + 'you wish to be learned.'
        wx.MessageBox(msg, 'Position Remotes', wx.OK)
        carrierClock = c_uint()
        signal = POINTER(c_uint)()
        signalLength = c_uint()
        self.StartBusy()
        try:
            libconcord.learn_from_remote(
                byref(carrierClock),
                byref(signal),
                byref(signalLength),
                libconcord.callback_type(dummy_callback_imp),
                None
            )
        except:
            self.EndBusy()
            msg = '%s\n    (libconcord function %s error %d)\n\n' % (
                sys.exc_value.result_str,
                sys.exc_value.func,
                sys.exc_value.result
            )
            wx.MessageBox('IR learning failed: ' + msg + '\nPerhaps you did '
                          + 'not press a key?', 'Error',
                          wx.OK | wx.ICON_WARNING)
            libconcord.deinit_concord()
            return
        self.EndBusy()

        rawSequence = c_char_p()
        libconcord.encode_for_posting(carrierClock, signal, signalLength,
                                      rawSequence)
        
        result = mhMgr.UpdateIRCommand(commandName, rawSequence.value,
                                       self.deviceId)
        if result is not None:
            wx.MessageBox('IR learning update failed: ' + result, 'Error',
                          wx.OK | wx.ICON_WARNING)
        self.PopulateCommandsLists()
        libconcord.delete_ir_signal(signal)
        libconcord.delete_encoded_signal(rawSequence)
        libconcord.deinit_concord()

    def GetTitle(self):
        return self.title

    def IsCancelInitiallyDisabled(self):
        return False

    def OnNext(self):
        pass

    def GetNext(self):
        return (self.next, None)

    def GetBack(self):
        return (self.resources.page_remote_configuration, None)

class FavoriteChannelsPanel(WizardPanelBase):
    _msg_welcome = (
        "Please select a favorite channel button to modify on the left.\n" +
        "Then, type the channel on the right as you would enter it on your " +
        "remote.\n"
        + "If there is an existing channel assigned to a button, it\n"
        + "will be displayed when you select the favorite control button."
    )

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

        WizardPanelBase.__init__(self, parent, resources)

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.textMessage = WrappedStaticText(self)
        self.sizer.Add(self.textMessage, 0, ALIGN_XTA, 5)

        self.hSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.leftSizer = wx.BoxSizer(wx.VERTICAL)
        self.remoteButtonsLabel = wx.StaticText(self, -1, "Buttons:")
        self.leftSizer.Add(self.remoteButtonsLabel, 0, 0, 0)
        self.remoteButtonsListBox = wx.ListBox(self, style=wx.LB_SINGLE)
        self.remoteButtonsListBox.Bind(wx.EVT_LISTBOX,
                                       self.OnRemoteButtonSelection)
        self.leftSizer.Add(self.remoteButtonsListBox, 0, 0, 0)
        self.hSizer.Add(self.leftSizer, 0, 0, 0)
        self.hSizer.AddSpacer(20)
        self.rightSizer = wx.BoxSizer(wx.VERTICAL)
        self.channelLabel = wx.StaticText(self, -1, "Channel:")
        self.rightSizer.Add(self.channelLabel, 0, 0, 0)
        self.channelCtrl = wx.TextCtrl(self, -1, "")
        self.channelCtrl.SetMinSize((70, 31))
        self.rightSizer.Add(self.channelCtrl, 0, 0, 0)
        self.hSizer.Add(self.rightSizer, 0, 0, 0)
        self.sizer.Add(self.hSizer, 0, 0, 0)
        self.sizer.AddSpacer(20)

        self.updateButton = wx.Button(self, label="Update Button")
        self.updateButton.Bind(wx.EVT_BUTTON, self.OnUpdate)
        self.updateButton.SetToolTip(wx.ToolTip(
                "Save selected button assignment"
        ))
        self.sizer.Add(self.updateButton, 0, 0, 0)

        self.SetSizerAndFit(self.sizer)

        self.next = None

    def _WorkerFunction(self):
        wx.CallAfter(
            self.textMessage.UpdateText,
            self._msg_welcome
        )

    def OnActivated(self, prev_page, data):
        thread.start_new_thread(self._WorkerFunction, ())
        self.parent.ReenableBack()
        return (None, None)

    def SetDevice(self, skinId, deviceId, deviceName):
        self.skinId = skinId
        self.deviceId = deviceId
        self.deviceName = deviceName
        self.title = self.deviceName
        self.PopulateData()

    def PopulateData(self):
        self.remoteButtons = mhMgr.GetProductButtonList(self.skinId)
        self.remoteButtonsList = []
        if self.remoteButtons is not None:
            for button in self.remoteButtons:
                if button.ButtonType == "FavoriteChannelButton":
                    self.remoteButtonsList.append(button.ButtonKey)
            self.remoteButtonsListBox.Set(self.remoteButtonsList)
            
        self.buttonMap = mhMgr.GetButtonMap(self.deviceId)

    def OnRemoteButtonSelection(self, event):
        key = self.remoteButtons[self.remoteButtonsListBox.GetSelection()] \
            .ButtonKey
        foundCommand = self.FindCommand(key)
        if foundCommand is not None:
            # ButtonAssignment might not be a ChannelButtonAssignment, but
            # ignore the error if it isn't.
            try:
                self.channelCtrl.SetValue(foundCommand.ButtonAssignment.Channel)
                return
            except AttributeError:
                pass
        self.channelCtrl.Clear()

    def FindCommand(self, buttonKey):
        for button in self.buttonMap.Buttons.AbstractButton:
            if buttonKey == button.ButtonKey:
                return button
        return None

    def OnUpdate(self, event):
        buttonSelection = self.remoteButtonsListBox.GetSelection()
        channel = self.channelCtrl.GetValue()
        if (buttonSelection == -1) or (not channel.isdigit()):
            wx.MessageBox('Please select a button and channel to assign.',
                          'No selection(s) made.', wx.OK | wx.ICON_WARNING)
            return
        self.StartBusy()
        button = self.remoteButtons[buttonSelection]
        mhMgr.UpdateButtonMap(self.buttonMap, button, channel,
                              isChannelButton = True)
        self.PopulateData()
        self.EndBusy()

    def GetTitle(self):
        return self.title

    def IsCancelInitiallyDisabled(self):
        return False

    def OnNext(self):
        pass

    def GetNext(self):
        return (self.next, None)

    def GetBack(self):
        return (self.resources.page_remote_configuration, None)

class Wizard(wx.Dialog):
    def __init__(
        self,
        resources,
        app_finalizer,
        min_page_width = 658,
        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, 'MHGUI 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)

        sizer_buttons.AddStretchSpacer()

        self.btn_back = wx.Button(self, wx.ID_BACKWARD)
        self.Bind(wx.EVT_BUTTON, self._OnBack, self.btn_back)
        sizer_buttons.Add(self.btn_back, 0, wx.EXPAND | wx.ALL, 5)

        self.btn_next = wx.Button(self, wx.ID_FORWARD)
        self.Bind(wx.EVT_BUTTON, self._OnNext, self.btn_next)
        sizer_buttons.Add(self.btn_next, 0, wx.EXPAND | wx.ALL, 5)

        self.btn_close = wx.Button(self, wx.ID_CLOSE)
        self.Bind(wx.EVT_BUTTON, self._OnNext, self.btn_close)
        sizer_buttons.Add(self.btn_close, 0, wx.EXPAND | wx.ALL, 5)

        self.btn_cancel = wx.Button(self, wx.ID_CLOSE)
        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, None)

    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 ReenableBack(self):
        self._ReenableButton(self.btn_back)

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

    def ReenableClose(self):
        self._ReenableButton(self.btn_close)

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

    def _OnBack(self, event):
        (page, data) = self.cur_page.GetBack()
        self._SetPage(page, data)

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

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

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

            prev_page = self.cur_page
            if prev_page:
                prev_page.Hide()
                self.sizer_top_right.Remove(prev_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.Hide()
                self.btn_close.Show()
            else:
                self.btn_next.Show()
                self.btn_close.Hide()

            self.btn_back.Enable(not self.cur_page.IsBackInitiallyDisabled())
            self.btn_next.Enable(not self.cur_page.IsNextInitiallyDisabled())
            self.btn_close.Enable(not self.cur_page.IsCloseInitiallyDisabled())
            self.btn_cancel.Enable(
                (not is_terminal)
                and
                (not self.cur_page.IsCancelInitiallyDisabled())
            )

            if self.btn_next.IsEnabled():
                self.btn_next.SetFocus()
            elif self.btn_close.IsEnabled():
                self.btn_close.SetFocus()

            (page, data) = self.cur_page.OnActivated(prev_page, data)

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

    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")

    def CreatePages(self, wizard):
        self.page_welcome = WelcomePanel(wizard, self)
        self.page_remote_select = RemoteSelectPanel(wizard, self)
        self.page_remote_configuration = RemoteConfigurationPanel(wizard, self)
        self.page_add_device = AddDevicePanel(wizard, self)
        self.page_create_account = CreateAccountPanel(wizard, self)
        self.page_configure_device = ConfigureDevicePanel(wizard, self)
        self.page_favorite_channels = FavoriteChannelsPanel(wizard, self)
        self.pages = [
            self.page_welcome,
            self.page_remote_select,
            self.page_remote_configuration,
            self.page_add_device,
            self.page_create_account,
            self.page_configure_device,
            self.page_favorite_channels,
        ]

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

    def __call__(self):
        pass


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

    app = wx.PySimpleApp()
    wx.InitAllImageHandlers()

    resources = Resources(appdir, True)
    resources.LoadImages()

    wizard = Wizard(resources, Finalizer(resources))

    resources.CreatePages(wizard)
    wizard.SetPages(resources.pages)
    wizard.SetInitialPage(resources.page_welcome)

    wizard.Show()

    app.MainLoop()

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