import json
import os
import traceback
from pathlib import Path
from typing import Tuple
from xml.etree.ElementTree import Element

from PyQt5.QtCore import QItemSelectionModel
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtWidgets import QWizardPage, QCheckBox, QListView, QMessageBox, QWizard, QTreeWidgetItem, QTreeWidget
import xml.etree.ElementTree as ET

from qgis.PyQt.Qt import Qt
from qgis._core import QgsApplication, Qgis, QgsProject
from ....configuration.crtdrd_layers_entry_data import SourceType
from ....configuration.crtdrd_layers_interpreter import CrtdrdLayersInterpreter
from ....configuration.xml_reader import XMLReader
from ....constants import QGIS_PLUGIN_NAME, CARTODRUID_CONFIG_NAME, QGIS_SCOPE_NAME, QGIS_CONFIG_KEY, \
    QGIS_PROJECT_CONFIG_KEY
from ....i18n import tr
from ....localsync.project.sync_mapper_reader import SyncMapperData
from ....tasks.copy_folders_from_device import CopyFoldersFromDevice
from ....tasks.load_files import LoadFiles
from ....tasks.read_config_file_carto import ReadConfigFileCarto


class FileSelector(QWizardPage):

    """
    Manages the 2rd page of the Cartodruid Project Wizard. Let the user select what files will be added to the synchronization, changing the configuration of the
    plugin and making a download of the files before going to the next page.
    :ivar s_task: stores the download Cartodruid configuration task.
    :vartype s_task: QTask
    :ivar d_task: stores the download files from device task.
    :vartype s_task: QTask
    :ivar carto_conf_pc_dir: path where the Cartodruid configuration will be stored in the pc.
    :vartype carto_conf_pc_dir: str
    :ivar _save_and_validate: indicates whether the download process has finished.
    :vartype _save_and_validate: bool
    :ivar data_list_full:
    """


    def __init__(self, config_box: QCheckBox, image_box: QCheckBox, data_box: QCheckBox, data_tree: QTreeWidget, name_mapper_page: QCheckBox):

        """
        Constructor.
        :param config_box: reference to the checkbox used to select the configuration files.
        :param image_box: reference to the checkbox used to select the image files.
        :param data_box: reference to the checkbox used to select the data files.
        :param data_tree: reference to the list view where the data files will be listed to select them.
        """

        super().__init__()
        self.config_box = config_box
        self.image_box = image_box
        self.data_box = data_box
        self.data_tree = data_tree
        self.config_box.stateChanged.connect(self.completeChanged.emit)
        self.image_box.stateChanged.connect(self.completeChanged.emit)
        self.data_box.stateChanged.connect(self.completeChanged.emit)
        self.data_box.stateChanged.connect(self.change_data_tree_state)
        self.name_mapper_page = name_mapper_page
        self.s_task = None
        self.d_task = None
        self.c_task = None
        self.carto_conf_pc_dir = ""
        self.data_tree.setEnabled(False)
        self._save_and_validate = False
        self.data_list_full = []


    def change_data_tree_state(self):
        """
        Changes the enable state of the data_tree depending on data_box.
        """
        self.data_tree.setEnabled(self.data_box.isChecked())


    def initial_configuration(self):
        """
        Set the initial state for the checkBoxes.
        """
        self.config_box.setEnabled(True)
        self.image_box.setEnabled(True)
        self.data_box.setEnabled(True)
        self.name_mapper_page.setEnabled(True)

        if self.wizard().new_project:
            self.config_box.setChecked(True)
            self.image_box.setChecked(True)
            self.data_box.setChecked(True)
            self.name_mapper_page.setChecked(False)
        else:
            self.config_box.setChecked(self.wizard().current_options.config_box_selected)
            self.image_box.setChecked(self.wizard().current_options.image_box_selected)
            self.data_box.setChecked(self.wizard().current_options.data_box_selected)
            self.name_mapper_page.setChecked(self.wizard().current_options.name_mapper_page_selected)



    def nextId(self):
        """Changes next page to skip page 2 depending on whether name_mapper_page is selected or not."""
        if self.name_mapper_page.isChecked():
            return self.wizard().id_pages_in_order[2]
        else:
            return self.wizard().id_pages_in_order[3]


    def initializePage(self):
        """
            Initialize the state of the checkboxes and launches the download cartodruid configuration task.
            Managed by QGIS, it is launched when the page is loaded.
        """
        self.initial_configuration()
        self.carto_conf_pc_dir = os.path.join(
            QgsApplication.qgisSettingsDirPath(),
            QGIS_PLUGIN_NAME,
            "conf",
            self.wizard().current_options.get_project_selected().project_name
        )
        os.makedirs(self.carto_conf_pc_dir, exist_ok=True)
        if not self.s_task:
            self.s_task = ReadConfigFileCarto(self.wizard().s_eng, self.wizard().current_options.get_project_selected(),
                                              self.wizard().device, self.carto_conf_pc_dir, self.wizard().s_listener)
            self.wizard().logger.info("Looking for data files in cartodruid project...")
            self.s_task.taskCompleted.connect(self._downloaded_conf_completed)
            self.s_task.taskTerminated.connect(self._downloaded_conf_terminated)
            QgsApplication.taskManager().addTask(self.s_task)
            self.wizard().progress2.show()


    def _get_or_create_category(self, db_url:str) -> QTreeWidgetItem:
        if "/" in db_url or "\\" in db_url:
            if "projects" in db_url:
                path_dburl = Path(db_url)
                parts = path_dburl.parts
                idx = parts.index("projects")
                if len(parts) > idx + 1:
                    project_name = parts[idx + 1]
                    parent_name = tr("Data files from {project_name}").format(project_name=project_name)
                    parent_item = self._search_or_create_parent_tree_item(parent_name, 1)
                    return parent_item
        else:
            parent_name = tr("Project files")
            parent_item = self._search_or_create_parent_tree_item(parent_name, 0)
            return parent_item
        parent_name = tr("Others data files")
        parent_item = self._search_or_create_parent_tree_item(parent_name, 2)
        return parent_item


    def _search_or_create_parent_tree_item(self, search_string:str, order:int) -> QTreeWidgetItem:
        items = self.data_tree.findItems(
            search_string,
            Qt.MatchExactly | Qt.MatchRecursive,
            0
        )
        if items:
            return items[0]
        else:
            new_parent_item = QTreeWidgetItem()
            new_parent_item.setText(0, search_string)
            new_parent_item.setFlags(new_parent_item.flags() | Qt.ItemIsUserCheckable)
            new_parent_item.setCheckState(0, Qt.Unchecked)
            if order != 2:
                self.data_tree.insertTopLevelItem(order,new_parent_item)
            else:
                self.data_tree.insertTopLevelItem(self.data_tree.topLevelItemCount(),new_parent_item)

            return new_parent_item



    def _downloaded_conf_completed(self):

        """
            This method is executed after a successful end of the download cartodruid configuration task.
            Populates the data_tree list and select the files selected on a previous execution of the wizard for this Cartodruid project.
        """

        if self.s_task:
            self.data_list_full = []
            self.wizard().xml_interpreter.project_xml_path = Path(self.carto_conf_pc_dir).as_posix()
            self.wizard().xml_interpreter.read_config()
            self.data_tree.clear()
            for entry in self.wizard().xml_interpreter.entries:
                if entry.source_type == SourceType.SpatiaLiteServiceDescriptor:
                    dburl = entry.source.db_url
                    if dburl is not None and dburl not in self.data_list_full:
                        tree_parent = self._get_or_create_category(dburl)
                        self.data_list_full.append(dburl)
                        name = Path(dburl).name
                        item = QTreeWidgetItem()
                        item.setText(0, name)
                        item.setData(0,Qt.UserRole, dburl)
                        item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
                        item.setCheckState(0, Qt.Unchecked)
                        tree_parent.addChild(item)
                        if name not in self.wizard().data_find_in_xml:
                            name_without_extension = name.split(".")[0]
                            self.wizard().data_find_in_xml.append(name_without_extension)
            if self.wizard().new_project:
                self.select_default_selection()
            else:
                self.select_last_selection()
            self.completeChanged.emit()
            for i in range(self.data_tree.topLevelItemCount()):
                self.data_tree.topLevelItem(i).setExpanded(True)
            self.data_tree.itemChanged.connect(self.on_item_changed)
            self.wizard().logger.info("Data files found!")
            self.wizard().s_listener.create_or_update_message_bar(tr("Read CartoDruid configuration finished."), Qgis.MessageLevel.Success,
                                                         time = 5000, clear_messages = False)
        self.wizard().progress2.hide()
        self.s_task = None


    def select_default_selection(self):
        self.data_tree.blockSignals(True)
        try:
            if self.data_tree.topLevelItemCount() > 0:
                top_item = self.data_tree.topLevelItem(0)
                top_item.setCheckState(0, Qt.Checked)
                for i in range(top_item.childCount()):
                    child_item = top_item.child(i)
                    child_item.setCheckState(0, Qt.Checked)
        finally:
            self.data_tree.blockSignals(False)

    def select_last_selection(self):
        for i in range(self.data_tree.topLevelItemCount()):
            parent = self.data_tree.topLevelItem(i)
            child_checked = False
            for j in range(parent.childCount()):
                child = parent.child(j)
                if child:
                    if child.data(0,Qt.UserRole) in self.wizard().current_options.data_list_selected_files:
                        child_checked = True
                        child.setCheckState(0, Qt.Checked)
                    else:
                        child.setCheckState(0, Qt.Unchecked)
            if child_checked:
                parent.setCheckState(0, Qt.Checked)
            else:
                parent.setCheckState(0, Qt.Unchecked)


    def on_item_changed(self, current:QTreeWidgetItem, column:int):
        self.data_tree.blockSignals(True)
        try:
            if not current.parent():
                for i in range(current.childCount()):
                    child = current.child(i)
                    child.setCheckState(0, current.checkState(0))
            else:
                parent = current.parent()
                if current.checkState(0) == Qt.Checked:
                    parent.setCheckState(0, Qt.Checked)
                else:
                    child_checked = False
                    for i in range(parent.childCount()):
                        child = parent.child(i)
                        if child.checkState(0) == Qt.Checked:
                            child_checked = True
                            break
                    if child_checked:
                        parent.setCheckState(0, Qt.Checked)
                    else:
                        parent.setCheckState(0, Qt.Unchecked)
        finally:
            self.data_tree.blockSignals(False)
            self.completeChanged.emit()




    def _downloaded_conf_terminated(self):
        """
            This method is executed after a bad end of the download cartodruid configuration task.
            Shows the error on the logger.
        """
        self.wizard().logger.error("Something went wrong while downloading CartoDruid config file...")
        self.wizard().logger.error(traceback.format_exc())
        self.s_task = None
        self.wizard().progress2.hide()



    def is_string_a_path(self, string_path: str) -> bool:
        """
            Check if string_path is a path with folders or not.
            :param string_path: path that will be checked.
            :return: boolean indicating if the given string is a path with folders or not.
        """
        return "/" in string_path or "\\" in string_path


    def validatePage(self):
        """
            Create the new plugin configuration using the selected checkboxes and data from the data_tree. Launch a task
            to do the first download of the files from the device. Also saves the current state of the checkboxes and the selected files
            in the ProjectWizardData for future interactions.
        """

        if self._save_and_validate:
            self.wizard().current_options.config_box_selected = self.config_box.isChecked()
            self.wizard().current_options.image_box_selected = self.image_box.isChecked()
            self.wizard().current_options.data_box_selected = self.data_box.isChecked()
            self.wizard().current_options.name_mapper_page_selected = self.name_mapper_page.isChecked()
            if self.data_box.isChecked():
                self.wizard().current_options.data_list_selected_files = []
                for i in range(self.data_tree.topLevelItemCount()):
                    parent = self.data_tree.topLevelItem(i)
                    for j in range(parent.childCount()):
                        item = parent.child(j)
                        if item.checkState(0) == Qt.Checked:
                            self.wizard().current_options.data_list_selected_files.append(item.data(0, Qt.UserRole))
            else:
                self.wizard().current_options.data_list_selected_files = []
            self.wizard().current_options.data_list_full = self.data_list_full
            self.wizard().save_carto_project_selections()
            self._save_and_validate = False
            return True
        if self.wizard().current_options and (self.config_box.isChecked() or self.image_box.isChecked() or self.data_box.isChecked()):
            includes = []
            new_configs = {}
            if self.data_tree.topLevelItemCount():
                if self.data_box.isChecked():
                    selected_items = []
                    number_of_items = 0
                    for i in range(self.data_tree.topLevelItemCount()):
                        parent = self.data_tree.topLevelItem(i)
                        for j in range(parent.childCount()):
                            number_of_items += 1
                            if parent.child(j).checkState(0) == Qt.Checked:
                                selected_items.append(parent.child(j))
                    if selected_items != []:
                        full = len(selected_items) == number_of_items
                        if full:
                            includes.append("*/data/*")
                        for item in selected_items:
                            data = item.data(0,Qt.UserRole)
                            is_path = self.is_string_a_path(data)
                            if is_path:
                                filename = item.text(0)
                                folder = Path(data).parent.as_posix()
                                new_configs[filename] = folder
                            if not full and not is_path:
                                includes.append("*/data/" + item.text(0))
                    else:
                        QMessageBox.warning(self.wizard(), tr("No data selected"), tr("Need to select what data files you want to copy from the list to continue."))
                        return False
            else:
                if self.data_box.isChecked():
                    includes.append("*/data/*")

            if self.config_box.isChecked():
                includes.append("*/config/*")
                includes.append("*/values/*")
            if self.image_box.isChecked():
                includes.append("*/pictures/*/*")

            qgis_p_instance = QgsProject.instance()
            path_to_project = qgis_p_instance.fileName()
            if path_to_project:
                source = Path(qgis_p_instance.fileName()).parent
                source = source / "cartodruid"
                source = source.as_posix()
                os.makedirs(source, exist_ok=True)
                source = "./cartodruid"
                new_sync_data = SyncMapperData(source, self.wizard().current_options.get_project_selected().path.as_posix(), includes, [], True)
                json_config = new_sync_data.to_dict()
                copy_folders = [True]
                config = [json_config]
                if new_configs:
                    for _ in new_configs:
                        copy_folders.append(False)
                    config = self.create_extra_configs(new_configs, config, source)
                self.wizard().c_manager.save_config(config)
                self.wizard().logger.info("New configuration created!")
                if self.name_mapper_page.isChecked():
                    self._save_and_validate = True
                    return self.validatePage()
                else:
                    self.launch_first_copy_process(copy_folders)
                return False
            else:
                QMessageBox.critical(self.wizard(), tr("Configuration not created"),
                                     tr("An active project must exist to be able to create the configuration. Open a project or create a new one to complete this step."))
                return True
        return False


    def create_extra_configs(self, new_configs:dict, config:list, source:str)->list:
        """
            Create extra configurations for those files found in the crtdrdLayers.xml that have an absolute path.
            :param new_configs: dictionary with where the key is the file_name of the absolute path and the value is the rest of the path.
            :param config: current configuration that is being created.
            :param source: path to the source folder.
            :return: the config value with a new extra configuration.
        """
        source_path = source + "data" if source.endswith("/") or source.endswith("\\") else source + "/data"
        for file_name, folder in new_configs.items():
            new_sync_data = SyncMapperData(source_path, folder, [file_name], [], False)
            config.append(new_sync_data.to_dict())
        return config

    def set_enable_interactables(self, enable: bool):
        """
            Enable or disable the interactable objects in the current page.
            :param enable: whether to enable or disable the interactable objects.
        """
        self.wizard().button(QWizard.WizardButton.FinishButton).setEnabled(enable)
        self.wizard().button(QWizard.WizardButton.BackButton).setEnabled(enable)
        self.config_box.setEnabled(enable)
        self.image_box.setEnabled(enable)
        self.data_box.setEnabled(enable)
        self.name_mapper_page.setEnabled(enable)


    def launch_first_copy_process(self, copy_folders: list):
        """
            Launch the first process that will copy the folder structure from the CartoDruid project.
            :param copy_folders: list of folder paths to copy from.
        """
        if not self.c_task:
            transformed_config = self.wizard().c_manager.convert_config_relative_path_to_absolute(self.wizard().c_manager.config)
            self.c_task = CopyFoldersFromDevice(self.wizard().s_eng, transformed_config, self.wizard().device, self.wizard().s_listener, copy_folders)
            self.set_enable_interactables(False)
            self.c_task.taskCompleted.connect(self._copy_directories_completed)
            self.c_task.taskTerminated.connect(self._copy_directories_finished)
            self.wizard().logger.info("Starting project copy.")
            self.wizard().progress2.show()
            QgsApplication.taskManager().addTask(self.c_task)

    def _copy_directories_finished(self):
        """
            launch_first_copy_process task terminated method. This method is called when CopyFoldersFromDevice is finished abruptly.
            Clean the current task, hides the progress bar and set the interactables enable again.
        """
        self.wizard().logger.error("There was an error copying the directories or it was terminated by the user.")
        self.c_task = None
        self.wizard().progress2.hide()
        self.set_enable_interactables(True)

    def _copy_directories_completed(self):
        """
            launch_first_copy_process task completed method. This method is called when CopyFoldersFromDevice is finished successfully.
            Clean the current task, hides the progress bar, set the interactables enable again, prepares the state of the page
            to pass to the next page and launches the download process.
        """
        self.wizard().logger.info("Project copy ended.")
        self.c_task = None
        self.wizard().progress2.hide()
        self._save_and_validate = True
        self.launch_first_download_process()

    def launch_first_download_process(self):
        """
            Launch the first download process that will download the files from the CartoDruid project.
        """
        if not self.d_task:
            transformed_config = self.wizard().c_manager.convert_config_relative_path_to_absolute(self.wizard().c_manager.config)
            self.d_task = LoadFiles(self.wizard().s_eng, transformed_config, self.wizard().device, True, self.wizard().s_listener)
            self.set_enable_interactables(False)
            self.d_task.taskCompleted.connect(self._download_files_completed)
            self.d_task.taskTerminated.connect(self._download_files_finished)
            self.wizard().logger.info("Starting project download.")
            self.wizard().progress2.show()
            QgsApplication.taskManager().addTask(self.d_task)

    def _download_files_finished(self):
        """
            launch_first_download_process task terminated method. This method is called when LoadFiles is finished abruptly.
            Clean the current task, hides the progress bar and set the interactables enable again.
        """
        self.wizard().logger.error("There was an error downloading the files or it was terminated by the user.")
        self.d_task = None
        self.wizard().progress2.hide()
        self.set_enable_interactables(True)

    def _download_files_completed(self):
        """
            launch_first_download_process task completed method. This method is called when LoadFiles is finished successfully.
            Clean the current task, hides the progress bar, set the interactables enable again, prepares the state of the page
            to pass to the next page and launches the download process.
        """
        self.wizard().logger.info("Project download ended.")
        self.d_task = None
        self.wizard().progress2.hide()
        self._save_and_validate = True
        self.set_enable_interactables(True)
        self.wizard().next()


    def on_cancel(self):
        """
        on_cancel method is called when the user cancels the download. Cancels the download tasks and cleans it.
        """
        if self.d_task:
            self.d_task.cancel()
            self.d_task = None
        if self.s_task:
            self.s_task.cancel()
            self.s_task = None


    def check_if_there_is_checked_item(self):
        for i in range(self.data_tree.topLevelItemCount()):
            parent = self.data_tree.topLevelItem(i)

            if parent.checkState(0) == Qt.Checked:
                return True

            for j in range(parent.childCount()):
                child = parent.child(j)

                if child.checkState(0) == Qt.Checked:
                    return True

        return False


    def isComplete(self):
        """
        Check if the current state of the page is ready to pass to the next page.
        :return: boolean indicating if the current state of the page is ready to pass to the next page.
        """
        if (self.config_box.isChecked() or self.image_box.isChecked()) and not self.data_box.isChecked():
            return True
        else:
            if self.data_box.isChecked():
                if self.data_tree.topLevelItemCount():
                    if self.check_if_there_is_checked_item():
                        return True
                    else:
                        return False
                else:
                    return True
            else:
                return False
