import sys
import re
import unidecode
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel, QLineEdit, QPushButton, QVBoxLayout, QWidget, \
    QMessageBox, QTableWidget, QTableWidgetItem, QGridLayout, QPlainTextEdit, QComboBox
from PyQt6.uic import loadUi
from random import shuffle, choice
from itertools import product, accumulate
from numpy import floor, sqrt


class ADFGVXCipherApp(QMainWindow):
    def __init__(self):
        super().__init__()
        loadUi("gui.ui", self)

        self.encrypt_button.clicked.connect(self.encrypt_message)
        self.decrypt_button.clicked.connect(self.decrypt_message)
        self.alphabet_entry.textChanged.connect(self.update_alphabet_table)
        self.alphabet_entry.textChanged.connect(self.check_alphabet)

        self.display_adfgvx_pairs_button.clicked.connect(self.display_adfgvx_pairs)
        self.update_key_button.clicked.connect(self.update_key_table)
        self.display_decrypt_pairs_button.clicked.connect(self.display_decryption_adfgvx_pairs)

        self.decrypt_key_table_button.clicked.connect(self.decrypt_key_table)

        self.generate_alphabet_button.clicked.connect(self.generate_random_alphabet)

        self.alphabet_entry.textChanged.connect(self.validate_alphabet)

        self.alphabet_mode_combobox.currentIndexChanged.connect(self.select_alphabet_mode)

        self.mode_combobox.currentIndexChanged.connect(self.handle_mode_combobox_change)

        self.mode_combobox.setCurrentText("NE")

        self.generate_alphabet_button.setEnabled(False)

        self.mode_combobox.currentIndexChanged.connect(self.clear_alphabet_entry)

        self.encryption_key.textChanged.connect(self.validate_encryption_key)

        self.decrypt_button.clicked.connect(self.update_de_filtered_text)



    ALLOWED_CHARACTERS = {
        'ADFGVX': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
        'ADFGX_CZ': 'ABCDEFGHIKLMNOPQRSTUVWXYZ',
        'ADFGX_EN': 'ABCDEFGHIJKLMNOPQRSTUVXYZ',
    }

    def validate_encryption_key(self, text):

        current_text = self.encryption_key.text()

        valid = re.match(r'^[A-Za-z0-9]*$', current_text)

        if not valid:

            self.show_error("Chyba", "Šifrovací klíč může obsahovat pouze písmena a číslice.")

            self.encryption_key.setText(''.join(char for char in current_text if char.isalnum()))

    def clear_alphabet_entry(self):
        self.alphabet_entry.clear()

    def handle_mode_combobox_change(self, index):
        selected_mode = self.mode_combobox.currentText()

        nazev_tlacitka = self.generate_alphabet_button

        if selected_mode == "ANO":
            nazev_tlacitka.setEnabled(True)
        else:
            nazev_tlacitka.setEnabled(False)

    def prepare_input_key(self, input_key, mode):

        filtered_key = input_key.upper()

        filtered_key = unidecode.unidecode(filtered_key)


        if mode == 'ADFGX_CZ':
            # For ADFGX_CZ mode, replace 'J' with 'I'
            filtered_key = filtered_key.replace('J', 'I')
        elif mode == 'ADFGX_EN':
            # For ADFGX_EN mode, replace 'W' with 'V'
            filtered_key = filtered_key.replace('W', 'V')


        allowed_characters = self.ALLOWED_CHARACTERS.get(mode, '')
        filtered_key = ''.join(char for char in filtered_key if char in allowed_characters)

        return filtered_key

    def prepare_input_message(self, input_message, mode):

        filtered_text = input_message.upper()

        filtered_text = unidecode.unidecode(filtered_text)


        if mode == 'ADFGX_CZ':
            filtered_text = filtered_text.replace('J', 'I')

        if mode == 'ADFGX_EN':
            filtered_text = filtered_text.replace('W', 'V')

        filtered_text = filtered_text.replace(' ', 'QXQ')

        filtered_text = re.sub(r'[^A-Z0-9]', '', filtered_text)

        self.filtered_text.setPlainText(filtered_text)

        return filtered_text

    def validate_alphabet(self, text):
        current_text = self.alphabet_entry.text().upper()

        allowed_chars = self.ALLOWED_CHARACTERS[self.alphabet_mode_combobox.currentText()]
        valid_text = ''.join(char for char in current_text if char in allowed_chars)

        self.alphabet_entry.setText(valid_text)

    def decrypt_key_table(self):
        try:
            decryption_key_text = self.decryption_key.text().upper()

            ciphertext = self.ciphertext_entry2.text().upper()


            sorted_decryption_key = ''.join(sorted(decryption_key_text))


            decrypt_table = self.findChild(QTableWidget, 'decrypt_table')
            decrypt_table.setRowCount(1)
            decrypt_table.setColumnCount(len(sorted_decryption_key))
            for i, char in enumerate(sorted_decryption_key):
                item = QTableWidgetItem(char)
                decrypt_table.setItem(0, i, item)


            num_rows = len(ciphertext) // len(sorted_decryption_key) + (
                1 if len(ciphertext) % len(sorted_decryption_key) > 0 else 0)
            decrypt_table.setRowCount(num_rows)


            for i, char in enumerate(ciphertext):
                row = i % num_rows
                col = i // num_rows
                item = QTableWidgetItem(char)
                decrypt_table.setItem(row, col, item)


            decrypt_table.resizeColumnsToContents()
        except Exception as e:

            error_message = str(e)
            self.show_error("Chyba", f"Došlo k chybě.: {error_message}")

    def display_decryption_adfgvx_pairs(self):
        encrypted_message = self.de_filtered_text.toPlainText().upper()
        key = self.encryption_key.text().upper()
        alphabet = self.alphabet_entry.text().upper()
        adfgvx_pairs = []

        for char in encrypted_message:
            if char in alphabet:
                encoded_char = self.encrypt_ADFGVX(char, key)
                adfgvx_pairs.append(encoded_char)


        adfgvx_pairs_second_row = [pair[::-1] for pair in adfgvx_pairs]


        num_columns = len(adfgvx_pairs)
        self.decrypt_pairs_table.setColumnCount(num_columns)
        self.decrypt_pairs_table.setRowCount(2)


        for i, char in enumerate(encrypted_message):
            item = QTableWidgetItem(char)
            self.decrypt_pairs_table.setItem(0, i, item)


        for i, pair in enumerate(adfgvx_pairs_second_row):
            item = QTableWidgetItem(pair)
            self.decrypt_pairs_table.setItem(1, i, item)


        self.decrypt_pairs_table.resizeColumnsToContents()

    def update_key_table(self):
        message_pairs_table = self.findChild(QTableWidget, 'message_pairs_table')
        if not message_pairs_table.item(0, 0):
            self.show_error("Chyba", "Zobrazte nejprve dvojice.")
            return

        input_key = self.encryption_key.text().upper()
        message_pairs_table = self.findChild(QTableWidget, 'message_pairs_table')
        key_table = self.findChild(QTableWidget, 'key_table')

        num_columns = message_pairs_table.columnCount()
        encryption_key_length = len(input_key)


        key_table.setRowCount(1)
        key_table.setColumnCount(encryption_key_length)
        for i, char in enumerate(input_key):
            item = QTableWidgetItem(char)
            key_table.setItem(0, i, item)


        adfgvx_text = [message_pairs_table.item(1, i).text() for i in range(num_columns)]

        individual_characters = [char for pair in adfgvx_text for char in pair]


        for i, char in enumerate(individual_characters):
            row = (i // encryption_key_length) + 1
            col = i % encryption_key_length
            item = QTableWidgetItem(char)
            # If the row doesn't exist, create it
            while row >= key_table.rowCount():
                key_table.insertRow(key_table.rowCount())
            key_table.setItem(row, col, item)


        key_table.resizeColumnsToContents()

    def display_adfgvx_pairs(self):
        input_message = self.filtered_text.toPlainText().upper()
        key = self.encryption_key.text().upper()
        alphabet = self.alphabet_entry.text().upper()
        adfgvx_pairs = []

        for char in input_message:
            if char in alphabet:
                encoded_char = self.encrypt_ADFGVX(char, key)
                adfgvx_pairs.append(encoded_char)


        adfgvx_pairs_second_row = [pair[::-1] for pair in adfgvx_pairs]


        num_columns = len(adfgvx_pairs)
        self.message_pairs_table.setColumnCount(num_columns)
        self.message_pairs_table.setRowCount(2)


        for i, char in enumerate(input_message):
            item = QTableWidgetItem(char)
            self.message_pairs_table.setItem(0, i, item)


        for i, pair in enumerate(adfgvx_pairs_second_row):
            item = QTableWidgetItem(pair)
            self.message_pairs_table.setItem(1, i, item)


        self.message_pairs_table.resizeColumnsToContents()

    def check_alphabet(self, text):

        current_text = self.alphabet_entry.text().upper()


        unique_characters = ''.join(sorted(set(current_text), key=current_text.index))


        self.alphabet_entry.setText(unique_characters)

    def update_alphabet_table(self):
        alphabet = self.alphabet_entry.text().upper()
        alphabet_table = self.findChild(QTableWidget, 'alphabet_table')

        num_rows = 0
        num_columns = 0
        custom_row_headers = []
        custom_col_headers = []
        missing_characters = set()


        mode_index = self.alphabet_mode_combobox.currentIndex()
        if mode_index == 0:  # ADFGVX
            num_rows = 6
            num_columns = 6
            custom_row_headers = ['A', 'D', 'F', 'G', 'V', 'X']
            custom_col_headers = ['A', 'D', 'F', 'G', 'V', 'X']
            missing_characters = set("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - set(alphabet)
        elif mode_index == 1:  # ADFGX_CZ
            num_rows = 5
            num_columns = 5
            custom_row_headers = ['A', 'D', 'F', 'G', 'V']
            custom_col_headers = ['A', 'D', 'F', 'G', 'V']
            missing_characters = set("ABCDEFGHIKLMNOPQRSTUVWXYZ") - set(alphabet)
        elif mode_index == 2:  # ADFGX_EN
            num_rows = 5
            num_columns = 5
            custom_row_headers = ['A', 'D', 'F', 'G', 'V']
            custom_col_headers = ['A', 'D', 'F', 'G', 'V']
            missing_characters = set("ABCDEFGHIJKLMNOPQRSTUVXYZ") - set(alphabet)

        alphabet_table.setRowCount(num_rows)
        alphabet_table.setColumnCount(num_columns)

        used_characters = set()

        for row in range(num_rows):
            for col in range(num_columns):
                index = row * num_columns + col
                if index < len(alphabet):
                    char = alphabet[index]
                    item = QTableWidgetItem(char)
                    alphabet_table.setItem(row, col, item)
                    used_characters.add(char)
                else:
                    item = QTableWidgetItem('')
                    alphabet_table.setItem(row, col, item)

        missing_characters_text = ", ".join(sorted(missing_characters))
        self.missing_characters_label.setText(f"Chybějící znaky: {missing_characters_text}")

        # Set the custom row headers
        for row, header in enumerate(custom_row_headers):
            alphabet_table.setVerticalHeaderItem(row, QTableWidgetItem(header))

        # Set the custom column headers
        for col, header in enumerate(custom_col_headers):
            alphabet_table.setHorizontalHeaderItem(col, QTableWidgetItem(header))

    def generate_random_alphabet(self):
        mode_index = self.alphabet_mode_combobox.currentIndex()
        modes = ['ADFGVX', 'ADFGX_CZ', 'ADFGX_EN']  # Add more modes as needed
        selected_mode = modes[mode_index] if 0 <= mode_index < len(modes) else 'ADFGVX'

        if selected_mode == 'ADFGVX':
            alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
        elif selected_mode == 'ADFGX_CZ':
            alphabet = 'ABCDEFGHIKLMNOPQRSTUVWXYZ'
        elif selected_mode == 'ADFGX_EN':
            alphabet = 'ABCDEFGHIJKLMNOPQRSTUVXYZ'
        else:
            alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'

        custom_dictionary = [alphabet[i:i + 6] for i in range(0, len(alphabet), 6)]
        shuffle(custom_dictionary)
        self.alphabet_entry.setText(''.join(custom_dictionary))
        self.update_alphabet_table()


    def select_alphabet_mode(self, index):
        modes = ['ADFGVX', 'ADFGX_CZ', 'ADFGX_EN']
        selected_mode = modes[index] if 0 <= index < len(modes) else 'ADFGVX'

        if selected_mode == 'ADFGVX':
            alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
        elif selected_mode == 'ADFGX_CZ':
            alphabet = 'ABCDEFGHIKLMNOPQRSTUVWXYZ'
        elif selected_mode == 'ADFGX_EN':
            alphabet = 'ABCDEFGHIJKLMNOPQRSTUVXYZ'
        else:

            alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'

        self.update_alphabet_table()

    def create_encode_dict(self):
        """ Create the ADFGVX encoding dictionary. """
        matrices = list('ADFGVX')
        pairs = [p[0] + p[1] for p in product(matrices, matrices)]
        return dict(zip(self.alphabet_entry.text().upper(), pairs))

    def encrypt_ADFGVX(self, msg, key):
        """ Encrypt with the ADFGVX cipher. """
        alphabet = list(self.alphabet_entry.text().upper())
        key = list(key.upper())
        pdim = int(floor(sqrt(len(alphabet))))
        encode = self.create_encode_dict()

        chars = list(''.join([encode[c] for c in msg.upper() if c in alphabet]))
        colvecs = [(lett, chars[i:len(chars):len(key)]) for (i, lett) in enumerate(key)]
        colvecs.sort(key=lambda x: x[0])
        return ''.join([''.join(a[1]) for a in colvecs])

    def decrypt_ADFGVX(self, cod, key):
        """ Decrypt with the ADFGVX cipher. Does not depend on spacing of encoded text """
        matrices = list('ADFGVX')
        chars = [c for c in cod if c in matrices]
        key = list(key.upper())
        sortedkey = sorted(key)
        order = [key.index(ch) for ch in sortedkey]
        originalorder = [sortedkey.index(ch) for ch in key]
        base, extra = divmod(len(chars), len(key))
        strides = [base + (1 if extra > i else 0) for i in order]
        starts = list(accumulate(strides[:-1], lambda x, y: x + y))
        starts = [0] + starts
        ends = [starts[i] + strides[i] for i in range(len(key))]
        cols = [chars[starts[i]:ends[i]] for i in originalorder]
        pairs = []
        for i in range((len(chars) - 1) // len(key) + 1):
            for j in range(len(key)):
                if i * len(key) + j < len(chars):
                    pairs.append(cols[j][i])

        decode = dict((v, k) for (k, v) in self.create_encode_dict().items())
        return ''.join([decode[pairs[i] + pairs[i + 1]] for i in range(0, len(pairs), 2)])

    def generate_alphabet(self):
        self.generate_random_alphabet()
        self.update_alphabet_table()

    def handle_alphabet_mode_change(self, index):
        self.generate_random_alphabet()

    def encrypt_message(self):
        input_message = self.input_message.text()
        key = self.encryption_key.text()
        mode = self.alphabet_mode_combobox.currentText()


        processed_input_message = self.prepare_input_message(input_message, mode)

        if not processed_input_message or not key or not self.alphabet_entry.text():
            self.show_error("Chyba", "Prosím zadejte text k šifrování, klíč a abecedu.")
            return

        ciphertext = self.encrypt_ADFGVX(processed_input_message, key)
        self.ciphertext_entry.setPlainText(ciphertext)

    def decrypt_message(self):
        ciphertext = self.ciphertext_entry2.text()
        key = self.decryption_key.text()

        if not ciphertext or not key or not self.alphabet_entry.text():
            self.show_error("Chyba", "Prosím zadejte text k dešifrování, klíč a abecedu.")
            return

        decrypted_message = self.decrypt_ADFGVX(ciphertext, key)

        decrypted_message = decrypted_message.replace('QXQ', ' ')

        self.decrypted_message_entry.setPlainText(decrypted_message)

    def update_de_filtered_text(self):
        decrypted_message = self.decrypted_message_entry.toPlainText()
        updated_message = decrypted_message.replace(' ', 'QXQ')
        self.de_filtered_text.setPlainText(updated_message)

    def show_error(self, title, message):
        msg_box = QMessageBox(self)
        msg_box.setIcon(QMessageBox.Icon.Critical)
        msg_box.setWindowTitle(title)
        msg_box.setText(message)
        msg_box.exec()


def main():
    app = QApplication(sys.argv)
    ex = ADFGVXCipherApp()
    ex.show()
    sys.exit(app.exec())


if __name__ == '__main__':
    main()