# -*- coding: utf-8 -*-
"""
/***************************************************************************
 MapsafeDialog
                                 A QGIS plugin
 export vector layers to common format and crs
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2018-12-08
        git sha              : $Format:%H$
        copyright            : (C) 2018 by Zoltan Siki
        email                : siki1958@gmail.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program 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 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

import os
# for swich button - https://github.com/yjg30737/pyqt-switch
# https://github.com/mpetroff/qgsazimuth/issues/23
import sip
sip.setapi("QVariant", 2)

from PyQt5 import (uic, QtWidgets, QtCore)
from osgeo import ogr

# imports
from random import uniform
from math import pi, sin, cos
from qgis.core import QgsVectorLayer, QgsGeometry, QgsFeature, QgsProject 

# from PyQt5.QtWidgets import QApplication, QWidget, QComboBox, QPushButton, QVBoxLayout
# from PyQt5.QtCore import Qt
from qgis.PyQt.QtWidgets import QAction, QApplication, QLabel, QComboBox, QFileDialog

from qgis.PyQt.QtGui import *


from pathlib import Path

from qgis.utils import iface

# generate passphrase
#import random
#import xkcdpass.xkcd_password as xp

# encryption decryption
from zipfile import ZipFile
from cryptography.fernet import Fernet
from datetime import datetime
from .encryptiondecryption import EncryptionDecryption

# Python program to find SHA256 hexadecimal hash string of a file
import hashlib

# ensure passphrase file being read has plaintext 
import chardet  # Library to detect the character encoding of the file

import os
import shutil

#binning
#from .hexagonal_binning import HexagonalBinning
from .algorithms import (
    CreateH3GridProcessingAlgorithm,
    CreateH3GridInsidePolygonsProcessingAlgorithm,
    CountPointsOnH3GridProcessingAlgorithm
)

#from .protect_passphrase import ProtectPassphrase
#public key encryption
from .encryptRSA import PublicKeyEncryption
# passphrase
from .passphrase import Passphrase

#geomasking 
from .masking import GeoMasking

#from .h3_grid_from_layer import HexTest
import os
from qgis.utils import iface
from qgis.core import (
    QgsCoordinateReferenceSystem,
    QgsCoordinateTransform,
    QgsFeature,
    QgsField,
    QgsFields,
    QgsGeometry,
    QgsPointXY,
    QgsProject,
    QgsProcessingFeedback,
    QgsMessageLog,
    QgsVectorFileWriter,
    QgsVectorLayer,
    QgsWkbTypes,
)
from qgis.PyQt.QtCore import QVariant
import processing
import h3

# end hex

# for progressbar
from PyQt5.QtWidgets import (QApplication, QDialog, QProgressBar, QPushButton)
import time

from reportlab.pdfgen import canvas

#notarisation
from .notarisation import Notarisation

# binning
from .h3binning import H3Binning

import ctypes  # An included library with Python install.   
#import easygui

# python should be installed on the machine
# e.g. from windows - https://www.python.org/downloads/windows/ - select windows installer
# along with pip
#  In home pc, run OSGeo4W.bat in this directory 'D:\QGIS' not from 'D\OSGEO4W' 

# required pip install in qgis
## Use D:\QGIS\OSGEO.bat
# D:\QGIS>pip install cryptography
# pip install cryptography
# pip install web3
# pip install python-dotenv
# pip install xkcdpass
# pip install easygui or python -m pip install easygui
# pip install python-dotenv
# (python -m) pip install pyqt-switch

# hash value
# this can be of the final encrypted volume (containing 1,2 or 3 levels), 
# original dataset or the masked dataset 
main_string_to_mint = ''  # filename and hashvalue combined string

# read environment variables
from os import environ
from dotenv import load_dotenv

# for toggle
from PyQt5.QtWidgets import QWidget, QFormLayout, QApplication, QLabel
from pyqt_switch import PyQtSwitch

from PyQt5.QtTest import QTest
from PyQt5.QtCore import Qt
from fpdf import FPDF
import os
from datetime import datetime

from PyQt5.QtWidgets import QLabel
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices

import json

import h3
from qgis.PyQt.QtCore import QCoreApplication, QVariant
from qgis.core import (
    QgsFeatureSink,
    QgsProcessing,
    QgsProcessingException,
    QgsProcessingAlgorithm,
    QgsProcessingParameterFeatureSource,
    QgsProcessingParameterFeatureSink,
    QgsProcessingParameterNumber,
    QgsProcessingParameterExtent,
    QgsPointXY,
    QgsGeometry,
    QgsFeature,
    QgsField,
    QgsFields,
    QgsCoordinateReferenceSystem,
    QgsCoordinateTransform,
    QgsWkbTypes,
    QgsFeatureRequest,
    QgsCoordinateTransformContext,
    QgsVectorLayer,
    QgsProject,
    QgsMapLayerType,
)
from qgis import processing

from .singlepartGeo import singlepartGeometries


from PyQt5.QtWidgets import * 
from PyQt5 import QtCore, QtGui 
from PyQt5.QtGui import * 
from PyQt5.QtCore import * 



# open window to save env variables
from .envvariables import envvariables

FORM_CLASS, _ = uic.loadUiType(os.path.join(
    os.path.dirname(__file__), 'mapsafe_dialog_base.ui'))

class MapSafeDialog(QtWidgets.QDialog, FORM_CLASS):  

    little_masked_layername = ''
    more_masked_layername = ''
    original_layer_full_path = ''

    # default masking parameter values
    minimum_distance = 0
    maximum_distance = 0

    filename1 = ''
    filename2 = ''
    filename3 = ''

    # passphrase
    passphrase =''
    passphrase_file_to_encrypt =''
    passphrase_to_decrypt_filename =''
    passphrase_generated = False

    # public key encryption
    PKE = ''
    public_key_for_encryption =''
    private_key_for_encryption =''

    
    debug = ''
    min_resolution = 0
    max_resolution = 9
    out_name_prefix = ''
    projectPath = ''
    geo_csrs = ''
    out_csrs = ''
    mylayer = ''
    geographic_coordsys =''
    output_projection = ''
    dataPath = ''
    #verification aspect
    encrypted_volume_filename = ''
    label_encrypted_volume = ''
    levels_to_encrypt = 1
    #encrypt_layers = True
    working_directory = ''
    #compute_privacy_rating = False
    encrypted_file_loaded = False
    hexabinning_resolution = 6      # set a default resolution

    decrypt_to_level = 1

    two_geomasking_levels = False
    two_binning_levels    = False

    working_directory_set = False

    # 1 = masking, 2 = binning 
    obfuscation_option = 0  

    TIME_LIMIT = 100

    # for when to enable the encrypt and decrypt button - 
    # enable encrypt only when the passphrase file and public key is chosen
    passphrase_file_chosen           = False
    public_key_chosen                = False
    # enable decrypt only when the passphrase file and public key is chosen    
    encrypted_passphrase_file_chosen = False
    private_key_chosen               = False 

    # what level has been encrypted, as specified at the end of the encrypted volume
    volume_encrypted_level              = 1 

    # selected file from devrypted tree
    selected_file_from_decrypted_tree = ""

    final_volume_filename = ""
    final_volume_hash_value  = ""

    # https://ethereum.stackexchange.com/questions/46706/web3-py-how-to-use-abi-in-python-when-solc-doesnt-work

    env_file_loc = ""
    #env_file_directory = ""
    env_file_path_set = False
    internal_file_loc = ""    # internal txt file that contains the location of the env file

    # decrypted layer that should be unloaded wheen another level is decrypted
    decrypted_layer_layerName = None

    # in masking with privacy rating, we do masking many times, so we dont show the masked map everytime, as it does not work
    add_layer_to_canvas = True

    # environment variable class object
    env_var = None

    # notarisation details
    blockchain_address = ""
    contract_address = ""

    # if we want to verify and automatically display the map just after decryption
    to_verify_display = True

    def add_action(
        self,
        icon_path,
        text,
        callback,
        enabled_flag=True,
        add_to_menu=True,
        add_to_toolbar=True,
        status_tip=None,
        whats_this=None,
        parent=None):
        """Add a toolbar icon to the toolbar.

        :param icon_path: Path to the icon for this action. Can be a resource
            path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
        :type icon_path: str

        :param text: Text that should be shown in menu items for this action.
        :type text: str

        :param callback: Function to be called when the action is triggered.
        :type callback: function

        :param enabled_flag: A flag indicating if the action should be enabled
            by default. Defaults to True.
        :type enabled_flag: bool

        :param add_to_menu: Flag indicating whether the action should also
            be added to the menu. Defaults to True.
        :type add_to_menu: bool

        :param add_to_toolbar: Flag indicating whether the action should also
            be added to the toolbar. Defaults to True.
        :type add_to_toolbar: bool

        :param status_tip: Optional text to show in a popup when mouse pointer
            hovers over the action.
        :type status_tip: str

        :param parent: Parent widget for the new action. Defaults None.
        :type parent: QWidget

        :param whats_this: Optional text to show in the status bar when the
            mouse pointer hovers over the action.

        :returns: The action that was created. Note that the action is also
            added to self.actions list.
        :rtype: QAction
        """

        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)

        if status_tip is not None:
            action.setStatusTip(status_tip)

        if whats_this is not None:
            action.setWhatsThis(whats_this)
        
        return action

    # open window for users to encrypt passphrase
    def openWindow(self):
        self.window = QtWidgets.QMainWindow()
        self.ui = ProtectPassphrase()
        self.ui.setupUi(self.window)
        self.window.show()

    def __init__(self, parent=None):
        """Constructor."""
        super(MapSafeDialog, self).__init__(parent)
        self.setupUi(self)
        self.btn_mask.clicked.connect(self.request_geomasking) #geomasking_function
        # try auto determine best min and max distances for provided privacy rating
        #self.btn_use_pr.clicked.connect(self.mask_based_on_privacy_rating) #geomasking_function
        #self.btn_bin.clicked.connect(self.binning_function)
        self.btn_bin.clicked.connect(self.request_binning_function) # request_binning_function
        
        #passphrase        
        self.btnGeneratePassphrase.clicked.connect(self.request_passphrase)
        self.btnENVFileDir.clicked.connect(self.set_env_file_location) #requestENVFileDirectory) #requestWorkingDirectory)
        
        
        # TOOLTIPS        

        # env file
        self.btnENVFileDir.setToolTip('Essential parameters for this plugin to work.')

        # about working directory
        self.label_working_dir_text.setToolTip('Used by plugin to save files.')
        self.label_working_dir.setToolTip('Used by plugin to save files.')

        # masking
        # second level masking 
        self.groupBox_4.setToolTip('You can choose to add a second (more) masked layer. \n Generallly these use double the min and max values.') 
        self.chkBox_privRating.setToolTip('Keep adjusting the minimum and maximum values for a high privacy rating. ')
        self.btnSaveMasked.setToolTip('Save the masked layer(s).')
        self.btn_saved_masked_layers_loc.setToolTip('Open the directory where the masked layer is saved.')
        self.masking_next_pushButton.setToolTip('Proceed to encrypt the saved layers.')
    
        # binning two levels
        self.chkBox_binning_two_levels.setToolTip('Create two hexabinned layers, \n with the second one binned at one more resolution.')
        self.btn_bin.setToolTip('Perform Hexabinning using the chosen resolution.')
        self.btnSaveBinned.setToolTip('Save the binned layer(s).')
        self.btn_saved_binned_layers_loc.setToolTip('Open the directory where the binned layer is saved.')
        self.label_8.setToolTip('Choose a hexabinning resolution.')
        self.binning_next_pushButton.setToolTip('Proceed to encrypt the saved layer(s).')

        # encryption        
        self.btnGeneratePassphrase.setToolTip('Passphrase is needed for encryption')  
        self.groupBox.setToolTip('You can choose one, two or three files to encrypt')
        self.encryption_next_pushButton.setToolTip('Proceed to notarise the encrypted volume.')
        self.encryption_back_pushButton.setToolTip('Return to anonymise the dataset again.')
        self.btnEncrypt.setToolTip('Encrypt the chosen file(s) into a single encrypted volume.')

        # notarise
        self.button_notarise.setToolTip('Notarise the filename and hash value \n of the encrypted volume on the Blockchain.')

        # verification display
        self.btn_upload_encrypted_vol.setToolTip('Upload the encrypted volume. \n Its hash value will be displayed for verification. ')
        self.verify_next_pushButton.setToolTip('Proceed to decrypt the volume.')

        # decryption
        self.btn_readPassphrase.setToolTip('Read the passphrase file.')
        self.btnDecrypt.setToolTip('Decrypt to the specified level.')
        self.btn_dec_volume_location.setToolTip('Directory where the decrypted file(s) are saved.')
        self.decrypt_next_pushButton.setToolTip('Proceed to display the decrypted dataset(s).')
        self.decrypt_back_pushButton.setToolTip('Return to verify the encrypted dataset(s).')

        # display
        self.treeWidget.setToolTip('Choose the decrypted dataset to display')
        self.btn_displaymap.setToolTip('Display the chosen dataset')

        # shield
        self.btn_generate_keys.setToolTip('Generate a public and private key pair.')
        
        self.btn_select_passphrase_file.setToolTip('The passphrase used to encrypt the volume')
        self.btn_select_public_key.setToolTip('The passphrase is to be encrypted using the public key \n of an intended recipient of the encrypted volume')
        self.btn_encrypt_passphrase.setToolTip('Encrypt using the public key')
        
        # The recipeint of the encrypted volume can use their
        self.btn_select_encrypted_passphrase_file.setToolTip('File containing the encrypted passphrase')
        self.btn_select_private_key.setToolTip('The private key decrypts the encrypted passphrase \n'
                                               + ' for decrypting the encrypted volume.')
        self.btn_decrypt_passphrase.setToolTip('Decrypt the encrypted passphrase using the private ley key')

        self.btnEncrypt.clicked.connect(self.request_encryption)
        self.btnDecrypt.clicked.connect(self.request_decryption)

        # encrypt passphrase
        self.btn_select_passphrase_file.clicked.connect(self.readPassphraseFileForEncryption)
        self.btn_select_public_key.clicked.connect(self.readPublicKeyForEncryption)
        self.btn_encrypt_passphrase.clicked.connect(self.request_encrypt_passphrase)

        # Decrypt encrypted Passphrase file
        #self.pushButton = QtWidgets.QPushButton(self.btnEncrypt_passphrase, clicked = lambda: self.openWindow())
        #self.btnEncrypt_passphrase.clicked.connect(self.openWindow)
        self.btn_select_encrypted_passphrase_file.clicked.connect(self.readPassphraseFileForDecryption)
        self.btn_select_private_key.clicked.connect(self.readPrivateKeyForDecryption)
        self.btn_decrypt_passphrase.clicked.connect(self.request_decrypt_passphrase)
        #self.pushButton.setGeometry(QtCore.QRect(90, 80, 261, 71))

        # public key encryption for the passphrase
        self.PKE = PublicKeyEncryption()      

        # if user wants to encrypt OS files
        self.btn_osfiles_level1.clicked.connect(self.getOSFile_level1)
        self.btn_osfiles_level2.clicked.connect(self.getOSFile_level2)
        self.btn_osfiles_level3.clicked.connect(self.getOSFile_level3)
        
        self.chkBox_privRating_two_levels.clicked.connect(self.onStateChanged)
        self.label_minimum_offset.setEnabled(False)
        self.label_maximum_offset.setEnabled(False)
        self.text_minimum_offset.setEnabled(False)
        self.text_maximum_offset.setEnabled(False)

        self.btnSaveMasked.clicked.connect(self.request_saveLayers)
        self.btnSaveBinned.clicked.connect(self.request_saveLayers)
        
        # notarise
        self.button_notarise.clicked.connect(self.request_notarisation)
               

        # Verification aspect
        self.btn_upload_encrypted_vol.clicked.connect(self.get_encrypted_volume)
        self.btn_readPassphrase.clicked.connect(self.readPassphraseFile)
        self.btn_generate_keys.clicked.connect(self.generate_keys)
        self.btn_displaymap.clicked.connect(self.display)
        
        # if user wants to notarise OS files
        self.btn_osfile_to_notarise.clicked.connect(self.getOSFile_to_notarise)

        #config = dotenv_values(".env")
        #print(config) # outputs OrderedDict([('VARIABLE1', 'test')])

        # read environment file location
        #self.button_read_env_file_location.clicked.connect(self.read_env_file_location)
        #self.button_set_env_file_location.clicked.connect(self.set_env_file_location)

        # set the environment file location and load it in the next line
        # #env_file = f'{self.plugin_dir}/.env'        
        
        ### Add Icon
        # initialize plugin directory
        self.plugin_dir = os.path.dirname(__file__)
        print('self.plugin_dir ' + str(self.plugin_dir))

        icon_path = ':/plugins/mapsafe/icon.png'
        self.add_action(
            icon_path,
            #add_to_menu=True,
            #add_to_toolbar=True,
            text=self.tr(u'MapSafe Complete Geoprivacy Plugin'),
            callback=self.run
            #parent=self.iface.mainWindow()
            ) 

        # this file location is also passed to the notarisation class upon its initialisation
        # the class will use the file to read the location of the env file and read the parameters 
        #self.internal_file_loc = f'{self.plugin_dir}/loc.txt'  
        #print('self.internal_file_loc: ' + self.internal_file_loc)

        # the location of the env file is within the internal file
        # we simply read the location from the internal file and then pass that location to this function
        # f = open(self.internal_file_loc, "r") # In this example, we will be opening a file to read-only.
        # env_file_loc = f.readline()        
        # f.close()  # closing the file
        # print('env_file_loc: ' + env_file_loc)

        # env_file_loc = self.read_env_file_location()
        # print('env_file_loc: ' + env_file_loc)

        self.plugin_dir = os.path.dirname(__file__)
        self.internal_envfile_loc = f'{self.plugin_dir}/parameters.txt' 
        print('self.internal_envfile_loc: ' + self.internal_envfile_loc)
        #load_dotenv(self.internal_envfile_loc ) #"D:\\datasets\\.env") #load_dotenv("D:\\datasets\\.env")
        
        # initialise the env object - which will read the variables
        # Create an instance of the Env Variables dialog
        self.env_var = envvariables(self)        
        print('loaded env variables from file: ' + str(self.internal_envfile_loc))
        
              
        # Read and set the Working directory
        wd = self.env_var.get_working_dir()
        self.set_working_directory(wd)
        
        
        self.btnEncrypt.setEnabled(False)
        #self.encryption_next_pushButton.setEnabled(False)
        #self.encryption_back_pushButton.setEnabled(False)
        #self.btnEncrypt_passphrase.setEnabled(False)
        #self.btn_encrypt_passphrase.setEnabled(False)

        # disable decryption level buttons a user can choose
        self.rbt_level1.setEnabled(False) 
        self.rbt_level2.setEnabled(False) 
        self.rbt_level3.setEnabled(False)

        # ### Add Icon
        # # initialize plugin directory
        # self.plugin_dir = os.path.dirname(__file__)
        # print('self.plugin_dir ' + str(self.plugin_dir))

        # icon_path = ':/plugins/mapsafe/icon.png'
        # self.add_action(
        #     icon_path,
        #     #add_to_menu=True,
        #     #add_to_toolbar=True,
        #     text=self.tr(u'MapSafe Complete Geoprivacy Plugin'),
        #     callback=self.run
        #     #parent=self.iface.mainWindow()
        #     ) 

        # self.internal_file_loc = f'{self.plugin_dir}/loc.txt'     
        
     
        # load the layers for choosing in the encryption tab 
        self.get_layers() 
   
        # Masking Parameters sliders
        self.horizontalSlider_min.setRange(0, 500)  # changed from 500 to 100
        self.horizontalSlider_min.setValue(100)
        self.horizontalSlider_min.setSingleStep(5)
        self.horizontalSlider_min.valueChanged.connect(self.on_value_changed_min)

        self.horizontalSlider_max.setRange(0, 3000) # changed from 500 to 0
        self.horizontalSlider_max.setValue(500)
        self.horizontalSlider_max.setSingleStep(100)
        self.horizontalSlider_max.valueChanged.connect(self.on_value_changed_max)
        self.label_min.setText('100') 
        self.label_max.setText('500') 

        # second level default values
        self.text_minimum_offset.setPlainText(str(200))
        self.text_maximum_offset.setPlainText(str(1000))

        # Hexabinning Parameters sliders
        self.horizontalSlider_resolution.setRange(0, 14)
        self.horizontalSlider_resolution.setValue(self.hexabinning_resolution)
        self.horizontalSlider_resolution.setSingleStep(1)
        #self.horizontalSlider_min.setPageStep(10)
        self.horizontalSlider_resolution.valueChanged.connect(self.on_resolution_value_changed)

        # set default values
        self.label_resolution.setText(str(self.hexabinning_resolution)) 

        # Encryption-decryption
        self.key_file = 'filekey.key'        

        self.btn_osfiles_level2.setEnabled(False)
        self.btn_osfiles_level3.setEnabled(False)

        self.label_success.setHidden(True)
        self.label_tranx.setHidden(True)
        self.label_tranx_url.setHidden(True)

        # code for switch button # https://pypi.org/project/pyqt-switch/
        switch = PyQtSwitch()
        # individual or combined mode
        switch.setToolTip('Individual mode lets you use any of the security functions.\n' + 
                          'Combined mode ensures these functions are executed in order')
        
        switch.toggled.connect(self.__toggled)
        # switch.setAnimation(True)  # does not work for some reason
        # switch.setCircleDiameter(40)
        self.__label = QLabel()
        self.__label.setText('Individual Mode')
        lay = QFormLayout()
        lay.addRow(self.__label, switch)
        self.setLayout(lay)
        # end for switch button

        # the swict should be in combined mode by default
        # Click the button
        QTest.mouseClick(switch, Qt.LeftButton)

        # open saved file locations
        self.btn_saved_masked_layers_loc.clicked.connect(self.open_masked_layer_location) # masked layer
        self.btn_enc_volume_location.clicked.connect(self.open_enc_volume_location)
        self.btn_dec_volume_location.clicked.connect(self.open_dec_volume_location)
        self.btn_passphrase_location.clicked.connect(self.open_passphrase_location)
        self.btn_open_decrypted_folder.clicked.connect(self.open_decrypted_folder_location)
        #self.btn_save_notarisation_record.clicked.connect(self.save_notarisation_pdf)

        # binning
        self.btn_saved_binned_layers_loc.clicked.connect(self.open_binned_layer_location) # masked layer
       
        # set the Safeguard tabs disabled
        self.tabWidget_2.setTabEnabled(1,False) #enable/disable the encryption tab
        self.tabWidget_2.setTabEnabled(2,False) #enable/disable the encryption tab
        # set the Verification - decryption and display tab disabled
        self.tabWidget_3.setTabEnabled(1,False) #enable/disable the decryption tab
        self.tabWidget_3.setTabEnabled(2,False) #enable/disable the display tab

        # during safeguarding, show encryption tab after masking or binning
        self.masking_next_pushButton.clicked.connect(self.change_safeguarding_tab)
        self.binning_next_pushButton.clicked.connect(self.change_safeguarding_tab)
        # during safeguarding, show notarisation tab  after encryption
        self.encryption_next_pushButton.clicked.connect(self.change_safeguarding_tab3)
        self.encryption_back_pushButton.clicked.connect(self.revert_safeguarding_tab)
        # the next buttons are initially disabled, enabled if the security function on the tab is executed
        self.masking_next_pushButton.setEnabled(False)
        self.binning_next_pushButton.setEnabled(False)
        self.encryption_next_pushButton.setEnabled(False)
        #self.encryption_back_pushButton.setEnabled(False)
        self.notarise_back_pushButton.clicked.connect(self.change_safeguarding_tab)

       
        self.verify_next_pushButton.setEnabled(False)
        self.decrypt_next_pushButton.setEnabled(False)

        # hide these tow until dataset decrypted
        self.label_decrypted_volume.hide()
        self.btn_dec_volume_location.hide()

        # during verification, next buton call decryption tab
        self.verify_next_pushButton.clicked.connect(self.change_verification_tab)

        self.decrypt_next_pushButton.clicked.connect(self.change_verification_tab2)
        self.decrypt_back_pushButton.clicked.connect(self.revert_verification_tab)

        self.display_back_pushButton.clicked.connect(self.change_verification_tab)
        
        # hide the groupbox 
        self.separatefile_notarisation_groupBox.hide()
        self.label_osfile_to_notarise.hide()
        self.btn_osfile_to_notarise.hide()

        # shielding passphrase
        self.btn_encrypt_passphrase.setEnabled(False) 
        self.btn_decrypt_passphrase.setEnabled(False)

        self.label_generated_key_pair.hide()
        self.label_encrypted_passphrase.hide()
        self.label_decrypted_passphrase.hide()

        # Initial Mode
        self.combined = True
        
        self.safeguard_progressBar.setValue(0)  
        self.verification_progressBar.setValue(0) 

        # https://github.com/jacklam718/PyQt-ProgressDialog
        # the green color we want   = #4CAF50
        # the grey background color =  #E9E9E9        
        # https://stackoverflow.com/questions/72644810/how-to-edit-stylesheet-for-qprogressbar
        style = """
                QProgressBar {
                    border: 1px solid grey;
                    border-radius: 5px;
                    text-align: center;
                
                    background-color: #E9E9E9;                    
                    width: 20px;
                }
                QProgressBar::chunk {
                    background-color: #4CAF50;
                }
                """

        self.safeguard_progressBar.setStyleSheet(style)
        self.verification_progressBar.setStyleSheet(style)
        
        # set allignment from here, as above way doesn't work
        # https://www.geeksforgeeks.org/pyqt5-how-to-change-style-and-size-of-text-progress-bar/
        self.safeguard_progressBar.setAlignment(Qt.AlignCenter)        
        self.verification_progressBar.setAlignment(Qt.AlignCenter) 
        
        # treeview display 
        self.treeWidget.itemClicked.connect(self.onItemClicked)

        self.tabWidget_2.tabBarClicked.connect(self.handle_tabbar_clicked)
        # on label anonymise text click event
        self.label_safeguard_options.mousePressEvent = self.doSomething

        # show help website
        self.btn_help.clicked.connect(self.onHelp)
        
        # Connect the signal to the handler
        # updates the variables in this class also
        self.env_var.envVariablesUpdated.connect(self.update_env_variables)

        # Connect the "Clear" button to the clear_masked_layers function
        #self.btn_clear_layers.clicked.connect(self.clear_anonymised_layers)

    # Override the closeEvent method to handle both ESC key and X button
    def closeEvent(self, event):
        # Show a confirmation dialog
        reply = QMessageBox.question(
            self,
            "Confirm Exit",
            "Are you sure you want to close the plugin? Your progress will be saved, but please ensure that your anonymized or encrypted layers are also saved before exiting.",
            QMessageBox.Yes | QMessageBox.No,
            QMessageBox.No
        )

        # Handle the user's response
        if reply == QMessageBox.Yes:
            event.accept()  # Close the plugin
        else:
            event.ignore()  # Keep the plugin open

    def set_working_directory(self, wd):

        print("environ[WORKING_DIR]: " + str(wd)) # str(environ["WORKING_DIR"]))
        self.working_directory = wd #environ["WORKING_DIR"]
        print("self.working_directory: " + str(wd)) #self.working_directory))

        if(self.working_directory):
            self.working_directory_set = True
            print('\t### working_directory ' + str(self.working_directory))
            self.label_working_dir.setText(self.working_directory)
        else:
            print('\t### working_directory not set' + str(self.working_directory))

    def doSomething(self, event):
        print('anonymise')

    def onHelp(self):
        """
        Open the help documentation in a web browser.
        """
        docDir = "https://sharmapn.github.io/MapSafeQGISGeoPrivPlugin/"
        QDesktopServices.openUrl(QUrl(docDir))

    # upon selection of any safeguard options in the tab
    def handle_tabbar_clicked(self, index):
        print(index)
        # only in individual mode and certain index level
        if(self.combined == False and index == 0):
            self.label_safeguard_options.setText("The original dataset can be anonymised.")
            self.label_safeguard_options.show()
        elif(self.combined == False and index == 1):          
            self.label_safeguard_options.setText("Any dataset can be encrypted: original or anonymised.")
            self.label_safeguard_options.show()
        elif(self.combined == False and index == 2):    
            self.label_safeguard_options.setText("Any dataset can be notarised: original, anonymised or encrypted..")
            self.label_safeguard_options.show()
        #print("x2:", index * 2)
  
    # individual or combined mode actions based on switch button
    def __toggled(self, f):
        if f:            
            print('Combined Mode')
            self.combined = True
            self.__label.setText('Combined Mode')
            self.masking_next_pushButton.show() 
            self.binning_next_pushButton.show() 
            self.encryption_next_pushButton.show() 
            self.encryption_back_pushButton.show() # making sure
            #notarisation 
            self.separatefile_notarisation_groupBox.hide()
            self.label_osfile_to_notarise.hide()
            self.btn_osfile_to_notarise.hide()
            # disable encryption tab
            self.tabWidget_2.setTabEnabled(1, False) #enable/disable the encryption tab
            self.tabWidget_2.setTabEnabled(2, False) #enable/disable the encryption tab

            # show progress bar
            self.safeguard_progressBar.show() 
            self.label_2.show() 
            self.label_3.show() 
            self.label_9.show() 
             # show the label with information
            self.label_safeguard_options.hide()            

        else:
            print('Individual Mode')
            self.combined = False
            self.__label.setText('Individual Mode') 
            self.masking_next_pushButton.hide() 
            self.binning_next_pushButton.hide() 
            self.encryption_next_pushButton.hide()
            #self.encryption_back_pushButton.hide()
            #notarisation
            self.separatefile_notarisation_groupBox.show()
            self.label_osfile_to_notarise.show()
            self.btn_osfile_to_notarise.show()
            # enable encryption tab
            self.tabWidget_2.setTabEnabled(1,True) #enable/disable the encryption tab
            self.tabWidget_2.setTabEnabled(2,True) #enable/disable the encryption tab

            # hide progress bar
            self.safeguard_progressBar.hide() 
            self.label_2.hide() # hide the workflow stops: anonymise, encrypt and notarise
            self.label_3.hide() 
            self.label_9.hide()
            # show the label with information
            # making it multi line 
            self.label_safeguard_options.setWordWrap(True)    
            self.label_safeguard_options.setText("The original dataset can be anonymised, directly encrypted, or directly notarised.")
            self.label_safeguard_options.show()                      


    def revert_safeguarding_tab(self):
        self.tabWidget_2.setCurrentIndex(0)

    def change_safeguarding_tab(self):
        self.tabWidget_2.setCurrentIndex(1)

    def change_safeguarding_tab2(self):
        self.tabWidget_2.setCurrentIndex(2)

    def change_safeguarding_tab3(self):
        self.tabWidget_2.setCurrentIndex(2)

    def revert_verification_tab(self):
        self.tabWidget_3.setCurrentIndex(0)

    def change_verification_tab(self):
        self.tabWidget_3.setCurrentIndex(1)

    def change_verification_tab2(self):
        self.tabWidget_3.setCurrentIndex(2)

    #def run(self):
    #    print ('here run')

    def onStateChanged(self):
        if(self.chkBox_privRating_two_levels.isChecked()):
            self.label_minimum_offset.setEnabled(True)
            self.label_maximum_offset.setEnabled(True)
            self.text_minimum_offset.setEnabled(True)
            self.text_maximum_offset.setEnabled(True)
        else:
            self.label_minimum_offset.setEnabled(False)
            self.label_maximum_offset.setEnabled(False)
            self.text_minimum_offset.setEnabled(False)
            self.text_maximum_offset.setEnabled(False)

    # Encryption   

    # local file that contains the location of env file 
    # env file is contained in a text file named 'loc.txt' within the plugin directory
    # def read_env_file_location(self): #, env_file_loc):
    #     try:
    #         if(self.internal_file_loc is None or self.internal_file_loc == ""):
    #             print('Internal file containing ENV file location not supplied')
    #             QMessageBox.information(None, "DEBUG:", 'Please specify location of file with ENV file location. ') 
    #         else: 
    #             #open and read the file after the overwriting:
    #             f = open(self.internal_file_loc, "r") # "loc.txt"
    #             env_file_loc = f.read()  # get the location of tyhe env file
    #             print(env_file_loc)
    #             f.close()
    #             return env_file_loc        
    #     except Exception as e:
    #         print(f"Exception reading env file - {e}")
    #         QMessageBox.information(None, "DEBUG:", 'Exception reading env file . ') 
    
    # Reads the location of an environment (ENV) file from a specified internal file.
    # Provides user feedback if the internal file is not supplied or if any error occurs.
    def read_env_file_location(self):
        try:
            # Check if the internal file location is provided
            if not self.internal_file_loc:
                print('Internal file containing ENV file location not supplied.')
                QMessageBox.information(None, "Error", "Please specify the location of the file containing the ENV file location.")
            else:
                # Open the internal file and read the location of the ENV file
                with open(self.internal_file_loc, "r") as f:
                    env_file_loc = f.read().strip()  # Read and strip any extra whitespace
                    print(f"ENV file location: {env_file_loc}")
                # Return the environment file location
                return env_file_loc

        # Handle specific exceptions related to file handling
        except FileNotFoundError:
            QMessageBox.warning(None, "Error", f"The file '{self.internal_file_loc}' was not found.")
            print(f"Error: The file '{self.internal_file_loc}' was not found.")
        except PermissionError:
            QMessageBox.warning(None, "Error", f"Permission denied to access the file '{self.internal_file_loc}'.")
            print(f"Error: Permission denied to access the file '{self.internal_file_loc}'.")
        except Exception as e:
            # Handle any other unexpected exceptions
            QMessageBox.warning(None, "Error", f"An unexpected error occurred: {e}")

    # sets the location of the env file, overwriting whatever is there
    #  # <-- Here, we create *and connect* the sub window's signal to the main window's slot
    def set_env_file_location(self): #, env_file_loc):  
        #internal_file_loc = f'{self.plugin_dir}/loc.txt'
        # call the signal
        # https://stackoverflow.com/questions/68453805/how-to-pass-values-from-one-window-to-another-pyqt
        self.env_var.submitClicked.connect(self.on_sub_window_confirm)
        self.env_var.show()

        #return

    # Use the signal to update the same variables here
    def update_env_variables(self, updated_vars):
        # Update the plugin's variables with the new values
        self.blockchain_address = updated_vars.get("BLOCKCHAIN_ADDRESS", "")
        self.contract_address = updated_vars.get("CONTRACT_ADDRESS", "")
        self.node_url = updated_vars.get("NODE_URL", "")
        self.private_key = updated_vars.get("PRIVATE_KEY", "")
        self.working_directory = updated_vars.get("WORKING_DIR", "")

        print("Environment variables updated in the main plugin:")
        print(f"Working Directory: {self.working_directory}")
    
    def on_sub_window_confirm(self, url):  # <-- This is the main window's slot
        print(f"Saved Working dir : {url}")
        self.label_working_dir.setText(str(url))

        # try:
        #     filepath = QFileDialog.getOpenFileName()
        #     env_file_loc = filepath[0] 
        #     print("User chosen ENV File: " + env_file_loc)

        #     if(env_file_loc is None or env_file_loc == ""):
        #         print('ENV file location not supplied')
        #         QMessageBox.information(None, "DEBUG:", 'Please specify ENV file location. ') 
        #     else: 
        #         print('read_env_file_location: ' + str(env_file_loc))
        #         f = open(self.internal_file_loc, "w")  # "demofile3.txt"
        #         f.write(env_file_loc) #"Woops! I have deleted the content!")
        #         f.close()
                
        #         #load the enviornment variable again from the env file
        #         load_dotenv(env_file_loc) #self.internal_file_loc) 

        #         # assign the env variable to the variable used in our script
        #         self.set_working_directory()

        #         print('Set ENV file location in internal_file_loc: ' + self.internal_file_loc)
        #         #open and read the file after the overwriting:
        #         #f = open(env_file_loc, "r") # "demofile3.txt"
        #         #print(f.read())                               
        # except Exception as e:
        #     print(f"Exception setting env file - {e}")
        #     QMessageBox.information(None, "DEBUG:", 'Exception reading env file . ') 

    # let user choose the passphrase file - note three passphrase files are created by the plugin after encryption
    # def readPassphraseFileForEncryption(self):
    #     print('Read Passphrase file for Encryption')
    #     try:
    #         filepath = QFileDialog.getOpenFileName()
    #         filename = filepath[0] 
    #         if filename != "":
    #             print(filename)
    #             self.passphrase_file_to_encrypt = filename 
    #             print('self.passphrase_file_to_encrypt')
    #             print(self.passphrase_file_to_encrypt)
    #             self.passphrase_file_chosen = True
    #         else:
    #             self.passphrase_file_chosen = False
    #             self.passphrase_file_to_encrypt = ""
    #             QMessageBox.information(None, "DEBUG:", 'Please specify Passphrase file for Encryption. ')
    #     except Exception as e:
    #         print(f'Error opening file + {e}')

    from PyQt5.QtWidgets import QMessageBox, QFileDialog

    # Allows the user to select a passphrase file for encryption.
    # Updates the internal state with the selected file path and provides user feedback if no file is selected.
    # Handles file-related errors gracefully.
    def readPassphraseFileForEncryption(self):
        print('Read Passphrase file for Encryption')
        try:
            # Open a file dialog to allow the user to select a file
            filepath = QFileDialog.getOpenFileName()
            # Get the first element from the returned tuple (the file path)
            filename = filepath[0]
            # Check if a valid file was selected
            if filename:
                # Store the selected file path in the internal variable
                self.passphrase_file_to_encrypt = filename

                # Print the selected file path for debugging
                print(f"Passphrase file selected: {filename}")
                print('self.passphrase_file_to_encrypt:', self.passphrase_file_to_encrypt)
                # Mark that a passphrase file has been chosen
                self.passphrase_file_chosen = True
            else:
                # If no file was selected, reset the internal state and show a message
                self.passphrase_file_chosen = False
                self.passphrase_file_to_encrypt = ""
                QMessageBox.information(None, "Info", "Please specify a Passphrase file for Encryption.")
                print("No passphrase file selected.")

        # Handle specific exceptions related to file handling
        except FileNotFoundError:
            # Handle case where the file does not exist
            QMessageBox.warning(None, "Error", "The selected file was not found.")
            print("Error: The selected file was not found.")
        except PermissionError:
            # Handle case where the user does not have permission to access the file
            QMessageBox.warning(None, "Error", "Permission denied to access the selected file.")
            print("Error: Permission denied to access the selected file.")
        except Exception as e:
            # Handle any other unexpected exceptions
            QMessageBox.warning(None, "Error", f"An unexpected error occurred: {e}")
            print(f"Error opening file: {e}")

    # # read the public key of recipient
    # def readPublicKeyForEncryption(self):
    #     print('Read Public Key for Passphrase Encryption')
    #     try:
    #         filepath = QFileDialog.getOpenFileName()
    #         filename = filepath[0] 
    #         if filename != "":
    #             print(filename)
    #             self.public_key_for_encryption = open(filename).read()
    #             print('self.public_key_for_encryption')
    #             print(self.public_key_for_encryption)
    #             self.PKE.read_PubKey(filename)
    #             self.public_key_chosen = True
    #         else:
    #             self.public_key_for_encryption = ""
    #             QMessageBox.information(None, "DEBUG:", 'Please specify Public Key for Passphrase Encryption. ')

    #         # enable encrypt button
    #         if(self.passphrase_file_chosen and self.public_key_chosen):
    #             self.btn_encrypt_passphrase.setEnabled(True)

    #     except Exception as e:
    #         print(f'Error opening file as {e}')

    #Allows the user to select a public key file for passphrase encryption.
    #Updates the internal state with the selected key and enables the encryption button if all requirements are met.
    #Handles file-related errors gracefully and provides user feedback.
    def readPublicKeyForEncryption(self):
        print('Read Public Key for Passphrase Encryption')

        try:
            # Open a file dialog to allow the user to select the public key file
            filepath = QFileDialog.getOpenFileName()
            # Get the first element from the returned tuple (the file path)
            filename = filepath[0]
            # Check if a valid file was selected
            if filename:
                # Print the selected file path for debugging
                print(f"Public key file selected: {filename}")
                # Read the public key file content
                with open(filename, 'r') as file:
                    self.public_key_for_encryption = file.read()

                # Print the public key content for debugging
                print('Public key content loaded:')
                print(self.public_key_for_encryption)
                # Load the public key into the PKE (Public Key Encryption) system
                self.PKE.read_PubKey(filename)
                # Mark that a public key has been chosen
                self.public_key_chosen = True
            else:
                # If no file was selected, reset the internal state and show a message
                self.public_key_for_encryption = ""
                QMessageBox.information(None, "Info", "Please specify a Public Key for Passphrase Encryption.")
                print("No public key file selected.")
            # Enable the encrypt button if both the passphrase file and public key are chosen
            if self.passphrase_file_chosen and self.public_key_chosen:
                self.btn_encrypt_passphrase.setEnabled(True)

        # Handle specific exceptions related to file handling
        except FileNotFoundError:
            QMessageBox.warning(None, "Error", "The selected public key file was not found.")
            print("Error: The selected public key file was not found.")
        except PermissionError:
            QMessageBox.warning(None, "Error", "Permission denied to access the selected public key file.")
            print("Error: Permission denied to access the selected public key file.")
        except Exception as e:
            # Handle any other unexpected exceptions
            QMessageBox.warning(None, "Error", f"An unexpected error occurred: {e}")
            print(f"Error opening file: {e}")

    def request_encrypt_passphrase(self):
        pke = self.PKE.encrypt(self.working_directory, self.passphrase_file_to_encrypt, self.label_encrypted_passphrase, self.label_encrypted_passphrase_file)
        print('Calling encryption of passphrase')

    # Decryption   
        
    # if passphrase is already in plaintext, let the user choose the passphrase file
    # in this case, no decryption of passphrase is required 
    # def readPassphraseFile(self):
    #     print('Read Pasphrase File')
    #     try:
    #         filepath = QFileDialog.getOpenFileName()
    #         filename_pp = filepath[0] 
    #         if filename_pp != "":
    #             print('passphrase file: ' + str(filename_pp))
    #             self.passphrase = open(filename_pp).read()
    #             self.txt_passphrase.setText(self.passphrase)
    #         else:
    #             print("self.passphrase: " + str(self.passphrase))
    #     except Exception as e:
    #         print(f'Error opening file + {e}')

    #import chardet  # Library to detect the character encoding of the file

    # This function allows the user to select a passphrase file,
    # validates that the file is a plaintext file, and displays its content in a text box.
    def readPassphraseFile(self):
        print('Read Passphrase File')  # Debug message to indicate the function is called       
        try:
            # Open a file dialog for the user to select a file
            # Filter to show only text files by default
            filepath = QFileDialog.getOpenFileName(filter="Text files (*.txt);;All files (*)")
            filename_pp = filepath[0]  # Get the selected file path
            
            # Check if a file was selected
            if filename_pp:
                print('Passphrase file selected: ' + str(filename_pp))
                
                # Open the file in binary mode to check its encoding
                with open(filename_pp, 'rb') as f:
                    raw_data = f.read()  # Read the entire file content as binary
                    result = chardet.detect(raw_data)  # Detect the file's character encoding
                    encoding = result['encoding']  # Get the detected encoding
                    
                    # If no encoding is detected, raise an error
                    if not encoding:
                        raise ValueError("Unable to detect the file encoding. The file may not be plaintext.")
                    
                    # Decode the file content using the detected encoding
                    content = raw_data.decode(encoding)
                    
                    # Ensure the decoded content contains only printable characters
                    # This helps avoid reading binary or non-human-readable files
                    if not content.isprintable():
                        raise ValueError("The file contains non-printable characters. Ensure it is a plaintext file.")
                
                # If all validations pass, save the content as the passphrase
                self.passphrase = content
                self.txt_passphrase.setText(self.passphrase)  # Display the passphrase in the text box
            
            else:
                # No file was selected, print a message for debugging purposes
                print("No file selected.")
                print("self.passphrase: " + str(self.passphrase))

        # Handle any exceptions that occur during file selection or processing
        except Exception as e:
            print(f"Error opening file: {e}")  # Print the error message for debugging


    # if passphrase is provided in encrypted form, let user choose the encrypted file
    # def readPassphraseFileForDecryption(self):
    #     print('Read Passphrase file for Decryption')
    #     try:
    #         filepath = QFileDialog.getOpenFileName()
    #         filename_pp = filepath[0] 
    #         if filename_pp != "":
    #             print('passphrase file: ' + str(filename_pp))
    #             self.passphrase_to_decrypt_filename = filename_pp 
    #             print('self.passphrase_to_decrypt_filename')
    #             print(self.passphrase_to_decrypt_filename)
    #             self.encrypted_passphrase_file_chosen  = True
    #         else:
    #             print("self.passphrase_to_decrypt_filename: " + str(self.passphrase_to_decrypt_filename))
    #     except Exception as e:
    #         print(f"Error Reading Passphrase file for Decryption + {e}")
    #         QMessageBox.information(None, "DEBUG:", 'Error Reading Passphrase file for Decryption. ') 
   
 
    # This function allows the user to select a passphrase file for decryption.
    # It validates the file selection and sets the necessary attributes for decryption.
    def readPassphraseFileForDecryption(self):
        print('Read Passphrase file for Decryption')  # Debug message indicating the function was called

        try:
            # Open a file dialog to allow the user to select a file
            filepath = QFileDialog.getOpenFileName(filter="Text files (*.txt);;All files (*)")
            filename_pp = filepath[0]  # Get the file path from the dialog result

            # Check if a file was selected
            if filename_pp:
                print('Passphrase file selected: ' + str(filename_pp))

                # Store the selected file path in an attribute to use during decryption
                self.passphrase_to_decrypt_filename = filename_pp
                print('Passphrase file for decryption stored as: ' + str(self.passphrase_to_decrypt_filename))

                # Set a flag to indicate that an encrypted passphrase file has been chosen
                self.encrypted_passphrase_file_chosen = True

            else:
                # If no file was selected, print a debug message
                print("No file selected.")
                print("self.passphrase_to_decrypt_filename: " + str(self.passphrase_to_decrypt_filename))

        # Handle any exceptions that occur during the file selection process
        except Exception as e:
            # Print the error message for debugging
            print(f"Error Reading Passphrase file for Decryption: {e}")

            # Display a message box to inform the user of the error
            QMessageBox.information(None, "Error", "Error Reading Passphrase file for Decryption.")


    #read the current user's private key for decryption of passphrase
    # def readPrivateKeyForDecryption(self):
    #     print('Read Private Key for Passphrase Decryption')
    #     try:
    #         filepath = QFileDialog.getOpenFileName()
    #         filename_pk = filepath[0] 
    #         if filename_pk != "":
    #             print('pk filename:' + str(filename_pk))
    #             self.private_key_for_decryption = open(filename_pk).read()
    #             print('self.private_key_for_encryption')
    #             print(self.private_key_for_encryption)
    #             self.PKE.read_PriKey(filename_pk)
    #             self.private_key_chosen = True
    #         else:
    #             print("self.private_key_for_encryption: " + str(self.private_key_for_encryption))

    #         # enable decrypt button
    #         if(self.encrypted_passphrase_file_chosen and self.private_key_chosen):
    #             self.btn_decrypt_passphrase.setEnabled(True)

    #     except Exception as e:
    #         print(f"Error opening file + {e}")
    #         QMessageBox.information(None, "DEBUG:", 'Error opening File. ') 

    def readPrivateKeyForDecryption(self):
        """
        This function allows the user to select a private key file for passphrase decryption.
        It validates the file selection, reads the private key content, and enables the decryption button
        if both the encrypted passphrase file and private key are chosen.
        """
        print('Read Private Key for Passphrase Decryption')  # Debug message indicating the function was called

        try:
            # Open a file dialog to allow the user to select the private key file
            filepath = QFileDialog.getOpenFileName(filter="Key files (*.pem *.key);;All files (*)")
            filename_pk = filepath[0]  # Get the file path from the dialog result

            # Check if a file was selected
            if filename_pk:
                print('Private key file selected: ' + str(filename_pk))

                # Read the private key file content
                with open(filename_pk, 'r') as f:
                    self.private_key_for_decryption = f.read()

                print('Private key content loaded into self.private_key_for_decryption.')

                # Load the private key into the PKE (Public Key Encryption) system
                self.PKE.read_PriKey(filename_pk)

                # Set a flag to indicate that the private key has been chosen
                self.private_key_chosen = True

            else:
                # If no file was selected, print a debug message
                print("No private key file selected.")
                print("self.private_key_for_encryption: " + str(self.private_key_for_encryption))

            # Enable the decrypt button if both the encrypted passphrase file and private key are chosen
            if self.encrypted_passphrase_file_chosen and self.private_key_chosen:
                self.btn_decrypt_passphrase.setEnabled(True)
                print("Decrypt button enabled.")

        # Handle any exceptions that occur during file selection or processing
        except Exception as e:
            # Print the error message for debugging
            print(f"Error opening file: {e}")

            # Display a message box to inform the user of the error
            QMessageBox.information(None, "Error", "Error opening the private key file.")


    def request_decrypt_passphrase(self):
        try:
            #self.pub_key_enc = PublicKeyEncryption()
            pke = self.PKE.decrypt(self.working_directory, self.passphrase_to_decrypt_filename, self.txt_passphrase, self.label_decrypted_passphrase, self.label_decrypted_passphrase_file)
            print('Calling decryption of passphrase')
            self.passphrase = self.PKE.return_passphrase()
            print('self passphrase')
            print(self.passphrase)
        except Exception as e:
            print(f"Error decrypting passphrase() + {e}")
            QMessageBox.information(None, "DEBUG:", 'Error Decrypting Passphrase. ') 

    def generate_keys(self):
        print('Generate Keys')
        try:
            self.PKE.generate_keys(self.working_directory, self.label_generated_key_pair, self.label_generated_keys)
            #btn_generate_keys        
        except Exception as e:
            print(f"Error generate_keys() + {e}")
            QMessageBox.information(None, "DEBUG:", 'Error Generating Keys. ') 
   

    def requestENVFileDirectory(self):
        print('requestENVFileDirectory')
        try:
            filepath = QFileDialog.getOpenFileName()
            filename = filepath[0] 
            print(filename)
            self.env_file_loc = filename 
            print('self.env_file_loc: ' + str(self.env_file_loc))
            self.env_file_path_set = True
            print('env_file_loc: ' + str(self.env_file_loc) + ' self.env_file_path_set ' + str(self.env_file_path_set))
            
            if(self.env_file_loc is None or self.env_file_loc == ""):
                print('Error')
            else: 
                # set location of env file in the loc.txt     
                self.set_env_file_location(self.env_file_loc)

            #load the enviornment variable again from the env file
            load_dotenv(self.env_file_loc)  

        except Exception as e:
            print(f"Error setting ENV file location+ {e}") 
            QMessageBox.information(None, "DEBUG:", 'Error setting ENV file location ')      

    # https://reintech.io/blog/how-to-create-pdfs-with-python
    def create_pdf(self):
        c = canvas.Canvas("C:\\dataset\\simple_pdf.pdf")
        c.drawString(100, 750, "Hello, I am a PDF document created with Python!")
        c.save()    
        print('PDF created')
        
    def request_passphrase(self):
        self.passph = Passphrase()
        pp = self.passph.generate_passphrase()
        if pp is not None:
            self.passphrase_generated = True
        # update class variable
        self.passphrase = str(pp)
        print ('passphrase ' + self.passphrase)        
        self.label_passphrase.setStyleSheet("color: #AA336A")  #dark pink
        self.label_passphrase.setText( self.passphrase )

    def mask_based_on_privacy_rating(self):
        self.geomasking = GeoMasking()

        #get layername
        layers = QgsProject.instance().mapLayers().values()
        layerName = None
        for layer in layers:
            layerName = layer.name()
            print('LayerName: ' + str(layerName))
            break

        if layerName is None:
            QMessageBox.information(None, "DEBUG:", 'No Layer detected. Please load a dataset.')
            return 

        #input_layer = QgsProject.instance().mapLayersByName(layerName)[0]  #"all_clusters_kamloops"  
        self.little_masked_layername = layerName + "_masked"
        print('self.little_masked_layername : ' + str(self.little_masked_layername ))

        # perform the same for two levels masking
        if self.two_geomasking_levels is True:
            self.more_masked_layername = layerName + "_moremasked" 
            print('self.more_masked_layername : ' + str(self.more_masked_layername ))

        

        # if(self.two_geomasking_levels):
        #     print('Two levels chosen A')
        #     min_offset = int(self.text_minimum_offset.toPlainText())  # self.text_minimum_offset.text()    # 100  
        #     max_offset = int(self.text_maximum_offset.toPlainText())  # self.text_maximum_offset.text()  # 100
        #     print('For second Level - using min_offset = ' + str(min_offset))
        #     print('For second Level - using max_offset = ' + str(max_offset))
        # else: 
        # min_offset = 0
        # max_offset = 0 

        # print("minimum_distance sent : " + str(self.minimum_distance))
        # print("maximum_distance sent : " + str(self.maximum_distance))        

        try:
            # minimum boundary = 500
            # and first maximum distance to try = 500
            # self.minimum_distance and self.maximum_distance are 0
            #i = minimum_offset_to_start
            provided_privacy_rating = 90
            minimum_distance_to_start = self.minimum_distance  # from user input #300
            compute_privacy_rating = True
            self.add_layer_to_canvas = False
            achieved_privacy_rating = 0
            min_offset = 0 # offset used in two level masking - its aded to the 1st (user provided) max value to get the 2nd max value
            max_offset = 0 # offset used in two level masking - its aded to the 1st (user provided) max value to get the 2nd max value
            self.maximum_distance = 0  #should be made aero everytime it starts

            while (achieved_privacy_rating <= provided_privacy_rating):            
            #numbers=[11,33,55,39,55,75,37,21,23,41,13]
            #for num in numbers:                 
                self.maximum_distance = minimum_distance_to_start + 100       
                print('max_offset ' + str(max_offset))
                achieved_privacy_rating = self.geomasking.geomasking_function(layerName, 
                                                    minimum_distance_to_start, self.maximum_distance, # only these two values are involved
                                                    compute_privacy_rating, self.two_geomasking_levels, 
                                                    self.little_masked_layername, self.more_masked_layername, 
                                                    min_offset, max_offset, 
                                                    self.label_privacy_rating, self.label_masking_time, self.progressBar, 
                                                    self.safeguard_progressBar, self.add_layer_to_canvas)
                #self.maximum_distance = self.maximum_distance + 100 
                print('\tAchieved Privacy_rating: ' + str(achieved_privacy_rating))
                if achieved_privacy_rating >= provided_privacy_rating:
                    print('Achieved_privacy_rating: ' + str(achieved_privacy_rating))
                    break
            
        
        except Exception as e:
            print(f'Exception while performing masking + {e}')
            QMessageBox.information(None, "DEBUG:", 'Exception while performing masking. ')

    # allow user to clear them 
    def clear_anonymised_layers(self):
        try:

            # # Show a confirmation dialog before proceeding
            # reply = QMessageBox.question(
            #     self,
            #     "Confirm Deletion",
            #     "Are you sure you want to delete all masked and binned layers?",
            #     QMessageBox.Yes | QMessageBox.No
            # )
            # if reply == QMessageBox.No:
            #     return

            # Get all layers in the QGIS project
            layers = QgsProject.instance().mapLayers().values()

            # Collect the IDs of layers to remove
            layers_to_remove = [
                layer.id() for layer in layers 
                if layer.name().endswith("_masked") or layer.name().endswith("_moremasked") or "_Hex" in layer.name()
            ]

            # Counter to track how many layers were removed
            removed_count = len(layers_to_remove)

            # Remove the layers by their IDs
            for layer_id in layers_to_remove:
                QgsProject.instance().removeMapLayer(layer_id)
                print(f"Removed layer: {layer_id}")

            # Show a message to the user
            if removed_count > 0:
                #QMessageBox.information(self, "Layers Removed", f"{removed_count} masked or hex layer(s) have been removed.")
                print("Layers Removed", str(removed_count) + " masked or hex layer(s) have been removed.")
            else:
                #QMessageBox.information(self, "No Layers Found", "No masked or hex layers were found.")
                print("Layers Removed", str(removed_count) + " masked or hex layer(s) have been removed.")

        except Exception as e:
            QMessageBox.critical(self, "Error", f"An unexpected error occurred: {e}")
            print(f"Error in clear_masked_layers: {e}")

    def request_geomasking(self):

        # clear previous anonymised layers
        self.clear_anonymised_layers()

        # rather than the saved text - everytime masking should allow to save again.
        self.btnSaveMasked.setText('Save')
        
        self.geomasking = GeoMasking()
        
        # get the layername and full filepath of the original (user loaded) layer 
        # Loop through all loaded layers in QGIS  
        layerName = None     
        for layer in QgsProject.instance().mapLayers().values():
            # Get the source path
            source_path = layer.source()            
            # Split the path at '|' and keep only the actual file path
            self.original_layer_full_path = source_path.split('|')[0]                        
            print(f"Original Filepath: {self.original_layer_full_path}\n")
            # set this in the encryption Tab
            #self.label_osfile_first_level.setText(str(self.original_layer_full_path))
            layerName = layer.name()
            #print('LayerName: ' + str(layerName))
            print(f"Original Layer Name: {layer.name()}")
            break

        # # set the anonymised layers
        # layers = QgsProject.instance().mapLayers().values()
        # layerName = None
        # for layer in layers:
        #     layerName = layer.name()
        #     print('LayerName: ' + str(layerName))
        #     break

        if layerName is None:
            QMessageBox.information(None, "DEBUG:", 'No Layer detected. Please load a dataset.')
            return 
        
        # clear the saved file directpry
        self.label_masking_saved.setText('')
        self.label_masking_time.setText('')
        self.label_privacy_rating.setText('')

        compute_privacy_rating = self.chkBox_privRating.isChecked()  
        print ('Compute Privacy Rating ' + str(compute_privacy_rating))  
        self.two_geomasking_levels = self.chkBox_privRating_two_levels.isChecked()  
        print ('Compute Privacy Rating Second level: ' + str(self.two_geomasking_levels))  

        #input_layer = QgsProject.instance().mapLayersByName(layerName)[0]  #"all_clusters_kamloops"  
        #layerName = self.sanitize(layerName)
        self.little_masked_layername = self.sanitize(layerName) + "_masked"
        #print('self.little_masked_layername : ' + str(self.little_masked_layername ))

        # perform the same for two levels masking
        if self.two_geomasking_levels is True:
            #layerName = self.sanitize(layerName)
            self.more_masked_layername = self.sanitize(layerName) + "_moremasked" 
            #print('self.more_masked_layername : ' + str(self.more_masked_layername ))
       

        if(self.two_geomasking_levels):
            print('Two levels chosen A')
            min_offset = int(self.text_minimum_offset.toPlainText())  # self.text_minimum_offset.text()    # 100  
            max_offset = int(self.text_maximum_offset.toPlainText())  # self.text_maximum_offset.text()  # 100
            print('For second Level - using min_offset = ' + str(min_offset))
            print('For second Level - using max_offset = ' + str(max_offset))
        else: 
            min_offset = 0
            max_offset = 0 

        print("minimum_distance sent : " + str(self.minimum_distance))
        print("maximum_distance sent : " + str(self.maximum_distance))

        try:
            gm = self.geomasking.geomasking_function(layerName, self.minimum_distance, self.maximum_distance, compute_privacy_rating, 
                                                 self.two_geomasking_levels, 
                                                 self.little_masked_layername, self.more_masked_layername,
                                                 min_offset, max_offset, self.label_privacy_rating,
                                                 self.label_masking_time, self.progressBar, self.safeguard_progressBar, self.add_layer_to_canvas)

            self.btn_mask.setText("Mask again")

        except Exception as e:
            print(f'Exception while performing masking + {e}')
            QMessageBox.information(None, "DEBUG:", 'Exception while performing masking. ') 

        print('Geomasking Initiated')
        self.get_layers()

        # enable the actions that can be perfomed after masking         
        self.btnSaveMasked.setEnabled(True)
        self.btn_saved_masked_layers_loc.setEnabled(True)
        # now these two only come after save button pressed
        #self.masking_next_pushButton.setEnabled(True)       # enable next button - for encryption
        #self.tabWidget_2.setTabEnabled(1, True)             #enable/disable the encryption tab

        # flag that helps in the save layers function to determine what was performed
        self.obfuscation_option = 1 

    # open location in windows explorer - https://stackoverflow.com/questions/64402000/how-to-open-a-folder-in-windows-explorer-by-python-script
    # def open_masked_layer_location(self):
    #     loc = self.label_masking_saved.text()
    #     print('masked saved loc: ' + str(loc))       
    #     os.startfile(loc)

    # def open_binned_layer_location(self):
    #     loc = self.label_binning_saved.text()
    #     print('binned saved loc: ' + str(loc))
    #     os.startfile(loc)

    # def open_enc_volume_location(self):
    #     try:
    #         loc = self.label_enc_vol.text() 
    #         #path = os.path.realpath(loc)
    #         ## Get the directory of the file
    #         path = os.path.dirname(loc)
    #         os.startfile(path)
    #     except Exception as e:
    #         print(f"Exception open_enc_volume_location() - {e}")
    #         QMessageBox.information(None, "DEBUG:", 'Error opening enc volume location ') 

    # Opens the saved location of the masked layer specified in the GUI label.
    def open_masked_layer_location(self):        
        try:
            
            # Get the saved location from the label
            loc = self.label_masking_saved.text().strip()

            # If 'label_masking_saved' is empty, use 'label_working_dir' instead
            if not loc:
                loc = self.label_working_dir.text().strip()
                print("Fallback to working directory: " + str(loc))

            # Print the location for debugging purposes
            print('Masked layer saved location: ' + str(loc))

            # Check if the location exists
            if not os.path.exists(loc):
                QMessageBox.warning(None, "Error", f"The specified location does not exist: {loc}")
                print(f"Error: The specified location does not exist: {loc}")
                return
       
            # Attempt to open the location in the file explorer
            os.startfile(loc)

        except FileNotFoundError:
            # Handle the case where the specified path does not exist
            QMessageBox.warning(None, "Error", "The masked layer location does not exist.")
            print("Error: The masked layer location does not exist.")

        except PermissionError:
            # Handle the case where the user does not have permission to access the location
            QMessageBox.warning(None, "Error", "Permission denied to access the masked layer location.")
            print("Error: Permission denied to access the masked layer location.")

        except Exception as e:
            # Handle any other unexpected errors
            QMessageBox.warning(None, "Error", f"An unexpected error occurred: {e}")
            print(f"Error: An unexpected error occurred: {e}")

    # Opens the saved location of the binned layer specified in the GUI label.
    def open_binned_layer_location(self):
        try:
            # Get the saved location from the label
            loc = self.label_binning_saved.text()

            # If 'label_binning_saved' is empty, use 'label_working_dir' instead
            if not loc:
                loc = self.label_working_dir.text().strip()
                print("Fallback to working directory: " + str(loc))

            # Print the location for debugging purposes
            print('Binned layer saved location: ' + str(loc))

            # Check if the location exists
            if not os.path.exists(loc):
                QMessageBox.warning(None, "Error", f"The specified location does not exist: {loc}")
                print(f"Error: The specified location does not exist: {loc}")
                return

            # Attempt to open the location in the file explorer
            os.startfile(loc)

        except FileNotFoundError:
            # Handle the case where the specified path does not exist
            QMessageBox.warning(None, "Error", "The binned layer location does not exist.")
            print("Error: The binned layer location does not exist.")

        except PermissionError:
            # Handle the case where the user does not have permission to access the location
            QMessageBox.warning(None, "Error", "Permission denied to access the binned layer location.")
            print("Error: Permission denied to access the binned layer location.")

        except Exception as e:
            # Handle any other unexpected errors
            QMessageBox.warning(None, "Error", f"An unexpected error occurred: {e}")
            print(f"Error: An unexpected error occurred: {e}")

    # Opens the directory containing the encrypted volume specified in the GUI label.
    def open_enc_volume_location(self):
        try:
            # Get the encrypted volume location from the label
            loc = self.label_enc_vol.text()

            # If 'label_binning_saved' is empty, use 'label_working_dir' instead
            if not loc:
                loc = self.label_working_dir.text().strip()
                print("Fallback to working directory: " + str(loc))

            # Print the location for debugging purposes
            print('Encrypted volume saved location: ' + str(loc))

            # Check if the location exists
            if not os.path.exists(loc):
                QMessageBox.warning(None, "Error", f"The specified location does not exist: {loc}")
                print(f"Error: The specified location does not exist: {loc}")
                return

            # Get the directory path of the file
            path = os.path.dirname(loc)
            # Attempt to open the directory in the file explorer
            os.startfile(path)
        except FileNotFoundError:
            # Handle the case where the specified path does not exist
            QMessageBox.warning(None, "Error", "The encrypted volume location does not exist.")
            print("Error: The encrypted volume location does not exist.")

        except PermissionError:
            # Handle the case where the user does not have permission to access the location
            QMessageBox.warning(None, "Error", "Permission denied to access the encrypted volume location.")
            print("Error: Permission denied to access the encrypted volume location.")

        except Exception as e:
            # Handle any other unexpected errors
            QMessageBox.warning(None, "Error", f"An unexpected error occurred: {e}")
            print(f"Error: An unexpected error occurred: {e}")

    # def open_dec_volume_location(self):
    #     loc = self.label_decrypted_volume.text() 
    #     #path = os.path.realpath(loc)
    #     ## Get the directory of the file
    #     path = os.path.dirname(loc)
    #     os.startfile(path)

    # def open_passphrase_location(self):
    #     loc = self.label_passphrase_loc.text() 
    #     #path = os.path.realpath(loc)
    #     ## Get the directory of the file
    #     path = os.path.dirname(loc)
    #     os.startfile(path)

    # def open_decrypted_folder_location(self):
    #     loc = self.label_decrypted_volume.text() 
    #     #path = os.path.realpath(loc)
    #     ## Get the directory of the file
    #     path = os.path.dirname(loc)
    #     os.startfile(path)

    # Opens the directory containing the decrypted volume location specified in the GUI label.
    def open_dec_volume_location(self):
        try:
            # Get the decrypted volume location from the label
            loc = self.label_decrypted_volume.text()

            # If 'label_enc_volume_saved' is empty, use 'label_working_dir' instead
            if not loc:
                loc = self.label_working_dir.text().strip()
                print("Fallback to working directory: " + str(loc))

            # Print the location for debugging purposes
            print('Decrypted volume saved location: ' + str(loc))

            # Check if the location exists
            if not os.path.exists(loc):
                QMessageBox.warning(None, "Error", f"The specified location does not exist: {loc}")
                print(f"Error: The specified location does not exist: {loc}")
                return

            # Get the directory path of the file
            path = os.path.dirname(loc)
            # Open the directory in the file explorer
            os.startfile(path)

        except FileNotFoundError:
            # Handle the case where the specified path does not exist
            QMessageBox.warning(None, "Error", "The decrypted volume location does not exist.")
            print("Error: The decrypted volume location does not exist.")

        except PermissionError:
            # Handle the case where the user does not have permission to open the directory
            QMessageBox.warning(None, "Error", "Permission denied to access the decrypted volume location.")
            print("Error: Permission denied to access the decrypted volume location.")

        except Exception as e:
            # Handle any other unexpected errors
            QMessageBox.warning(None, "Error", f"An unexpected error occurred: {e}")
            print(f"Error: An unexpected error occurred: {e}")


    ##  Opens the directory containing the passphrase file location specified in the GUI label.  
    def open_passphrase_location(self):        
        try:
            # Get the passphrase file location from the label
            loc = self.label_passphrase_loc.text()

            # If 'label_passphrase_saved' is empty, use 'label_working_dir' instead
            if not loc:
                loc = self.label_working_dir.text().strip()
                print("Fallback to working directory: " + str(loc))

            # Print the location for debugging purposes
            print('Encrypted volume saved location: ' + str(loc))

            # Check if the location exists
            if not os.path.exists(loc):
                QMessageBox.warning(None, "Error", f"The specified location does not exist: {loc}")
                print(f"Error: The specified location does not exist: {loc}")
                return

            # Get the directory path of the file
            print('a ' + str(loc))
            path = os.path.dirname(loc)
            print('a ' + str(path))
            # Open the directory in the file explorer
            os.startfile(path)

        except FileNotFoundError:
            # Handle the case where the specified path does not exist
            QMessageBox.warning(None, "Error", "The passphrase file location does not exist.")
            print("Error: The passphrase file location does not exist.")

        except PermissionError:
            # Handle the case where the user does not have permission to open the directory
            QMessageBox.warning(None, "Error", "Permission denied to access the passphrase file location.")
            print("Error: Permission denied to access the passphrase file location.")

        except Exception as e:
            # Handle any other unexpected errors
            QMessageBox.warning(None, "Error", f"An unexpected error occurred: {e}")
            print(f"Error: An unexpected error occurred: {e}")

    # Opens the directory containing the decrypted folder location specified in the GUI label.
    def open_decrypted_folder_location(self):
        try:
            # Get the decrypted folder location from the label
            loc = self.label_decrypted_volume.text()

            # If 'label_passphrase_saved' is empty, use 'label_working_dir' instead
            if not loc:
                loc = self.label_working_dir.text().strip()
                print("Fallback to working directory: " + str(loc))

            # Print the location for debugging purposes
            print('Encrypted volume saved location: ' + str(loc))

            # Check if the location exists
            if not os.path.exists(loc):
                QMessageBox.warning(None, "Error", f"The specified location does not exist: {loc}")
                print(f"Error: The specified location does not exist: {loc}")
                return

            # Get the directory path of the file
            path = os.path.dirname(loc)
            # Open the directory in the file explorer
            os.startfile(path)

        except FileNotFoundError:
            # Handle the case where the specified path does not exist
            QMessageBox.warning(None, "Error", "The decrypted folder location does not exist.")
            print("Error: The decrypted folder location does not exist.")

        except PermissionError:
            # Handle the case where the user does not have permission to open the directory
            QMessageBox.warning(None, "Error", "Permission denied to access the decrypted folder location.")
            print("Error: Permission denied to access the decrypted folder location.")

        except Exception as e:
            # Handle any other unexpected errors
            QMessageBox.warning(None, "Error", f"An unexpected error occurred: {e}")
            print(f"Error: An unexpected error occurred: {e}")

    # Function to sanitize layer name by replacing special characters with an underscore, even dots
    def sanitize(self, layer_name):
        forbidden_chars = r'<>:."/\|?*'
        return ''.join(c if c not in forbidden_chars else '_' for c in layer_name)

    # Saves each layer in the project
    # This function has been taken from the 'Save All' QGIS Plugin, https://plugins.qgis.org/plugins/SaveAllScript 
    # We are very grateful to the plugin's author. https://github.com/CaptainDang/save_all
    # Refer to the 'run' function https://github.com/CaptainDang/save_all/blob/master/save_all.py     
    # option 1 = masking, 2 = binning
    def request_saveLayers(self): 
        proceed = False
        failedSaves = []

        project = QgsProject.instance()
        layers = project.mapLayers().values()

        print('project ' + str(project))
        print('layers ' + str(layers))

        # Check to make sure all layers have different names
        unique_names = set()
        non_unique_names = []

        name = None
        for layer in project.mapLayers().values():
            name = layer.name()
            if name in unique_names:
                # Layer name is not unique, so add it to the list
                non_unique_names.append(name)
            else:
                # Add the layer name to the set
                unique_names.add(name)

        if name is None:
            QMessageBox.information(None, "Info:", 'No dataset to save.') 
            return

        # Check if non-unique layer names were found
        if len(non_unique_names) > 0:
            # Display a pop-up message with the non-unique layer names
            print("Layer Name Conflict - Two or more layers have the same name, which will lead to unintended effects. Please make sure that all layers have different names and try again. Non-unique layer names: ")
            
        # Check if all layer names are unique
        if len(unique_names) == len(layers):
            # Open a dialog to select the folder
            selected_folder = self.working_directory
            proceed = True

        else:
            print("Not all layer names are unique. Make sure all layers have different names and try again.")

        if proceed:
            # Ask the user to enter a folder name
            #folder_name, ok = QInputDialog.getText(None, "Folder Name",
            #                                        "Enter desired folder name. Use same name (case sensitive) to overwrite previous save:")
            # we dont ask, but we use the 'working directory' set by the user
            new_folder_path = self.working_directory

            # Which level is it for. This wil be used to automatically poplulate the encryption levels 
            counter = 1     
            # Save each layer in the project
            for layer in project.mapLayers().values():
                layer_name = self.sanitize(layer.name())
                layer_file_path = os.path.join(new_folder_path, layer_name)
                
                # not needed as dot added in sanitise function
                # name_without_extension = os.path.splitext(layer.name())[0]
                # if not name_without_extension:
                #     continue  # Skip empty layer names                
                # sanitized_name = sanitize(name_without_extension)
                # print(f"Sanitized Layer Name: {sanitized_name}")

                # Save vector layers
                if layer.type() == QgsMapLayerType.VectorLayer:
                    layerProvider = layer.dataProvider()
                    layerStorage = layerProvider.storageType()                                            
                                                    
                    # Save all masked and binned (vector) layers as GPKG files 
                    output_file = os.path.join(new_folder_path, layer_name + ".gpkg")

                    # set the filepaths by default to prevent user having to choose the file
                    #if counter == 1:
                    #    self.label_osfile_second_level.setText(str(output_file))
                    #elif counter == 2:
                    #    self.label_osfile_third_level.setText(str(output_file))

                    # only save if the file does not currently exist
                    # since this function is called for both: masking and binning, and if we call binning after already masking, 
                    # will have the masked files already saved - so we dont replace those files
                    if not os.path.exists(output_file):
                        parameters = {
                            'LAYERS': [layer],
                            'OUTPUT': layer_file_path + ".gpkg",
                            'OVERWRITE': True,
                            'SAVE_STYLES': True,
                            'SAVE_METADATA': True,
                            'SELECTED_FEATURES_ONLY': False,
                            'EXPORT_RELATED_LAYERS': False}

                        feedback = QgsProcessingFeedback()

                        # Execute the package algorithm
                        try:
                            result = processing.run("native:package", parameters, feedback=feedback)
                            if result['OUTPUT']:
                                pass
                            else:
                                iface.messageBar().pushMessage("Failed: ", "Layer '{}' was not saved.".format(layer.name()), level=2)
                                failedSaves.append(layer_name)
                        except QgsProcessingException as e:
                            iface.messageBar().pushMessage("Error: ", "An error occurred while packaging layer '{}': '{}'".format(layer.name(), str(e)), level=2)

                    # Sets the layer's data source to the newly created path, replaces temp layers with their permanent ones
                    layer.setDataSource(output_file, layer.name(), "ogr")

                    counter = counter + 1
                # We won't have any reason to save a raster file, as coded in the 'Save All' plugin                              

            if len(failedSaves) > 0:
                print("Not all layers were successfully saved. Unsaved Layers: {}")
            else:
                print("Success: All layers saved.")

            # Set the QGIS project file name and the project path and get the project instance
            project_file_name = self.working_directory + ".qgs"
            project_file_path = os.path.join(new_folder_path, project_file_name)

            # Save the project if already in folder, else save the QGIS project file into the folder for the first time
            if os.path.exists(project_file_path):
                #iface.mainWindow().findChild(QAction, 'mActionSaveProject').trigger()
                print("Success: QGIS project file saved successfully.")
            else:
                project.write(project_file_path)
                print("Success: QGIS project file saved successfully for the first time.")

        # masking
        if(self.obfuscation_option == 1 ):
            print('obfuscation_option == 1')
            self.btnSaveMasked.setText('Saved')
            self.label_masking_saved.setStyleSheet("color: #AA336A")
            self.label_masking_saved.setText(str(self.working_directory))
        # binning
        elif(self.obfuscation_option == 2):
            print('obfuscation_option == 2')
            self.btnSaveBinned.setText('Saved')
            self.label_binning_saved.setStyleSheet("color: #AA336A")
            self.label_binning_saved.setText(str(self.working_directory))

        # these two only come after save button pressed
        self.masking_next_pushButton.setEnabled(True)       # enable next button - for encryption
        self.tabWidget_2.setTabEnabled(1, True)             #enable/disable the encryption tab

    def request_encryption(self):

        # clear these
        self.label_enc_vol.setText("")
        self.label_passphrase_loc.setText("")
        #self.label_hash_val.setText("")
        self.label_encryption_time.setText("")

        # check if 'working directory' is set      
        print ('Working Directory Set: ' + str(self.working_directory_set)) 

        if (self.working_directory_set == False):  
            QMessageBox.information(None, "DEBUG:", 'Please set working directory! ')             
            return        
       
        self.btn_encrypt_passphrase.setEnabled(True)

        if not self.label_osfile_first_level.text():
            QMessageBox.information(None, "DEBUG:", 'Please choose OS file for First level!. ')                     
            return

        # ensure passphrase
        if(self.passphrase_generated == False):            
            QMessageBox.information(None, "DEBUG:", 'Please generate passphrase!. ')                      
            return
        else:                       
            self.encryption_decryption = EncryptionDecryption()
            
            print('\t\t----------------------------------------------------------')
            print('\t\tself.passphrase: ' + str(self.passphrase))
            print('\t\tself.levels_to_encrypt: ' + str(self.levels_to_encrypt))
            print('\t\tself.filename1: ' + str(self.filename1))
            print('\t\tself.filename2: ' + str(self.filename2))
            print('\t\tself.filename3: ' + str(self.filename3))
            print('\t\tself.label_passphrase_loc: ' + str(self.label_passphrase_loc))
            print('\t\t----------------------------------------------------------')


            ed = self.encryption_decryption.encryption(self.passphrase, self.levels_to_encrypt, 
                                                    self.filename1, self.filename2, self.filename3, 
                                                    self.label_passphrase_loc, 
                                                    self.label_hash_value, 
                                                    self.label_enc_vol, self.label_final_encrypted_volume_notarise, 
                                                    self.working_directory, self.label_encryption_time,
                                                    self.safeguard_progressBar)
            print('Calling os files')    
            # keep checking each level to finalise the level_to_encrypt 
            
            self.final_volume_filename = self.encryption_decryption.get_final_encrypted_volume_filename()
            self.final_volume_hash_value = self.encryption_decryption.get_final_encrypted_volumes_hash_value()

        self.main_string_to_mint = str(self.final_volume_filename) + "_" + str(self.final_volume_hash_value)
        # enable the next button  for notarisation
        self.encryption_next_pushButton.setEnabled(True)
        # enable notarisation tab
        self.tabWidget_2.setTabEnabled(2, True) #enable/disable the encryption tab

    def request_decryption(self):

        # clear these
        self.label_decrypted_volume.setText("")
        #self.label_passphrase_loc.setText("")
        self.lbl_hash_value.setText("")
        self.label_decryption_time.setText("")

        # check if 'working directory' is set
        self.working_directory_set = self.working_directory_set #self.chkBox_privRating_two_levels.isChecked()  
        print ('Working Directory Set: ' + str(self.working_directory_set)) 

        if(self.working_directory_set == False):    
            ctypes.windll.user32.MessageBoxW(0, "Please set working directory!", "Working Directory", 1)
            #easygui.msgbox("Please generate passphrase!", title="Passphrase")
            return 

        # first check if pasphrase provided       
        print ('passphrase ' + self.passphrase)
        
        if (self.encrypted_file_loaded == False):
            ctypes.windll.user32.MessageBoxW(0, "Please select an encrypted volume", "Select Ecrypted Volume", 1)
            return
               

         # checking if it is checked 
        if (self.rbt_level1.isChecked()): 
            self.decrypt_to_level = 1
        elif (self.rbt_level2.isChecked()): 
            self.decrypt_to_level = 2
        elif (self.rbt_level3.isChecked()): 
            self.decrypt_to_level = 3

        print ('level_to_decrypt ' + str(self.decrypt_to_level))

        self.encryption_decryption = EncryptionDecryption()
        ed = self.encryption_decryption.decryption(self.encrypted_volume_filename, self.decrypt_to_level, 
                                                   self.passphrase, self.working_directory, self.label_decryption_time,
                                                   self.label_decrypted_volume, self.verification_progressBar, self.btnDecrypt,
                                                   self.btn_dec_volume_location, self.tabWidget_3, self.volume_encrypted_level,
                                                   self.to_verify_display)
        print('Called decryption function. self.passphrase : (' + self.passphrase + ') level to decrypt (' + str(self.decrypt_to_level) + ') ')
        
        # show the next button
        self.decrypt_next_pushButton.setEnabled(True)
        
        # enable the display tab 
        self.tabWidget_3.setTabEnabled(2, True) #enable/disable the display tab
        self.clear_show_directory_structure()
    
    # we prepare the directory structure
    def clear_show_directory_structure(self): #, layername, spatial_File):        

        # clear the tree first
        self.treeWidget.clear()

        decryption_working_dir = os.path.join(self.working_directory, "decrypted")
        startpath = decryption_working_dir # self.working_directory  #"D:\\datasets\\"
        tree = self.treeWidget

        # let the tree load again
        self.load_project_structure(startpath, tree)
        self.verification_progressBar.setValue(100)     

    def display(self):
        print('display')

        spatial_File = self.selected_file_from_decrypted_tree
        # layername to be displayed in QGIS
        layername = Path(spatial_File).stem

        try:
            # if we found something
            if(layername is not None):
                layer = QgsVectorLayer(spatial_File, layername, "ogr")
            
                if not layer.isValid():
                    print("\tLayer failed to load C!")
                else:
                    print("\tLayer was loaded successfully B!")
                    self.verification_progressBar.setValue(100)  

                QgsProject.instance().addMapLayer(layer)
                
        
        except ValueError:
            print("Error: ValueError.")

        except Exception as e:
            print(f"Some error during display - {e}")

   
    # https://stackoverflow.com/questions/5144830/how-to-create-folder-view-in-pyqt-inside-main-window
    def load_project_structure(self, startpath, tree):
        """
        Load Project structure tree
        :param startpath: 
        :param tree: 
        :return: 
        """
        #import os
        from PyQt5.QtWidgets import QTreeWidgetItem
        from PyQt5.QtGui import QIcon
         
        #tree.clear()  # have to clear otherwise it appends
        #self.tree.insertTopLevelItems(0, items)
        #tree.clear()        

        for element in os.listdir(startpath):
            path_info = startpath + "/" + element
            parent_itm = QTreeWidgetItem(tree, [os.path.basename(element)])
            if os.path.isdir(path_info):
                self.load_project_structure(path_info, parent_itm)
                parent_itm.setIcon(0, QIcon('assets/folder.ico'))
            else:
                parent_itm.setIcon(0, QIcon('assets/file.ico'))

    # https://github.com/Gordarg/gordarg.github.io/commit/1e39b3befc6cd090881a8c82887c0b09c586ce5a?diff=split
    def getItemFullPath(self, item):
        out = item.text(0)

        if item.parent():
            out = self.getItemFullPath(item.parent()) + "/" + out
        else:
            out =  "../content/" + out
        return out

    def onItemClicked(self, it, col):
        try:            
            file = self.getItemFullPath(it)
            if file != "":
                print (file)  
                # "../content" is automatically added whoch we replace with the working directory
                filepath = os.path.join(self.working_directory, "decrypted")
                file = file.replace("../content", filepath)                                    
                self.selected_file_from_decrypted_tree = file #self.getItemFullPath(it)
                print('self.selected_file_from_decrypted_tree: ' + self.selected_file_from_decrypted_tree)
                
        except Exception as e:
            print(f"Exception on item click {e}")

    # https://gist.github.com/tcrowson/8273931
    def clearQTreeWidget(tree):
        iterator = QTreeWidgetItemIterator(tree, QTreeWidgetItemIterator.All)
        while iterator.value():
            iterator.value().takeChildren()
            iterator += 1
        i = tree.topLevelItemCount()
        while i > -1:
            tree.takeTopLevelItem(i)
            i -= 1

    def request_notarisation(self, env_var):
        self.label_final_encrypted_volume_notarise.setStyleSheet("color: #AA336A")  #dark pink        
        self.notarisation = Notarisation(self.plugin_dir, self.env_var) #, self.working_directory)
        print('Calling minting function')
        nota_success = False # notarisation successfull
        
        try:
            if self.label_hash_value.text() is None or self.label_hash_value.text() == "":
                QMessageBox.information(None, "DEBUG:", 'No Hash Value provided for Notarisation on the Blockchain. ')
            else:
                nota_success = self.notarisation.mint(self.main_string_to_mint) #, self.final_volume_filename, self.final_volume_hash_value) # use global if this variable does not go through       
                transx = self.notarisation.getTransaction()
                print('nota_success: ' + str(nota_success))
        except Exception as e:
            print(f"Exception while notarisation - {e}")
            QMessageBox.information(None, "DEBUG:", 'Notarisation unsucessfull. ') 
                
        if (nota_success == True):             
            # label with sucess information
            self.label_success.setHidden(False)
            # shows 'Success' text only
            self.label_tranx.setHidden(False)
            self.label_tranx.setStyleSheet("color: #AA336A")  #dark pink 
            self.label_tranx.setText("Success")

            self.label_tranx_url.setHidden(False) 
            self.label_tranx_url.setStyleSheet("color: #AA336A")  #dark pink
            self.label_tranx_url.setWordWrap(True)  # making it multi line        
            self.label_tranx_url.setOpenExternalLinks(True)

            href_string = "https://sepolia.etherscan.io/tx/0x" + str(transx)
            caption_string  = href_string
            s = '<a href="%s" >"%s"</a>'
            # https://stackoverflow.com/questions/5420560/how-to-interfere-some-text-in-a-html-link-string-in-python
            link = s % (href_string, caption_string) 
            print('link: ' + str(link))
            self.label_tranx_url.setText(link) #"<a href=\"http://www.qtcentre.org\" />QtCentre</a>")
            #self.label_tranx_url.setText("<a href=\"https://sepolia.etherscan.io/tx/" + str(transx) + " />QtCentre</a>")
                       
            print("https://sepolia.etherscan.io/tx/Ox"+str(transx))
            self.safeguard_progressBar.setValue(100)

            self.save_receipt(self.main_string_to_mint, self.blockchain_address, transx, None, self.final_volume_filename, self.final_volume_hash_value)
        
        else:
            print("Notarisation unsucessfull.")   

    # def save_notarisation_pdf(self, env_var):
    #     # self.label_final_encrypted_volume_notarise.setStyleSheet("color: #AA336A")  #dark pink        
    #     self.notarisation = Notarisation(self.plugin_dir, self.env_var) #, self.working_directory)
    #     # print('Calling minting function')
    #     # nota_success = False # notarisation successfull
        
    #     try:
    #         # if self.label_hash_value.text() is None or self.label_hash_value.text() == "":
    #         #     QMessageBox.information(None, "DEBUG:", 'No Hash Value provided for Notarisation on the Blockchain. ')
    #         # else:
    #         nota_success = self.save_receipt(None, None, None, None, None, None) # use global if this variable does not go through                   
    #         print('Saved PDF file: ')
    #     except Exception as e:
    #         print(f"Exception while notarisation - {e}")
    #         QMessageBox.information(None, "DEBUG:", 'Notarisation unsucessfull. ') 

    def save_receipt(self, minted_string, from_addr, tx_hash, gas_price, full_file_name, hash_value):
        print("Saving Transaction Receipt as PDF...")

        # Create a PDF object
        pdf = FPDF()
        pdf.add_page()

        # Set metadata
        pdf.set_title('Mapsafe Transaction Record')
        pdf.set_subject(full_file_name)
        pdf.set_author('Mapsafe QGIS Plugin')
        pdf.set_creator('Mapsafe QGIS Plugin')

        # Add a logo (replace with your plugin's logo path)
        #logo_path = os.path.join(os.path.dirname(__file__), 'images', 'logo_name.png')
        logo_path = os.path.join(os.path.dirname(__file__), 'logo_name.png')
        if os.path.exists(logo_path):
            pdf.image(logo_path, x=25, y=10, w=50, h=12.5)  # x, y, width, height

        # Add title
        pdf.set_font('Arial', 'B', 16)
        pdf.set_y(40)
        pdf.cell(0, 10, 'Transaction Details!', ln=True)

        # Add date
        date_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        pdf.set_font('Arial', '', 10)
        pdf.cell(0, 10, f'Date: {date_str}', ln=True)

        # Add wallet address
        pdf.cell(0, 10, 'Wallet Account Address:', ln=True)
        pdf.set_text_color(0, 0, 255)
        pdf.cell(0, 10, from_addr, ln=True)
        pdf.set_text_color(0, 0, 0)

        # Add encrypted volume details
        pdf.set_font('Arial', 'B', 12)
        pdf.cell(0, 10, 'Encrypted Volume:', ln=True)
        pdf.set_font('Arial', '', 10)
        pdf.cell(0, 10, 'Details of the generated encrypted volume:', ln=True)

        pdf.cell(0, 10, f'Filename: {full_file_name}', ln=True)
        pdf.set_text_color(0, 0, 255)
        pdf.cell(0, 10, f'Hash Value: {hash_value}', ln=True)
        pdf.set_text_color(0, 0, 0)

        # Add Ethereum blockchain details
        pdf.set_font('Arial', 'B', 12)
        pdf.cell(0, 10, 'Ethereum Blockchain Notarisation Details:', ln=True)


        # Build the transaction URL by prepending the explorer URL to the tx_hash
        etherscan_url = f"https://sepolia.etherscan.io/tx/Ox{tx_hash}"

        # # Add a clickable link to the PDF
        # pdf.set_text_color(0, 0, 255)  # Blue color for link
        # pdf.set_font('Arial', 'U', 10)  # Underline font for link
        # pdf.cell(0, 10, 'View Transaction on Etherscan', ln=True, link=etherscan_url)

        pdf.set_font('Arial', '', 9)
        pdf.cell(0, 10, f'Value: {minted_string}', ln=True)
        pdf.set_text_color(0, 0, 255)
        #pdf.cell(0, 10, f'Minted Address: {tx_hash}', ln=True)
        pdf.cell(0, 10, 'Minted Address', ln=True, link=etherscan_url)
        pdf.set_text_color(0, 0, 0)

        pdf.cell(0, 10, f'Gas Price: {gas_price}', ln=True)

        # Add footer
        pdf.set_y(-30)
        pdf.set_font('Arial', 'I', 10)
        pdf.cell(0, 10, 'MapSafe QGIS Geoprivacy Plugin', ln=True)
        pdf.cell(0, 10, 'https://sharmapn.github.io/MapSafeQGISGeoPrivPlugin/', ln=True)

        # Save the PDF
        #only_date = datetime.now().strftime('%Y-%m-%d')
        # Get the current date and time
        timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
        #output_file = f'MapsafeReceipt_{only_date}.pdf'
        # Create the output filename with date and time
        output_file = f'MapsafeReceipt_{timestamp}.pdf'
        # WORKING_DIR
        #output_path = os.path.join(os.path.expanduser('~'), 'Documents', output_file)
        output_path = os.path.join(self.working_directory, output_file)
        pdf.output(output_path)

        print(f'Receipt saved to: {output_path}')
        #self.label_notarisation_record.labelText = 'output_path'
        #QMessageBox.information(None, "Success", f"Transaction receipt saved as PDF:\n{output_path}")
        # Set the label text as clickable HTML
        self.label_notarisation_record.setText(f'<a href="{output_path}">Transaction receipt saved as PDF</a>')

        # Enable the label to recognize links
        self.label_notarisation_record.setOpenExternalLinks(True)

    def on_value_changed_min(self, val):
        self.minimum_distance = val
        self.label_min.setText( str(self.minimum_distance) )     
        # the default second level min is the double of this value
        self.text_minimum_offset.setPlainText(str(val*2))

    def on_value_changed_max(self, val):        
        self.maximum_distance = val
        self.label_max.setText( str(self.maximum_distance)  )       
        # the default second level max is the double of this value
        self.text_maximum_offset.setPlainText(str(val*2))

    # on hexagonal binning slider value changed
    def on_resolution_value_changed(self, val):
        #hexabinning_resolution         # update global variable
        self.hexabinning_resolution = val
        self.label_resolution.setText( str(self.hexabinning_resolution)  ) 
        
    def request_binning_function(self):

        # clear previous anonymised layers
        # self.clear_anonymised_layers()
        
        # try:
        #     self.hexabinning_folder = "hexabinned_datasets/"        # 'hexabinned_datasets'
        #     self.dataPath = os.path.join(self.working_directory, self.hexabinning_folder) 
        #     print('output ' + str(self.dataPath))  # D:\datasets\hexabinned_datasets

        #     # Check if the directory exists and delete it
        #     if os.path.exists(self.dataPath):
        #         shutil.rmtree(self.dataPath)  # Removes the directory and all its contents
        #         print('Directory deleted: ' + str(self.dataPath))

        # except Exception as e:
        #     print(f'Error delecting folder. {self.dataPath}')
        #     QMessageBox.information(None, "DEBUG:", 'Error delecting folder. ') 

            # # Create the directory
            # os.mkdir(self.dataPath)
            # print('Directory created: ' + str(self.dataPath))    

        print('hexabinning using uber h3')

        # rather than the saved text - everytime masking should allow to save again.
        self.btnSaveBinned.setText('Save')
       
        # clear the saved file directpry
        self.label_binning_saved.setText('')        

        self.two_binning_levels = self.chkBox_binning_two_levels.isChecked()  
        print ('Binning Second level' + str(self.two_binning_levels))

        layers = QgsProject.instance().mapLayers().values()
        layerName = None
        for layer in layers:
            layerName = layer.name()
            print('LayerName: ' + str(layerName))
            break

        if layerName is None:
            QMessageBox.information(None, "Ino:", 'No Layer detected. Please load a dataset.') 
            return

        self.h3_binning = H3Binning()
        ed = self.h3_binning.set_parameters("data/", self.working_directory, "")
        ed = self.h3_binning.binning_function(self.hexabinning_resolution, layerName, self.safeguard_progressBar, self.two_binning_levels )
        print('Calling Binning with resolution: ' + str(self.hexabinning_resolution) )  
        

        self.btn_bin.setText("Bin again")

        # enable the actions that can be perfomed after binning         
        self.btnSaveBinned.setEnabled(True)
        self.btn_saved_binned_layers_loc.setEnabled(True)

        # enable the next button - for encryption
        self.binning_next_pushButton.setEnabled(True)
        # enable encryption tab
        self.tabWidget_2.setTabEnabled(1, True) #enable/disable the encryption tab

        # flag that helps in the save layers function to determine what was performed
        self.obfuscation_option = 2 

    # START BINNING
    # https://github.com/maphew/mhwcode/blob/1ef8338e20ff24ddbe741af225e556b9d81ec416/gis/qgis/h3-grid-from-layer.py    
    def binning_function(self):
        print('Binning function')

        self.debug = False

        ###---------- Edit these variables ----------
        # Min & max h3 resolution levels, from 0 to 15 (global to sub-meter)
        # High resolutions over broad areas can be slow and consume a lot of storage space
        # https://h3geo.org/docs/core-library/restable
        # Resolution 7 is ~2,000m across, 9 is ~320m across, 11 is ~45m (in YT Albers)
        self.min_resolution = 0
        self.max_resolution = 9

        # Output files are {prefix}_{resolution}: Hex_3, Hex_4, ...
        self.out_name_prefix = "Hex"

        self.geographic_coordsys = "EPSG:4617"  # e.g. WGS84, NAD83(CSRS)
        self.output_projection = "EPSG:3579"  # placeholder, not currently used
        # --------------------------------------------

        projectPath = os.path.dirname(QgsProject.instance().fileName())
        self.geo_csrs = QgsCoordinateReferenceSystem(self.geographic_coordsys)
        self.out_csrs = QgsCoordinateReferenceSystem(self.output_projection)

        dataPath = os.path.join(projectPath, "data/")
        if not os.path.exists(dataPath):
            os.mkdir(dataPath)

        #instead of chooser, just use active layer, and selected features within that layer
        self.mylayer = iface.activeLayer()
        if self.mylayer.selectedFeatures():
            params = {'INPUT':self.mylayer, 'OUTPUT':'memory:sel'}
            self.mylayer = processing.run("qgis:saveselectedfeatures", params)["OUTPUT"]
            if self.debug:
                QgsProject.instance().addMapLayer(self.mylayer)

        self.run()

    def log(self, item):
        return QgsMessageLog.logMessage(str(item))


    def proj_to_geo(self, in_layer):
        """Project to geographic coordinate system, in memory.
        H3 needs all coordinates in decimal degrees"""
        params = {
            "INPUT": self.mylayer,
            "TARGET_CRS": self.geographic_coordsys,
            "OUTPUT": "memory:dd_",
        }
        geo_lyr = processing.run("native:reprojectlayer", params)["OUTPUT"]
        if self.debug:
            QgsProject.instance().addMapLayer(geo_lyr)
        return geo_lyr


    def poly_from_extent(self, layer):
        """Return polygon as coordinate list from layer's extent
        Ex:
            [(-142.0, 74.0), (-115.0, 74.0), (-115.0, 54.0), (-142.0, 54.0)]

        Adapted from
        https://gis.stackexchange.com/questions/245811/getting-layer-extent-in-pyqgis
        """
        ext = layer.extent()
        xmin = ext.xMinimum()
        xmax = ext.xMaximum()
        ymin = ext.yMinimum()
        ymax = ext.yMaximum()
        return [(xmin, ymax), (xmax, ymax), (xmax, ymin), (xmin, ymin)]


    def hexes_within_layer_extent(self, layer, level):
        """Return list of HexID within layer's extent
        In: qgis layer object, hex resolution level (0-15)
        Out: ['8412023ffffffff', '84029d5ffffffff', '8413a93ffffffff']
        """
        ext_poly = self.poly_from_extent(layer)
        hex_ids = set(h3.polyfill_polygon(ext_poly, res=level, lnglat_order=True))
        self.log(f"Hex IDs within extent poly: {str(len(hex_ids))}")
        return hex_ids

    def run(self):
        geo_layer = self.proj_to_geo(self.mylayer)


        # For each resolution level fetch geometry of each hex feature and write to shapefile with id
        for res in range(self.min_resolution, self.max_resolution + 1):
            self.log("Resolution: {res}")
            fields = QgsFields()
            fields.append(QgsField("id", QVariant.String))
            shpfile = os.path.join(self.dataPath, f"{self.out_name_prefix}_{res}.shp")
            writer = QgsVectorFileWriter(
                shpfile, "UTF8", fields, QgsWkbTypes.Polygon, driverName="ESRI Shapefile"
            )
            features = []
            for id in set(self.hexes_within_layer_extent(geo_layer, res)):
                f = QgsFeature()
                f.setGeometry(
                    QgsGeometry.fromPolygonXY(
                        [
                            # note reversing back to X,Y
                            [QgsPointXY(c[1], c[0]) for c in h3.h3_to_geo_boundary(id)]
                        ]
                    )
                )
                f.setAttributes([id])
                if self.debug:
                    self.log(f"Hex: {id} " + str(h3.h3_to_geo_boundary(id)))
                features.append(f)
            writer.addFeatures(features)
            del writer
            self.log("Features out: " + str(len(features)))

            processing.run("qgis:definecurrentprojection", {"INPUT": shpfile, "CRS": self.geo_csrs})

            layer = QgsVectorLayer(shpfile, f"{self.out_name_prefix} {res}", "ogr")
            QgsProject.instance().addMapLayer(layer)
    # END BINNING

    # https://stackoverflow.com/questions/26528716/error-loading-layer-shapefile-in-a-standalone-python-qgis-application    
    # def load_function(self):
    #     zipbase = 'D:\\datasets\\kx-site-of-significance-SHP.zip'
    #     shp_name = 'site-of-significance.shp'
    #     vl = QgsVectorLayer(f"{zipbase}/{shp_name}", 'SPC Day 1 Categorical Outlook', 'ogr')
    #     if not vl.isValid():
    #         print ("Layer failed to load B!")
    #     else:
    #         QgsProject.instance().addMapLayer(vl)   

        # https://gis.stackexchange.com/questions/470210/qgis-python-load-shapefile-within-zip-file
        # the following works great
        # shpbase = 'D:\\datasets\\all_clusters_kamloops\\all_clusters_kamloops.shp'
        # vl = QgsVectorLayer(f"{zipbase}", 'all_clusters_kamloops', 'ogr')
        # if not vl.isValid():
        #     print ("Layer failed to load!")
        # else:
        #     QgsProject.instance().addMapLayer(vl)         

    def get_layers(self):        
        print('get layers')    
    
    # get OS files
    # def getOSFile_level1(self):
    #     try:                
    #         filepath = QFileDialog.getOpenFileName()
    #         print('filepath: ' + str(filepath))
    #         #if filepath and os.path.exists(filepath): #(filepath is None or filepath == ""):                
    #         self.filename1 = filepath[0] 
    #         if self.filename1 != "":        # filename found
    #             print('filepath: ' + str(filepath))                
    #             self.label_osfile_first_level.setText( str(self.filename1) ) 
    #             print(self.filename1)
    #             self.levels_to_encrypt = 1 
    #             self.btnEncrypt.setEnabled(True) 
    #             self.btn_osfiles_level2.setEnabled(True) 
    #             # clear these
    #             self.label_enc_vol.setText("")
    #             self.label_passphrase_loc.setText("")
    #             #self.label_hash_val.setText("")
    #             self.label_encryption_time.setText("")
    #         else:                           # no filename found     
    #             self.levels_to_encrypt = 0
    #             self.label_osfile_first_level.setText("Choose File") 
    #             self.btn_osfiles_level2.setEnabled(False) 
    #             self.btnEncrypt.setEnabled(False)
    #             # clear these
    #             self.label_enc_vol.setText("")
    #             self.label_passphrase_loc.setText("")
    #             #self.label_hash_val.setText("")
    #             self.label_encryption_time.setText("")
    #         print("\tXXself.levels_to_encrypt " + str(self.levels_to_encrypt))
    #     except Exception as e:
    #         print(f"GetOSFile_level1 Exception - {e}")
    #         QMessageBox.information(None, "DEBUG:", 'Error Getting OSFile for level 1 ')


    ##  Allows the user to select a file for the first level of encryption.
    ##  Updates the UI accordingly and enables the next steps if a valid file is selected.  
    def getOSFile_level1(self):        
        try:
            # Open a file dialog to allow the user to select a file
            filepath = QFileDialog.getOpenFileName()
            # Print the selected file path for debugging
            print('Filepath selected: ' + str(filepath))
            # Get the first element from the returned tuple (the file path)
            self.filename1 = filepath[0]

            # Check if a valid file was selected
            if self.filename1:
                # Print the selected file path for debugging
                print('Filepath: ' + str(self.filename1))

                # Update the label to show the selected file path
                self.label_osfile_first_level.setText(str(self.filename1))
                # Enable encryption buttons and set the encryption level to 1
                self.levels_to_encrypt = 1
                self.btnEncrypt.setEnabled(True)
                self.btn_osfiles_level2.setEnabled(True)
                # Clear other UI labels related to encryption
                self.label_enc_vol.setText("")
                self.label_passphrase_loc.setText("")
                self.label_encryption_time.setText("")

            else:
                # No file was selected, reset UI elements and disable buttons
                self.levels_to_encrypt = 0
                self.label_osfile_first_level.setText("Choose File")
                self.btnEncrypt.setEnabled(False)
                self.btn_osfiles_level2.setEnabled(False)
                # Clear other UI labels related to encryption
                self.label_enc_vol.setText("")
                self.label_passphrase_loc.setText("")
                self.label_encryption_time.setText("")

            # Print the current encryption level for debugging
            print("\tXX self.levels_to_encrypt: " + str(self.levels_to_encrypt))

        # Handle specific exceptions related to file handling
        except FileNotFoundError:
            # Handle case where the file does not exist
            QMessageBox.warning(None, "Error", "The selected file does not exist.")
            print("Error: The selected file does not exist.")
        except PermissionError:
            # Handle case where the user does not have permission to access the file
            QMessageBox.warning(None, "Error", "Permission denied to access the selected file.")
            print("Error: Permission denied to access the selected file.")
        except Exception as e:
            # Handle any other unexpected exceptions
            QMessageBox.warning(None, "Error", f"An unexpected error occurred: {e}")
            print(f"GetOSFile_level1 Exception - {e}")

    # def getOSFile_level2(self): 
    #     try:
    #         #self.encrypt_layers = False             
    #         filepath = QFileDialog.getOpenFileName()
    #         self.filename2 = filepath[0] 
    #         if self.filename2 != "":
    #             self.label_osfile_second_level.setText( str(self.filename2) )
    #             print(self.filename2)
    #             self.levels_to_encrypt = 2
    #             self.btn_osfiles_level3.setEnabled(True)
    #             # clear these
    #             self.label_enc_vol.setText("")
    #             self.label_passphrase_loc.setText("")
    #             #self.label_hash_val.setText("")
    #             self.label_encryption_time.setText("")
    #         else:
    #             self.levels_to_encrypt = 1 
    #             self.label_osfile_second_level.setText("Choose File")
    #             self.btn_osfiles_level3.setEnabled(False) 
    #             # clear these
    #             self.label_enc_vol.setText("")
    #             self.label_passphrase_loc.setText("")
    #             #self.label_hash_val.setText("")
    #             self.label_encryption_time.setText("")
    #         print("\t self.levels_to_encrypt " + str(self.levels_to_encrypt)) 
    #     except Exception as e:
    #         print(f"GetOSFile_level2 Exception - {e}")  
    #         QMessageBox.information(None, "DEBUG:", 'Error Getting OSFile for level 2 ')



    #  Allows the user to select a GIS file for the second level of encryption.
    #  Updates the UI accordingly and enables the next steps if a valid file is selected.
    #  Only GIS file types are allowed in the selection.
    def getOSFile_level2(self):
        try:            
            # Open a file dialog to allow the user to select a file
            filepath = QFileDialog.getOpenFileName()
            
            # Open a file dialog to allow the user to select a GIS file            
            # filepath = QFileDialog.getOpenFileName(
            #     filter="GIS Files (*.shp *.geojson *.gpx *.kml *.kmz *.tif *.tiff *.asc *.grd *.vrt *.hdf *.nc *.csv *.dbf *.gpkg *.img *.bil *.adf *.mif *.tab *.prj *.dxf *.dwg *.lyr *.qgs *.qgz *.sdat *.ecw *.jp2);;All Files (*)"
            # )

            # Get the first element from the returned tuple (the file path)
            self.filename2 = filepath[0]

            # Check if a valid file was selected
            if self.filename2:
                # Update the label to show the selected file path
                self.label_osfile_second_level.setText(str(self.filename2))
                # Print the selected file path for debugging
                print('Second level file selected: ' + str(self.filename2))
                # Set the encryption level to 2 and enable the button for the next level
                self.levels_to_encrypt = 2
                self.btn_osfiles_level3.setEnabled(True)
                # Clear other UI labels related to encryption
                self.label_enc_vol.setText("")
                self.label_passphrase_loc.setText("")
                self.label_encryption_time.setText("")

            else:
                # No file was selected, reset UI elements and disable the next button
                self.levels_to_encrypt = 1
                self.label_osfile_second_level.setText("Choose File")
                self.btn_osfiles_level3.setEnabled(False)
                # Clear other UI labels related to encryption
                self.label_enc_vol.setText("")
                self.label_passphrase_loc.setText("")
                self.label_encryption_time.setText("")

            # Print the current encryption level for debugging
            print("\t self.levels_to_encrypt: " + str(self.levels_to_encrypt))

        # Handle specific exceptions related to file handling
        except FileNotFoundError:
            # Handle case where the file does not exist
            QMessageBox.warning(None, "Error", "The selected file does not exist.")
            print("Error: The selected file does not exist.")
        except PermissionError:
            # Handle case where the user does not have permission to access the file
            QMessageBox.warning(None, "Error", "Permission denied to access the selected file.")
            print("Error: Permission denied to access the selected file.")
        except Exception as e:
            # Handle any other unexpected exceptions
            QMessageBox.warning(None, "Error", f"An unexpected error occurred: {e}")
            print(f"GetOSFile_level2 Exception - {e}")

    # def getOSFile_level3(self):
    #     try:
    #         filepath = QFileDialog.getOpenFileName()
    #         self.filename3 = filepath[0] 
    #         if self.filename3 != "":
    #             self.label_osfile_third_level.setText( str(self.filename3) )
    #             print(self.filename3)
    #             self.levels_to_encrypt = 3 
    #             # clear these
    #             self.label_enc_vol.setText("")
    #             self.label_passphrase_loc.setText("")
    #             #self.label_hash_val.setText("")
    #             self.label_encryption_time.setText("")
    #         else:
    #             self.levels_to_encrypt = 2
    #             self.label_osfile_third_level.setText("Choose File") 
    #             # clear these
    #             self.label_enc_vol.setText("")
    #             self.label_passphrase_loc.setText("")
    #             #self.label_hash_val.setText("")
    #             self.label_encryption_time.setText("")
    #         print("\tXXself.levels_to_encrypt " + str(self.levels_to_encrypt))      
    #         #  IF USER DECIDES TO CHOOSE OS FILES , WE HAVE TO EMPTY THE LAYER COMBO BOX FOR EACH 
    #     except Exception as e:
    #         print(f"GetOSFile_level3 Exception - {e}") 
    #         QMessageBox.information(None, "DEBUG:", 'Error Getting OSFile for level 3 ')


    # Allows the user to select a GIS file for the third level of encryption.
    # Updates the UI accordingly and manages encryption levels based on the file selection.
    # Only GIS file types are allowed in the selection.
    def getOSFile_level3(self):
        try:
            # Open a file dialog to allow the user to select a GIS file
            filepath = QFileDialog.getOpenFileName()
            # filter="GIS Files (*.shp *.geojson *.gpx *.kml *.kmz *.tif *.tiff *.asc *.grd *.vrt *.hdf *.nc *.csv *.dbf *.gpkg *.img *.bil *.adf *.mif *.tab *.prj *.dxf *.dwg *.lyr *.qgs *.qgz *.sdat *.ecw *.jp2);;All Files (*)"
            
            # Get the first element from the returned tuple (the file path)
            self.filename3 = filepath[0]
            # Check if a valid file was selected
            if self.filename3:
                # Update the label to show the selected file path
                self.label_osfile_third_level.setText(str(self.filename3))
                # Print the selected file path for debugging
                print('Third level file selected: ' + str(self.filename3))
                # Set the encryption level to 3
                self.levels_to_encrypt = 3
                # Clear other UI labels related to encryption
                self.label_enc_vol.setText("")
                self.label_passphrase_loc.setText("")
                self.label_encryption_time.setText("")
            else:
                # No file was selected, reset UI elements and adjust encryption level
                self.levels_to_encrypt = 2
                self.label_osfile_third_level.setText("Choose File")
                # Clear other UI labels related to encryption
                self.label_enc_vol.setText("")
                self.label_passphrase_loc.setText("")
                self.label_encryption_time.setText("")
            # Print the current encryption level for debugging
            print("\tXX self.levels_to_encrypt: " + str(self.levels_to_encrypt))

        # Handle specific exceptions related to file handling
        except FileNotFoundError:
            # Handle case where the file does not exist
            QMessageBox.warning(None, "Error", "The selected file does not exist.")
            print("Error: The selected file does not exist.")
        except PermissionError:
            # Handle case where the user does not have permission to access the file
            QMessageBox.warning(None, "Error", "Permission denied to access the selected file.")
            print("Error: Permission denied to access the selected file.")
        except Exception as e:
            # Handle any other unexpected exceptions
            QMessageBox.warning(None, "Error", f"An unexpected error occurred: {e}")
            print(f"GetOSFile_level3 Exception - {e}")

    # def getOSFile_to_notarise(self):
    #     try:
    #         filepath = QFileDialog.getOpenFileName()
    #         self.filename = filepath[0] 
    #         if self.filename != "":
    #             print('filepath ' + str(filepath))                
    #             print('self.filename ' + self.filename)
    #             filename_name = Path(self.filename).name
    #             print ('filename_name to notarise ' +str(filename_name))
    #             self.label_final_encrypted_volume_notarise.setText( str(filename_name) ) # just the filename
    #             self.label_osfile_to_notarise.setText(str(self.filename)) # full filepath

    #             self.encrypted_volume_filename = filepath
            
    #             self.encryption_decryption = EncryptionDecryption()
    #             hash_val = self.encryption_decryption.compute_hash_encrypted(self.filename, self.label_hash_value) # hash
    #             self.main_string_to_mint = str(filename_name) + "_" + str(hash_val)
    #             self.label_final_encrypted_volume_notarise.setStyleSheet("color: #AA336A") 
    #             self.lbl_hash_value.setStyleSheet("color: #AA336A")  #dark pink
    #             self.lbl_hash_value.setText( hash_val )
    #         else:
    #             self.label_final_encrypted_volume_notarise.setText("") # just the filename
    #             self.label_osfile_to_notarise.setText( "" )
            
    #     except Exception as e:
    #         print(f"GetOSFile_level3 Exception - {e}")
    #         QMessageBox.information(None, "DEBUG:", 'Error Getting OSFile to notarise ')
        
    # Allows the user to select a file to be notarized.
    # Updates the UI with the selected file's details and computes its hash value for notarization.
    # Handles all file types and includes comprehensive exception handling.
    def getOSFile_to_notarise(self):
        try:
            # Open a file dialog to allow the user to select any file
            filepath = QFileDialog.getOpenFileName()
            # Get the first element from the returned tuple (the file path)
            self.filename = filepath[0]

            # Check if a valid file was selected
            if self.filename:
                # Print the selected file path for debugging
                print('Filepath selected: ' + str(filepath))
                print('Self.filename: ' + self.filename)
                # Extract and print the filename (without the full path) for display
                filename_name = Path(self.filename).name
                print('Filename to notarize: ' + str(filename_name))
                # Update UI labels with the filename and full file path
                self.label_final_encrypted_volume_notarise.setText(str(filename_name))  # Just the filename
                self.label_osfile_to_notarise.setText(str(self.filename))  # Full file path
                # Store the file path for notarization
                self.encrypted_volume_filename = filepath
                # Initialize the EncryptionDecryption class
                self.encryption_decryption = EncryptionDecryption()
                # Compute the hash value of the selected file and update the UI
                hash_val = self.encryption_decryption.compute_hash_encrypted(self.filename, self.label_hash_value)
                self.main_string_to_mint = str(filename_name) + "_" + str(hash_val)
                # Update the UI with the computed hash value
                self.label_final_encrypted_volume_notarise.setStyleSheet("color: #AA336A")  # Dark pink color for filename
                self.lbl_hash_value.setStyleSheet("color: #AA336A")  # Dark pink color for hash value
                self.lbl_hash_value.setText(hash_val)
            else:
                # If no file was selected, reset the UI labels
                self.label_final_encrypted_volume_notarise.setText("")  # Just the filename
                self.label_osfile_to_notarise.setText("")

        # Handle specific exceptions related to file handling
        except FileNotFoundError:
            # Handle case where the file does not exist
            QMessageBox.warning(None, "Error", "The selected file does not exist.")
            print("Error: The selected file does not exist.")
        except PermissionError:
            # Handle case where the user does not have permission to access the file
            QMessageBox.warning(None, "Error", "Permission denied to access the selected file.")
            print("Error: Permission denied to access the selected file.")
        except Exception as e:
            # Handle any other unexpected exceptions
            QMessageBox.warning(None, "Error", f"An unexpected error occurred: {e}")
            print(f"GetOSFile_to_notarise Exception - {e}")

    # def get_encrypted_volume(self):
    #     try:
    #         filepath = QFileDialog.getOpenFileName()
    #         print('filepath[0] ' + str(filepath[0]))
    #         self.encrypted_volume_filename = filepath[0] 

    #         if self.encrypted_volume_filename != "":
    #             self.encrypted_file_loaded = True
    #             self.label_encrypted_volume.setStyleSheet("color: #AA336A") 
    #             self.label_encrypted_volume.setText( str(self.encrypted_volume_filename) ) 
    #             print('self.encrypted_volume_filename: ' + self.encrypted_volume_filename)
    #             # show the filename only
    #             # https://www.geeksforgeeks.org/python-program-to-get-the-file-name-from-the-file-path/
    #             print('os.path.basename(self.encrypted_volume_filename ' + os.path.basename(self.encrypted_volume_filename).split('/')[-1])
            
    #             # set the valid 'level' options that can be decrypted
    #             # get the filename
    #             file_name = os.path.basename(self.encrypted_volume_filename)  
    #             print('Verification of levels in encrypted file. Filename : ' + file_name)
    #             if(".enc1") in file_name:   # enable just one option
    #                 self.rbt_level1.setEnabled(True) 
    #                 self.volume_encrypted_level = 1
    #             elif(".enc2") in file_name: # enable two options
    #                 self.rbt_level1.setEnabled(True)  
    #                 self.rbt_level2.setEnabled(True)  
    #                 self.volume_encrypted_level = 2
    #             elif(".enc3") in file_name: # enable all three options   
    #                 self.rbt_level1.setEnabled(True)  
    #                 self.rbt_level2.setEnabled(True)
    #                 self.rbt_level3.setEnabled(True)  
    #                 self.volume_encrypted_level = 3   
    #             else:
    #                 self.btnDecrypt.setEnabled(False)   # we dont allow the user to Decrypt           
    #             # check if it has '_1'
    #             # then enable only level 1 radio button 

    #             self.encryption_decryption = EncryptionDecryption()
    #             #ed = self.encryption_decryption.decryption(1, self.passphrase, self.filename1, self.filename2, self.filename3)
    #             hash_val = self.encryption_decryption.compute_hash_encrypted(self.encrypted_volume_filename, self.label_hash_value) # hash
    #             self.lbl_hash_value.setStyleSheet("color: #AA336A")
    #             self.lbl_hash_value.setText( hash_val )

    #             # enable the next - decryption tab and show the next button
    #             self.tabWidget_3.setTabEnabled(1, True) #enable/disable the decryption tab
    #             self.verify_next_pushButton.setEnabled(True)
    #             # update the progress bar
    #             self.verification_progressBar.setValue(33)  
    #         else:
    #             self.encrypted_volume_filename = ""
        
    #     except Exception as e:
    #         print(f"Getting encrypted volume Exception - {e}") 
    #         QMessageBox.information(None, "DEBUG:", 'Error Getting encrypted volume ')


# Allows the user to select an encrypted volume file.
# Updates the UI with the file's details, verifies the encryption level based on the filename,
# computes the hash value of the file, and enables the decryption options accordingly.
    def get_encrypted_volume(self):
        try:
            # Open a file dialog to allow the user to select any file
            filepath = QFileDialog.getOpenFileName()
            # Get the first element from the returned tuple (the file path)
            self.encrypted_volume_filename = filepath[0]
            # Check if a valid file was selected
            if self.encrypted_volume_filename:
                # Mark that an encrypted file has been loaded
                self.encrypted_file_loaded = True
                # Update the UI to show the selected file path with styling
                self.label_encrypted_volume.setStyleSheet("color: #AA336A")
                self.label_encrypted_volume.setText(str(self.encrypted_volume_filename))
                print('Encrypted volume file: ' + self.encrypted_volume_filename)
                # Extract and print just the filename (without the full path) for debugging
                file_name = os.path.basename(self.encrypted_volume_filename)
                print('Filename from path: ' + file_name)
                # Determine the encryption level based on the file's extension
                print('Verifying encryption level based on filename...')
                if ".enc1" in file_name:
                    self.rbt_level1.setEnabled(True)
                    self.volume_encrypted_level = 1
                elif ".enc2" in file_name:
                    self.rbt_level1.setEnabled(True)
                    self.rbt_level2.setEnabled(True)
                    self.volume_encrypted_level = 2
                elif ".enc3" in file_name:
                    self.rbt_level1.setEnabled(True)
                    self.rbt_level2.setEnabled(True)
                    self.rbt_level3.setEnabled(True)
                    self.volume_encrypted_level = 3
                else:
                    # If no valid encryption level is detected, disable the decrypt button
                    self.btnDecrypt.setEnabled(False)
                    print("No valid encryption level detected in the filename.")

                # Initialize the EncryptionDecryption class
                self.encryption_decryption = EncryptionDecryption()
                # Compute the hash value of the encrypted file and update the UI
                hash_val = self.encryption_decryption.compute_hash_encrypted(self.encrypted_volume_filename, self.label_hash_value)
                self.lbl_hash_value.setStyleSheet("color: #AA336A")
                self.lbl_hash_value.setText(hash_val)
                # Enable the decryption tab and the next button
                self.tabWidget_3.setTabEnabled(1, True)
                self.verify_next_pushButton.setEnabled(True)
                # Update the progress bar to show verification progress
                self.verification_progressBar.setValue(33)

            else:
                # If no file was selected, reset the encrypted volume filename
                self.encrypted_volume_filename = ""

        # Handle specific exceptions related to file handling
        except FileNotFoundError:
            QMessageBox.warning(None, "Error", "The selected file does not exist.")
            print("Error: The selected file does not exist.")
        except PermissionError:
            QMessageBox.warning(None, "Error", "Permission denied to access the selected file.")
            print("Error: Permission denied to access the selected file.")
        except Exception as e:
            # Handle any other unexpected exceptions
            QMessageBox.warning(None, "Error", f"An unexpected error occurred: {e}")
            print(f"Getting encrypted volume Exception - {e}")
