from __future__ import annotations
from typing import Optional, Callable
import enum
import json
import os.path
import tempfile
import time
import traceback
import zipfile
from functools import partial
from pathlib import Path
from typing import Generator, List, Tuple

from PySide6 import QtCore, QtWidgets, QtGui
from PySide6.QtCore import Signal
from PySide6.QtWebEngineCore import QWebEngineDownloadRequest

import unreal
from GN.ContentLibrary.Functions import identifier_to_unique_id

from GN.Globals import DEV_MODE
import GN.Logger as Log

from GN.ContentLibrary.Backends.Fab.CacheSync import CachesSynchronizer
from GN.ContentLibrary.Backends.Fab.Data import (data_to_cache, FabData,
                                                 SUPPORTED_2D_FORMATS, check_archive_contains_supported_files)
from GN.ContentLibrary.Backends.Fab.WebEngine import FabView, html_to_fab_data
from GN.ContentLibrary.Core import ContentLibraryAsset, AssetTypes, DownloadQueue, AssetLoader, AssetQueueElement
from GN.ContentLibrary.Data import (LIBRARY_PREFERENCES_GROUP, get_library_data_folder,
                                    LIBRARIES_INSTANCES, ContextMenuEntry, WidgetMenuEntry, WidgetMenuSpacer)
from GN.ContentLibrary.Models import LibraryModel
from GN.ContentLibrary.Widgets import LibraryWindow

from GN.UnrealContentLibrary.Core import UEAsset
from GN.UnrealContentLibrary.ImportersUtilities import find_asset_data
from GN.UnrealContentLibrary.LibraryFabImporter import (get_asset_project_folder_path, import_3d_asset, import_material,
                                                        create_quixel_lod_mappings, create_mesh_lods)

from GN.UnrealContentLibrary.Widgets import UnrealAssetWidgetAndTree
from GN.ResourcesUtils import get_content_dir

from GN.GNL.Popup import PopupWidget
from GN.GNL.PreferenceQuery import Preferences
from GN.Core.Data import DataRegistry
from GN.GNL.Preferences import SettingData, SettingType, registerSettings, SettingGroup, registerGroup, PreferenceWidget
from GN.GNL.Widgets.DashWidgetLibrary import ButtonWidget, IconWidget, Design, MenuPopup
from GN.GNL.Widgets.BaseWidget import BaseIconWidget

logger = Log.get_logger(__name__)

# if DEV_MODE:
#     logger.setLevel(Log.DEBUG)

WITH_WEB_VIEW = False

# data names, can't never change
FAB_PREF_PATH_NAME = "fab/download_path_unreal"  # folder with our data
FAB_UECACHE_PREF_PATH_NAME = "fab/cache_directory"  # folder to watch for new files from Fab integration
FAB_PREF_AUTOSYNC_NAME = "fab/epic_auto_sync"  # Should sync or not with epic fab integration


# UI names, can be edited and will only affect the UI
PREF_GROUP_UI_STR = "Fab Settings"
FAB_PREF_PATH_NAME_UI_STR = "Fab Library Path"
FAB_UECACHE_PREF_PATH_NAME_UI_STR = "Fab UE Cache Path"
FAB_PREF_AUTOSYNC_NAME_UI_STR = "Fab UE Cache Auto Sync"

# By default epic fab integration stores downloads in a folder under the user temp
CACHE_DEFAULT_VALUE = os.path.join(tempfile.gettempdir(), "FabLibrary")
CACHE_AUTO_SYNC = Preferences.getValue(LIBRARY_PREFERENCES_GROUP, FAB_PREF_AUTOSYNC_NAME, True)
logger.debug(f"AUTO SYNC {CACHE_AUTO_SYNC}")


class FabTierLabel(enum.Enum):
    RAW = "Raw"
    VERY_HIGH = "VeryHigh"
    HIGH = "High"
    MEDIUM = "Medium"
    LOW = "Low"
    UNKNOWN = "Unknown"

    @classmethod
    def __lookup(cls, reversed_lookup=False):
        lookup = {
            0: cls.VERY_HIGH,
            1: cls.HIGH,
            2: cls.MEDIUM,
            3: cls.LOW,
        }

        if reversed_lookup:
            return {v: k for k, v in lookup.items()}

        return lookup

    @classmethod
    def sort_key(cls, item):
        order = {
            cls.RAW: 5,
            cls.VERY_HIGH: 4,
            cls.HIGH: 3,
            cls.MEDIUM: 2,
            cls.LOW: 1,
            cls.UNKNOWN: -1
        }
        return order.get(item, 5)

    @classmethod
    def from_integer(cls, value: int):
        return cls.__lookup().get(value, cls.UNKNOWN)

    @classmethod
    def to_integer(cls, tier):
        return cls.__lookup(reversed_lookup=True).get(tier, -1)


def open_project_paths_prefs():
    PreferenceWidget.openPreferences(focus_group=PREF_GROUP_UI_STR)


def convert_meter_string_to_cm_float(meter_string: str):
    """Converts a string like 1.5m to 150.0"""
    if meter_string.endswith("m"):
        return float(meter_string[:-1]) * 100.0
    else:
        return float(meter_string)


def extract_relevant_quixel_metadata(current_tier: FabTierLabel, dash_type, quixel_meta_file: str):
    """From the json file included in quixel assets, extend our metadata with the relevant info"""
    data = dict()

    with open(quixel_meta_file, "r") as f:
        quixel_meta = json.load(f)

        # Try to get the quixel identifier
        megascan_id = quixel_meta.get("id")
        if megascan_id:
            data["megascan_id"] = megascan_id

        # Try to get dimensions and triangle counts
        bounding_box_extents = list()
        triangle_counts = dict()
        texture_resolutions = dict()

        semantic_tags = quixel_meta.get("semanticTags", dict())
        megascan_type = semantic_tags.get("asset_type", None)

        # Further refine the type from metadata specific to quixel
        if megascan_type == "3D asset":
            dash_type = AssetTypes.ASSET_3D
        elif megascan_type == "surface":
            dash_type = AssetTypes.ASSET_PF_SURFACES_FPS
        elif megascan_type == "3D plant":
            dash_type = AssetTypes.ASSET_3D_PLANTS
        elif megascan_type == "decal":
            dash_type = AssetTypes.ASSET_DECALS
        elif megascan_type == "atlas":
            dash_type = AssetTypes.ASSET_ATLAS_2D
        elif megascan_type == "imperfection":
            dash_type = AssetTypes.ASSET_DECALS

        # Ensure we store the new type in the data directly!
        data["asset_type"] = dash_type

        if dash_type == AssetTypes.ASSET_3D:
            for entry in quixel_meta.get("meta", list()):
                # print("entry", entry)
                if entry.get("key") in ["width", "height", "length"]:
                    meter_string = entry["value"]
                    float_cm = convert_meter_string_to_cm_float(meter_string)
                    bounding_box_extents.append(float_cm)

            for entry in quixel_meta.get("models", list()):
                triangle_count = entry.get("tris")
                lod = entry.get("tier")
                lod_tier = FabTierLabel.from_integer(lod)

                if triangle_count and lod_tier:
                    current_tri_count = triangle_counts.get(lod_tier.value, 0)
                    if triangle_count > current_tri_count:
                        triangle_counts[lod_tier.value] = triangle_count

        elif dash_type in (AssetTypes.ASSET_PF_SURFACES,
                           AssetTypes.ASSET_DECALS,
                           AssetTypes.ASSET_ATLAS_2D):
            res_lookup = {
                FabTierLabel.LOW: 1024,
                FabTierLabel.MEDIUM: 2048,
                FabTierLabel.HIGH: 4096,
                FabTierLabel.VERY_HIGH: 8192
            }
            texture_resolutions[current_tier.value] = res_lookup.get(current_tier, 0)

        if bounding_box_extents:
            data["bounding_box"] = bounding_box_extents

        if triangle_counts:
            data["triangle_counts"] = triangle_counts

        if texture_resolutions:
            data["texture_resolutions"] = texture_resolutions

    return data


def get_resolution_from_quixel_folder(dash_type, folder_name) -> FabTierLabel:
    """From a folder name, try to get the tier label with string checks"""
    current_tier = FabTierLabel.UNKNOWN

    folder_tokens = folder_name.split("_")

    if dash_type in AssetTypes.ANY_3D_ASSET:
        if "low" in folder_tokens:
            current_tier = FabTierLabel.LOW
        elif "mid" in folder_tokens:
            current_tier = FabTierLabel.MEDIUM
        elif "high" in folder_tokens:
            current_tier = FabTierLabel.HIGH
        elif "raw" in folder_tokens:
            current_tier = FabTierLabel.RAW

    elif dash_type in (AssetTypes.ASSET_PF_SURFACES_FPS,
                       AssetTypes.ASSET_DECALS,
                       AssetTypes.ASSET_ATLAS_2D):
        if "1k" in folder_tokens:
            current_tier = FabTierLabel.LOW
        elif "2k" in folder_tokens:
            current_tier = FabTierLabel.MEDIUM
        elif "4k" in folder_tokens:
            current_tier = FabTierLabel.HIGH
        elif "8k" in folder_tokens:
            current_tier = FabTierLabel.VERY_HIGH
    else:
        logger.error(f"Asset type {dash_type} is not yet implemented {folder_name}")

    return current_tier


def find_opacity_texture(current_source):
    """Given the source folder of a quixel atlas, return the opacity texture if found"""
    opacity_texture = None
    logger.debug(f"Looking for opacity texture in {current_source}")
    for data in os.walk(current_source):
        root, _, files = data
        for file in files:
            logger.debug(f"Checking file {file}")
            file_tokens = file.split(".")
            extension = file_tokens[-1]
            file_name = file_tokens[0]
            if extension in SUPPORTED_2D_FORMATS:
                if file_name.lower().endswith("_opacity"):
                    opacity_texture = str(os.path.join(root, file))
                    logger.debug(f"Found opacity texture {opacity_texture}")
                    break
            else:
                logger.debug(f"Invalid extension {extension}")

    return opacity_texture


def _create_mesh_cards(asset_folder: str,
                      opacity_texture_path: str,
                      material_instance: unreal.MaterialInstanceConstant):
    """Pretty much identical copy on how we generate cards inside megascan library"""

    def setNaniteEnabled(static_meshes: list[unreal.StaticMesh], enabled: bool = True):
        ue_version = str(unreal.SystemLibrary.get_engine_version())
        if not ue_version.startswith('5'):
            print("Nanite is only available in Unreal Engine 5.x")
            return

        static_mesh_editor_subsystem = unreal.get_editor_subsystem(
            unreal.StaticMeshEditorSubsystem)

        for static_mesh in static_meshes:
            if static_mesh is None:
                continue
            nanite_setting = static_mesh_editor_subsystem.get_nanite_settings(static_mesh)
            nanite_setting.enabled = enabled
            static_mesh_editor_subsystem.set_nanite_settings(static_mesh, nanite_setting, True)

    from GN.UnrealFunctionLibraries.SceneLibrary import convertProceduralToStaticMeshes
    from GN.Standard.Libraries.SceneContainer import AssetContainer, SceneContainer
    from GN.Standard.ObjectsInterface import AssetObjectInterface
    from GN.GNL.Core import GnTool

    alpha_cut_id = 1381739
    texture_property_id = 'a77ab249468e'
    face_method_id = '92d25dad73d6'
    pivot_placement_id = 'ada98d6c0635'
    material_id = 'f643d7d0ce84'

    # Create the tool instance with the settings the user might have added in the prompt
    alpha_tool = GnTool.createFromID(alpha_cut_id, show=False)

    # Set default properties
    material_prop = alpha_tool.activeInstance.getPropertyByID(material_id)
    material_prop.value = AssetContainer([AssetObjectInterface(material_instance)])

    face_to_prop = alpha_tool.activeInstance.getPropertyByID(face_method_id)
    face_to_prop.value = "Face Up"

    pivot_placement_prop = alpha_tool.activeInstance.getPropertyByID(pivot_placement_id)
    is_bottom = False
    # categories_bottom_pivot = {
    #     "plant",
    #     "flower",
    #     "fern"
    # }
    # if any([cat.lower() in categories_bottom_pivot for cat in self.categories]):
    #     is_bottom = True
    pivot_placement_prop.value = "Bottom" if is_bottom else "Center"

    texture_prop = alpha_tool.activeInstance.getPropertyByID(texture_property_id)
    texture_prop.value = opacity_texture_path

    tool_result = alpha_tool.activeInstance.outputObjects()
    created_actors: SceneContainer = SceneContainer.fromInterfaceIds(tool_result)

    # Remove the tool instance, convert to static meshes and place the cards where the user can see it
    convertProceduralToStaticMeshes(created_actors, project_path=asset_folder)

    alpha_tool.removeTool(alpha_tool.identifier)
    created_actors.delete()

    meshes = []
    for asset_string in unreal.EditorAssetLibrary.list_assets(asset_folder):
        asset_data: unreal.AssetData = unreal.EditorAssetLibrary.find_asset_data(asset_string)
        if not asset_data.is_valid():
            continue
        if isinstance(asset_data.get_asset(), unreal.StaticMesh):
            static_mesh: unreal.StaticMesh = asset_data.get_asset()
            static_mesh.set_material(0, material_instance)
            meshes.append(static_mesh)
    setNaniteEnabled(meshes, True)
    for mesh in meshes:
        mesh.modify()
        unreal.EditorAssetLibrary.save_loaded_asset(mesh)

    return meshes


class UnpackTaskSignal(QtCore.QObject):
    unpackFinished = Signal(str)


class UnpackTask(QtCore.QRunnable):
    """
    After a zip file is in place in the directory, unpack it and update the metadata file
    """

    def __init__(self, dir_path: str):
        super().__init__()

        self.zip_full_path = dir_path
        self.signals = UnpackTaskSignal()

    def run(self):
        # Search for files inside the zip archive
        try:
            with zipfile.ZipFile(self.zip_full_path, 'r') as zip_ref:
                found_fbx, found_gltf, found_images = check_archive_contains_supported_files(zip_ref)

                # Name the resulting folder with some uniqueness to the content
                # (different content zip archives have the same name usually)
                output_name = f'{os.path.basename(self.zip_full_path).split(".")[0]}'
                if found_fbx:
                    output_name += "_fbx_extracted"
                elif found_gltf:
                    output_name += "_gltf_extracted"
                elif found_images:
                    output_name += "_images_extracted"
                else:
                    logger.error(f"Could not find any valid files in {self.zip_full_path}")
                    return

                output_dir = os.path.join(os.path.dirname(self.zip_full_path), output_name)
                zip_ref.extractall(output_dir)
                logger.info(f"Unpacked {self.zip_full_path} to {output_dir}")

        except zipfile.BadZipFile:
            print(f"Error: {self.zip_full_path} is not a valid zip file.")
            return

        # We can safely remove the zip file
        os.remove(self.zip_full_path)
        logger.info(f"Removed {self.zip_full_path}")

        # Update the metadata file with the new data
        base_dir = os.path.dirname(self.zip_full_path)
        metadata_file = os.path.join(base_dir, "metadata.json")

        if os.path.exists(metadata_file):
            with open(metadata_file, "r+") as m_handle:
                data = json.load(m_handle)

                current_tier = FabTierLabel.UNKNOWN

                fbx_files = data.get("fbx_files", dict())
                gltf_files = data.get("gltf_files", dict())
                image_files = data.get("image_files", dict())

                # Convert and store the type right away
                asset_type = data.get("asset_type", "")

                # Check the vendor, and if quixel, we can do extra processing
                vendor = data.get("vendor", "Unknown")
                folder_name = os.path.basename(output_dir)

                # -------------------- Extra metadata
                if vendor == "Quixel":
                    current_tier = get_resolution_from_quixel_folder(asset_type, folder_name)

                    all_files = os.listdir(output_dir)
                    json_files = [f for f in all_files if f.endswith(".json")]
                    if json_files:
                        match = json_files[0]
                        extra_data = extract_relevant_quixel_metadata(current_tier, asset_type, os.path.join(output_dir, match))
                        data.update(extra_data)

                # -------------------- Update the metadata file with the new data per tier (store RELATIVE PATH!)
                if found_fbx:
                    fbx_files[current_tier.value] = folder_name
                elif found_gltf:
                    gltf_files[current_tier.value] = folder_name
                elif found_images:
                    image_files[current_tier.value] = folder_name

                data["fbx_files"] = fbx_files
                data["gltf_files"] = gltf_files
                data["image_files"] = image_files

                m_handle.seek(0)
                json.dump(data, m_handle, indent=4)
                m_handle.truncate()
                logger.info(f"Updated metadata file {metadata_file}")
                self.signals.unpackFinished.emit(output_dir)

        else:
            logger.error(f"Metadata file not found {metadata_file}")


class FabLoader(AssetLoader):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        global CACHE_AUTO_SYNC

        fab_cache_path = Preferences.getValue(LIBRARY_PREFERENCES_GROUP, FAB_UECACHE_PREF_PATH_NAME,
                                              default=CACHE_DEFAULT_VALUE)

        if fab_cache_path:
            self.sync_obj = CachesSynchronizer(fab_cache_path, FabAsset.getRootFolder())
            self.sync_obj.can_sync = CACHE_AUTO_SYNC
            self.sync_obj.syncStarted.connect(self.onSyncStarted)
            self.sync_obj.syncProgress.connect(self.onSyncProgress)
            self.sync_obj.syncFinished.connect(self.onSyncFinished)
            self.sync_obj.dataAvailable.connect(self.onDataAvailable)
        else:
            logger.error("Could not find a valid path for the cache sync")

        self.download_in_progress = False
        self.sync_value = True
        self.sync_item_name = "Fab Cache Sync"
        self.sync_total_items = 0

    def setAutoSync(self, value: bool):
        """Turn on or off the automatic sync with the cache"""
        global CACHE_AUTO_SYNC

        logger.debug(f"Setting auto sync to {value}")
        Preferences.setValue(LIBRARY_PREFERENCES_GROUP, FAB_PREF_AUTOSYNC_NAME, value)

        if value is False:
            # first turn it off
            self.sync_obj.can_sync = False
            # Then set the global value
            CACHE_AUTO_SYNC = value
        else:
            # First unlock the internal value
            CACHE_AUTO_SYNC = value
            # Then set back whatever value has been set externally
            self.sync_obj.can_sync = self.sync_value

    def setSyncValue(self, value: bool):
        """
        Turn on or off the sync operation. Mostly useful to avoid background operations if the library is closed
        """
        self.sync_value = value

        if CACHE_AUTO_SYNC is False:
            logger.info("Auto sync is disabled, enable it to use this feature")
            return

        self.sync_obj.can_sync = value

    def startSync(self):
        self.sync_obj.syncCaches()

    def onSyncStarted(self, total_asset_num: int):
        self.messageSet.emit("Checking new downloads from Fab..")
        self.download_in_progress = True

        # Create the download asset in the queue
        data_sync_item = FabSyncItem(self.sync_item_name)
        self.sync_total_items = total_asset_num
        DownloadQueue.updateProgress(data_sync_item, 0, "Copy Asset Data...")

    def onSyncProgress(self, asset_number: int):
        self.messageSet.emit("")

        # Update the sync item
        data_sync_item = DownloadQueue.getQueueItem(self.sync_item_name)
        if data_sync_item:
            progress = int((asset_number / self.sync_total_items) * 100)

            DownloadQueue.updateProgress(data_sync_item, progress, f"Sync Asset {asset_number}")
            if progress == 100:
                DownloadQueue.updateProgress(data_sync_item, progress, "Finished")

    def deleteLater(self):
        self.on_destroyed()
        super().deleteLater()

    def on_destroyed(self):
        print("FabLoader destroyed")
        try:
            self.sync_obj.disconnect()
        except:
            pass
        self.sync_obj.deleteLater()

    def onDataAvailable(self, data: list[str]):
        for path in data:
            unpack_task = UnpackTask(path)
            unpack_task.signals.unpackFinished.connect(self.onDownloadExtracted, QtCore.Qt.ConnectionType.QueuedConnection)
            QtCore.QThreadPool.globalInstance().start(unpack_task)

    def onSyncFinished(self):
        self.download_in_progress = False
        self.messageSet.emit("")

        # Ensure the 100 is reached to signal to the queue the item can be removed (yeah assume no errors for now...)
        data_sync_item = DownloadQueue.getQueueItem(self.sync_item_name)
        if data_sync_item:
            DownloadQueue.updateProgress(data_sync_item, 100, "Finished")

    def _onWatchedDiskDataChanged(self, *args):
        # Downloads will trigger the folder watcher, but the data might not be ready yet...
        if self.download_in_progress:
            logger.debug("Skipping folder changed.. download in progress")
            return

        super()._onWatchedDiskDataChanged(*args)

    def onDownloadExtracted(self):
        logger.info("Download finished and extracted.. check updates now")
        super()._onWatchedDiskDataChanged()
        self.download_in_progress = False


class FabAsset(UEAsset):
    """
    Extend the main ContentLibraryAsset class to define your own asset type.
    This object will be responsible to provide the data needed to display and acquire the resources needed
    to use the asset in the project.
    """

    ASSET_ROOT_PATH = None

    @staticmethod
    def processData(root_path, data: dict):
        """Do the necessary conversions from the original metadata to the one used in the library"""
        # Build the absolute path for the thumbnail
        data["thumbnail"] = os.path.join(root_path, data["thumbnail"])

        # dev helper: we downloaded a lot before introducing PF_SURFACES_FPS, so we convert it at runtime
        saved_type = data["asset_type"]
        if saved_type == AssetTypes.ASSET_PF_SURFACES:
            data["asset_type"] = AssetTypes.ASSET_PF_SURFACES_FPS

        return data

    # ---------------------------------------- Class Abstract methods impl ------------------------------------------- #
    @classmethod
    def _createFromIdentifier(cls, identifier: str) -> ContentLibraryAsset or None:
        asset_folder = os.path.join(cls.getRootFolder(), identifier)
        asset_data = os.path.join(asset_folder, "metadata.json")
        if not os.path.exists(asset_data):
            return None

        with open(asset_data, "r") as f:
            data = json.load(f)
            data = cls.processData(asset_folder, data)
            return cls(data)

    @classmethod
    def getRootFolder(cls) -> str:
        """
        Get where all the asset data and metadata is stored
        """
        if cls.ASSET_ROOT_PATH is not None:
            return cls.ASSET_ROOT_PATH

        library_folder = "FabLibrary"
        default_path = Path(get_library_data_folder()).joinpath(library_folder).as_posix()
        find_prefs_path: str = Preferences.getValue(LIBRARY_PREFERENCES_GROUP, FAB_PREF_PATH_NAME,
                                                    default=default_path)
        if library_folder not in find_prefs_path:
            find_prefs_path += f"/{library_folder}"

        cls.ASSET_ROOT_PATH = Path(find_prefs_path).as_posix()
        logger.debug(f"Root folder cached to {cls.ASSET_ROOT_PATH}")

        return cls.ASSET_ROOT_PATH

    @classmethod
    def setRootFolder(cls, path, save=True):
        """
        Set the root folder where the asset data and metadata is stored
        """
        Preferences.setValue(LIBRARY_PREFERENCES_GROUP, FAB_PREF_PATH_NAME, path, save=save)
        cls.ASSET_ROOT_PATH = None
        cls.ASSET_ROOT_PATH = cls.getRootFolder()
        logger.debug(f"Root folder is now set to {cls.ASSET_ROOT_PATH}")

    @classmethod
    def getAllObjects(cls) -> Generator[UEAsset, None, None]:
        """Build all the assets items for the library view from the root folder"""
        if os.path.exists(cls.getRootFolder()):
            # Iterate over the directories
            root = cls.getRootFolder()
            directories = sorted(os.listdir(root), key=lambda x: os.path.getmtime(os.path.join(root, x)), reverse=True)

            for directory in directories:

                # Look for a metadata file .json
                root = os.path.join(cls.getRootFolder(), directory)
                metadata_file = os.path.join(root, "metadata.json")

                if os.path.exists(metadata_file):
                    logger.log(Log.VERBOSE, f"Reading file  {metadata_file}")
                    try:
                        with open(metadata_file, "r") as f:
                            data = json.load(f)

                            fbx_files = data.get("fbx_files", dict())
                            gltf_files = data.get("gltf_files", dict())
                            image_files = data.get("image_files", dict())

                            # Ensure at least one of these is not empty
                            if not any([fbx_files, gltf_files, image_files]):
                                logger.warning(f"Asset {directory} has no valid files saved")
                                continue

                            data = cls.processData(root, data)
                            yield cls(data)

                    except Exception as e:
                        logger.error(f"Error loading asset from {metadata_file}: {e}")
                        # traceback.print_exc()
                        continue

    # ---------------------------------------------------------------------------------------------------------------- #
    def __init__(self, data: dict):
        super().__init__(data)

        self.current_asset_dir = os.path.join(self.getRootFolder(), self.identifier)
        self.is_quixel = self.vendor == "Quixel"
        self.megascan_id = data.get("megascan_id", "___")

        if not self.is_quixel:
            self.merge_meshes = True

        # Generate the in-out folders for the engine, based on the currently selected tier
        self.fbx_files: dict[FabTierLabel, str] = data.get("fbx_files", dict())
        self.gltf_files: dict[FabTierLabel, str] = data.get("gltf_files", dict())
        self.image_files: dict[FabTierLabel, str] = data.get("image_files", dict())

        self._tiers_found: list[FabTierLabel] = list()
        self._tier_data: list[int] = list()
        self.source_folders = dict()
        self.dest_engine_paths = dict()
        self.dest_disk_paths = dict()

        # Stores the max_resolution for textures, organized by tier str
        self._texture_data = data.get("texture_resolutions", dict())
        self._tri_data = data.get("triangle_counts", dict())

        # Storage for valid paths once the asset is imported
        self._mesh_paths = []
        self._texture_paths = []
        self._material_instances_paths = []

        # Order the data and use the file formats we prefer
        self.__generateFolderData()
        self.setActiveTier(0)

    def __getActiveTierLabel(self) -> FabTierLabel:
        try:
            curr_tier = self._tiers_found[self.getActiveTier()]
            logger.log(Log.VERBOSE, f"Current tier returned {curr_tier} mem id: {id(self)} from tier id {self.getActiveTier()}")
        except IndexError:
            curr_tier = self._tiers_found[0]
            logger.warning(f"Active tier was invalid: looking for {self.getActiveTier()} but available indices are {range(len(self._tiers_found))}")

        return curr_tier

    def updateTiers(self):
        # Re read the metadata file to get the new tiers
        metadata_file = os.path.join(self.current_asset_dir, "metadata.json")
        logger.log(Log.VERBOSE, f"Reading metadata file {metadata_file}")

        if os.path.exists(metadata_file):
            with open(metadata_file, "r") as f:
                data = json.load(f)
                self.fbx_files = data.get("fbx_files", dict())
                self.gltf_files = data.get("gltf_files", dict())
                self.image_files = data.get("image_files", dict())
                logger.log(Log.VERBOSE, f"Updated tiers folders fbx: {len(self.fbx_files)} gltf: {len(self.gltf_files)} img: {len(self.image_files)}")

        logger.log(Log.VERBOSE, "Will now re cache the folders")
        self.__generateFolderData()

    def __generateFolderData(self):
        """
        From the folders found on disk, generate the in-out folders for the engine.
        If a new tier is downloaded while this instance is alive, it will need to be re-run.
        """
        # Clear the previous data as we might call this in update
        self._tiers_found = list()
        self._tier_data: list[int] = list()
        self.source_folders = dict()
        self.dest_engine_paths = dict()
        self.dest_disk_paths = dict()

        # Read the folders with priority to fbx
        if self.fbx_files:
            tier_folders = self.fbx_files
        elif self.gltf_files:
            tier_folders = self.gltf_files
        else:
            tier_folders = self.image_files

        for tier_label, folder_name in tier_folders.items():
            # Some metadata has old label, causing import errors..
            # we can update the metadata at some point.. for now, leave it!
            if tier_label == "Very High":
                tier_label = "VeryHigh"

            curr_tier = FabTierLabel(tier_label)
            self._tiers_found.append(curr_tier)
            logger.log(Log.VERBOSE, f"Reading {tier_label} folder {folder_name}")

            # Content folders are stored as relative paths, make it absolute
            folder_full_path = os.path.join(self.current_asset_dir, folder_name)
            engine_asset_name = self.name

            if self.is_quixel:

                # DEV ONLY Update meta on read
                up_on_read = False and DEV_MODE
                if up_on_read:
                    all_files = os.listdir(folder_full_path)
                    json_files = [f for f in all_files if f.endswith(".json")]
                    if json_files:
                        extra = extract_relevant_quixel_metadata(curr_tier, self.asset_type, os.path.join(folder_full_path, json_files[0]))
                        print("extra", extra)
                        with open(os.path.join(self.current_asset_dir, "metadata.json"), "r+") as m_handle:
                            data = json.load(m_handle)
                            data.update(extra)
                            m_handle.seek(0)
                            json.dump(data, m_handle, indent=4)
                            m_handle.truncate()
                            print(f"Updated metadata file {self.name}")

                engine_asset_name = f"{self.name}_{self.megascan_id}"

            folder_path = get_asset_project_folder_path(engine_asset_name, self.asset_type, self.is_quixel, str(curr_tier.value))
            asset_folder_on_disk, out_engine_path = get_content_dir(folder_path)
            self.source_folders[curr_tier] = folder_full_path
            self.dest_engine_paths[curr_tier] = out_engine_path
            self.dest_disk_paths[curr_tier] = asset_folder_on_disk
            logger.log(Log.VERBOSE, f"Saving source folder {folder_name} to {asset_folder_on_disk}")
            logger.log(Log.VERBOSE, f"Saving engine folder {folder_name} to {out_engine_path}")

        # Sort the tiers
        self._tiers_found = sorted(self._tiers_found, key=FabTierLabel.sort_key)
        self._tier_data = list(range(len(self._tiers_found)))

        logger.log(Log.VERBOSE, f"Tiers found: {self._tiers_found}")
        logger.log(Log.VERBOSE, f"Tier data: {self._tier_data}")
        logger.log(Log.VERBOSE, f"Source folders: {self.source_folders}")

    def clearPathCaches(self):
        super().clearPathCaches()  # clear the actual asset references!!
        # Clear the paths we have cached.. this mechanism needs a cleanup...
        self._mesh_paths = []
        self._texture_paths = []
        self._material_instances_paths = []
        logger.log(Log.VERBOSE, "Cleared cached paths")

    def __cacheAssetPaths(self):
        """
        When the assets are already imported, we need to retrieve the paths
        from the actual data in order to be able to spawn them in the level
        """
        self.clearPathCaches()

        asset_folder = self.getCurrentDestEnginePath()
        for asset_path in unreal.EditorAssetLibrary.list_assets(asset_folder):
            asset_data = find_asset_data(asset_path)
            if asset_data.is_valid():
                asset = asset_data.get_asset()
                if isinstance(asset, (unreal.StaticMesh, unreal.SkeletalMesh)):
                    if asset.get_path_name() not in self._mesh_paths:
                        self._mesh_paths.append(asset.get_path_name())
                if isinstance(asset, unreal.MaterialInstanceConstant):
                    if asset.get_path_name() not in self._material_instances_paths:
                        self._material_instances_paths.append(asset.get_path_name())
                if isinstance(asset, unreal.Texture):
                    if asset.get_path_name() not in self._texture_paths:
                        self._texture_paths.append(asset.get_path_name())

    def getCurrentSourceQuixelMeta(self):
        """Return the metadata file inside the folder with the data to import"""
        source_folder = self.getCurrentSourceFolder()
        all_files = os.listdir(source_folder)
        json_files = [f for f in all_files if f.endswith(".json")]
        if json_files:
            json_data_file = os.path.join(source_folder, json_files[0])
            with open(json_data_file, "r") as f:
                return json.load(f)

    def getCurrentSourceFolder(self):
        """Get the folder where we have the data ready for an import operation, for the current tier"""
        curr_tier = self.__getActiveTierLabel()
        return self.source_folders[curr_tier]

    def getCurrentDestEnginePath(self):
        """Get the engine folder where we can import asset in the project, for the current tier"""
        curr_tier = self.__getActiveTierLabel()
        return self.dest_engine_paths[curr_tier]

    def getCurrentDestDiskPath(self):
        """Get the path on disk of the folder where we can import asset, for the current tier"""
        curr_tier = self.__getActiveTierLabel()
        return self.dest_disk_paths[curr_tier]

    # ------------------------------------------ Abstract methods impl/override -------------------------------------- #
    @property
    def acquire_resources_on_drop(self) -> bool:
        return True

    @property
    def is_downloadable(self) -> bool:
        return False

    @property
    def num_assets(self):
        if self.isAssetOnDisk():
            if not self._initialized:
                self.__cacheAssetPaths()

            return len(self._mesh_paths)
        return 1

    @property
    def num_triangles(self) -> int or None:
        """Returns the number of triangles, if applicable"""
        curr_tier = self.__getActiveTierLabel()
        return self._tri_data.get(curr_tier.value, None)

    @property
    def max_texture_size(self) -> int or None:
        """Returns the maximum texture size, if applicable"""
        curr_tier = self.__getActiveTierLabel()
        return self._texture_data.get(curr_tier.value, None)

    def getTierLabel(self, tier_id) -> str:
        """
        Return the label for the given tier id
        """
        try:
            value = self._tiers_found[tier_id].value
        except IndexError:
            value = "Unknown"
        return value

    def setActiveTier(self, tier_id):
        super().setActiveTier(tier_id)
        logger.log(Log.VERBOSE, f"Setting active tier  {tier_id} correspond to label {self._tiers_found[tier_id].value}")

    def isSourceOnDisk(self):
        """If the source of the asset data is on disk, return True"""
        if os.path.exists(self.getCurrentSourceFolder()):
            return True
        return False

    def isAssetOnDisk(self):
        """If the asset is imported in the project, return True"""
        curr_dest_on_disk = self.getCurrentDestDiskPath()
        path_exists = os.path.exists(curr_dest_on_disk)
        if path_exists:
            has_content = len(os.listdir(curr_dest_on_disk)) > 0
            return has_content
        return False

    def acquireResources(self):
        """
        If isAssetOnDisk is False, or is the first time the asset is dragged (self._initialized is False),
        this function will be called to import the asset in the project.
        In this stage we also cache the engine specific paths, for spawning/assigning/placing
        the correct actor in the level.
        """
        # check if asset is already added to project, if so, all good
        # NOTE: this mechanism should be build in in the logic and not require to "hack" the acquireResources method
        assets_are_imported = self.isAssetOnDisk()
        if assets_are_imported:
            self.__cacheAssetPaths()
            return

        source_folder = self.getCurrentSourceFolder()
        engine_dest = self.getCurrentDestEnginePath()

        logger.debug(f"{self.name} from source folder {source_folder}")
        logger.debug(f"{self.name} to engine dest {engine_dest}")

        if self.asset_type in AssetTypes.ANY_3D_ASSET:

            auto_save = True
            if self.asset_type == AssetTypes.ASSET_3D_PLANTS:
                auto_save = False  # We save after LODs are created if it's a plant

            created_assets = import_3d_asset(source_folder,
                                             engine_dest,
                                             self.is_quixel,
                                             self.asset_type,
                                             auto_save=auto_save)

            # Create the LODs for the plants
            if self.asset_type == AssetTypes.ASSET_3D_PLANTS:
                quixel_meta = self.getCurrentSourceQuixelMeta()
                variations = create_quixel_lod_mappings(quixel_meta)

                # Get the id of the tier, as it's used as multiplier for LOD screen size
                lod_id = FabTierLabel.to_integer(self.__getActiveTierLabel())
                created_assets.static_meshes = create_mesh_lods(self.getCurrentDestEnginePath(), variations, lod_id)

        elif self.asset_type in (AssetTypes.ASSET_PF_SURFACES_FPS,
                                 AssetTypes.ASSET_DECALS,
                                 AssetTypes.ASSET_ATLAS_2D):

            created_assets = import_material(source_folder,
                                             engine_dest,
                                             self.is_quixel,
                                             self.asset_type)

            if self.asset_type == AssetTypes.ASSET_ATLAS_2D:

                # We need the original source file for Alpha cut, not the uasset!
                current_source = self.getCurrentSourceFolder()
                opacity_texture = find_opacity_texture(current_source)
                logger.debug(f"Looking for opacity texture in {current_source}")

                if opacity_texture:
                    logger.debug("Creating mesh cards...")
                    created_assets.static_meshes = _create_mesh_cards(self.getCurrentDestEnginePath(),
                                                                      opacity_texture,
                                                                      created_assets.material_instances[0])
                else:
                    logger.error(f"Could not find opacity texture for {self.name}")

        else:
            logger.error(f"Asset type {self.asset_type} is not yet implemented")
            return

        # Store the paths for the assets
        self.clearPathCaches()
        all_meshes = created_assets.static_meshes + created_assets.skeletal_meshes
        self._mesh_paths = [mesh.get_path_name() for mesh in all_meshes]
        self._texture_paths = [texture.get_path_name() for texture in created_assets.texture_instances]
        self._material_instances_paths = [material.get_path_name() for material in created_assets.material_instances]

        unreal.DashLib.close_editor_tab("MessageLog")

    def getMeshPaths(self) -> List[str]:
        logger.debug(self._mesh_paths)
        return self._mesh_paths

    def getMaterialInstancesPaths(self) -> List[str]:
        logger.debug(self._material_instances_paths)
        return self._material_instances_paths

    def getPolygonflowMaterialInstances(self):
        return self.getMaterialInstanceAssets()

    def getTexturesPaths(self) -> List[str]:
        logger.debug(self._texture_paths)
        return self._texture_paths

    # these methods below should probably be removed.....
    def getMasterMaterialsPaths(self) -> List[str]:
        logger.debug("...")
        return []

    def getFoliagePaths(self) -> List[str]:
        logger.debug("...")
        return []

    def getEnginePath(self) -> str:
        logger.debug("...")
        return self.getCurrentDestEnginePath()


class FabDownloadingItem(AssetQueueElement):
    """Item representing a zip asset being downloaded"""
    def __init__(self, name, identifier, parent=None):
        super().__init__(name, identifier, parent=parent)
        self.__identifier = identifier  # the id used for the library

    @property
    def unique_id(self):
        # Compose the unique id to be able to find the asset back from the global storage
        return identifier_to_unique_id(FabLibraryWidget.LIBRARY_NAME, self.__identifier)


class FabSyncItem(AssetQueueElement):
    """Item used to fill the queue when the caches are syncing"""
    def __init__(self, name, parent=None):
        super().__init__(name, name, parent=parent)


class FabWebPanel(QtWidgets.QWidget):

    assetDownloadStarted = Signal(str)
    assetDownloadFinished = Signal(str)
    assetDataExtracted = Signal(str)
    revealDownloadedAsset = Signal(str)

    def __init__(self, parent=None):
        super().__init__(parent=parent)

        self.main_layout = QtWidgets.QVBoxLayout(self)
        self.main_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
        self.main_layout.setContentsMargins(0, 0, 0, 0)
        self.main_layout.setSpacing(0)
        self.setLayout(self.main_layout)

        # Create the web object storage
        self.__fab_store = FabView(parent=self)
        self.__fab_store.onDownloadRequested.connect(self.onItemDownloadRequested)
        self.__fab_store.onDownloadFinished.connect(self.processDownloadQueue)
        self.__fab_store.onDownloadProgress.connect(self.onDownloadProgress)
        self.__fab_store.urlChanged.connect(self.__onUrlChanged)
        self.__fab_store.urlChanged.connect(self.__updateNavigationButtons)
        self.main_layout.addWidget(self.__fab_store)

        # Navigation controls
        self.previous_page_w = IconWidget("arrow_left_base", "arrow_left_color", offset=3)
        self.previous_page_w.setToolTip("Go back")
        self.previous_page_w.setVisible(False)
        self.previous_page_w.Click.connect(lambda: self.__fab_store.back())
        
        self.forward_page_w = IconWidget("arrow_right_base", "arrow_right_color", offset=3)
        self.forward_page_w.setToolTip("Go forward")
        self.forward_page_w.setVisible(False)
        self.forward_page_w.Click.connect(lambda: self.__fab_store.forward())

        # Quixel shortcuts button
        self.quixel_categories_w = ButtonWidget()
        self.quixel_categories_w.setText("Quixel")
        self.quixel_categories_w.setFixedHeight(Design.getItemHeight()-4)
        self.quixel_categories_w.computeWidth(set_width=True, offset=25)
        self.quixel_categories_w.design.setColor(Design.ColorDarkGray2, Design.ColorGray4)
        self.quixel_categories_w.design.setBorderWidth(0)
        self.quixel_categories_w.design.update()
        self.quixel_categories_w.setToolTip("Quickly access various Quixel Megascans pages")
        self.quixel_categories_w.Click.connect(self.__openQuixelShortcuts)
        self.quixel_categories_w.setVisible(False)

        self.download_queue = list()
        self.downloading = False

    def deleteLater(self):
        self.__fab_store.close()
        self.__fab_store.deleteLater()
        super().deleteLater()

    def __updateNavigationButtons(self):
        """Update the enabled state of navigation buttons based on history"""
        self.previous_page_w.setEnabled(self.__fab_store.page().history().canGoBack())
        self.forward_page_w.setEnabled(self.__fab_store.page().history().canGoForward())

    def __openQuixelShortcuts(self):
        quixel_options = MenuPopup(self)
        quixel_options.addOption("Homepage", partial(self.setUrl, "https://www.fab.com/sellers/Quixel?ui_filters=0", self.on_url_finished))
        quixel_options.addOption("Surfaces", partial(self.setUrl, "https://www.fab.com/sellers/Quixel?listing_types=material&ui_filters=0", self.on_url_finished))
        quixel_options.addOption("3D Assets", partial(self.setUrl, "https://www.fab.com/sellers/Quixel?listing_types=3d-model&ui_filters=0", self.on_url_finished))
        quixel_options.addOption("3D Plants", partial(self.setUrl, "https://www.fab.com/sellers/Quixel?listing_types=3d-model&q=plant&ui_filters=0", self.on_url_finished))
        quixel_options.addOption("Decals", partial(self.setUrl, "https://www.fab.com/sellers/Quixel?listing_types=decal&ui_filters=0", self.on_url_finished))
        quixel_options.addOption("Atlases", partial(self.setUrl, "https://www.fab.com/sellers/Quixel?listing_types=atlas&ui_filters=0", self.on_url_finished))

        quixel_options.showAtWidget(self.quixel_categories_w)

    def on_url_finished(self, url: str):
        self.__fab_store.open_search_input()
    
    def open_search_input(self):
        return # TODO: This is not practical as long as the cookies banner also shows up :/
        self.__fab_store.open_search_input()

    def showNavigationControls(self, show: bool):
        """Show/hide all navigation related controls"""
        self.previous_page_w.setVisible(show)
        self.forward_page_w.setVisible(show)
        self.quixel_categories_w.setVisible(show)

    def resizeEvent(self, event: QtGui.QResizeEvent):
        self.__fab_store.resize(self.width() - 2, self.height() - 2)
        super().resizeEvent(event)

    def setUrl(self, url: str, callback: Optional[Callable[['QUrl'], None]] = None):
        self.__fab_store.setUrl(url)
        if callback:
            self.__fab_store.set_url_loaded_task(url, callback)
        self.__fab_store.resize(self.width() - 2, self.height() - 2)

    def onDownloadProgress(self, progress_item: QWebEngineDownloadRequest):
        item_name = progress_item.downloadFileName()

        if "thumbnail" in item_name:  # No need to show this in the progress
            return

        asset_identifier = os.path.basename(progress_item.downloadDirectory())

        # Create/update the download asset in the queue
        dependency_download_asset = DownloadQueue.getQueueItem(item_name)
        if dependency_download_asset is None:
            dependency_download_asset = FabDownloadingItem(item_name, asset_identifier)

        progress = int((progress_item.receivedBytes() / progress_item.totalBytes()) * 100)

        DownloadQueue.updateProgress(dependency_download_asset, progress, "Downloading")
        if progress == 100:
            DownloadQueue.updateProgress(dependency_download_asset, progress, "Finished")

    def processDownloadQueue(self, old_download):
        self.downloading = False

        if self.download_queue:
            logger.debug("Process next item in queue")
            download_url = self.download_queue.pop()
            self.__fab_store.page().download(download_url, "")  # generate a new download request
        else:
            logger.debug("Nothing else in the queue")

    def onItemDownloadRequested(self, download_item: QWebEngineDownloadRequest):
        logger.debug(f"Download requested {download_item.downloadFileName()} {download_item.downloadDirectory()}")

        if self.downloading:
            self.download_queue.append(download_item.url())
            logger.debug("Download in progress.. queue")
            return

        # Type guards
        if "thumbnail" in download_item.downloadFileName():
            self.downloading = True
            download_item.accept()
            logger.debug("Download thumbnail")
            return

        if ".zip" not in download_item.downloadFileName():
            download_item.cancel()
            logger.info(f"Download not supported for: {download_item.downloadFileName()}")
            return

        # Path generation
        identifier = self.__fab_store.url().toString().split("/")[-1]
        download_dir = os.path.join(FabAsset.getRootFolder(), identifier)
        image_path = os.path.join(download_dir, "thumbnail.jpg")
        metadata_path = os.path.join(download_dir, "metadata.json")
        os.makedirs(download_dir, exist_ok=True)

        # We download the asset data with a particular order
        first_download = False
        download_url = download_item.url()

        def on_data_ready(data: FabData):
            """Asset metadata generation function and related post actions"""
            data_to_cache(data, download_dir)
            logger.debug(f"Saved metadata for {identifier} to {download_dir}")

            # When we first download an item, we will need to call the actual download here, AFTER the data creation
            if first_download:
                logger.debug(f"First download for {data.asset_name}, metadata ready, download asset will start")
                self.__fab_store.page().download(download_url, "")  # generate a new download request

        # Use we need both metadata and thumbnail to consider this asset not needing metadata actions
        if not os.path.exists(image_path) or not os.path.exists(metadata_path):
            first_download = True
            logger.debug(f"Thumbnail requested for {identifier} to {download_dir}")
            html_to_fab_data(self.__fab_store, identifier, image_path, on_data_ready, world_id=2)

        # Proceed in downloading the asset right away ONLY if we already have the metadata
        # otherwise the download will be called AFTER the metadata is ready
        if not first_download:
            logger.debug(f"Asset data already present for {identifier}, download asset will start right away")
            download_item.setDownloadDirectory(download_dir)
            download_full_path = os.path.join(download_dir, download_item.downloadFileName())

            # Since we delete the zip files, we can't detect if the exact asset is already present
            # the data is extracted with some suffixes for allowing multiple 3d formats to co-exist
            # we can though, with reasonable success, see if the zip name is a substring of any folder, and at
            # least prompt the user with an action
            destination_directories = [dir_name for dir_name in os.listdir(download_dir) if os.path.isdir(os.path.join(download_dir, dir_name))]
            file_name_no_ext = download_item.downloadFileName().split(".")[0]

            can_download = True

            for dir_available in destination_directories:

                if file_name_no_ext in dir_available:
                    can_download = False

                    def on_special_button_click():
                        asset_identifier = os.path.basename(download_item.downloadDirectory())
                        self.revealDownloadedAsset.emit(asset_identifier)

                    message = f"Data already available {download_item.downloadFileName()}"
                    logger.info(message)

                    warning = PopupWidget()
                    warning.setWindowTitle("Asset Data Available")
                    warning.setMessage(message)

                    warning.addButton("Download", role=PopupWidget.AcceptRole)

                    special_button = warning.addButton("Show Asset", role=PopupWidget.HelpRole)
                    special_button.clicked.connect(on_special_button_click)

                    warning.addButton("Cancel", role=PopupWidget.RejectRole)

                    warning.exec_()

                    if warning.clickedButtonRole() == PopupWidget.AcceptRole:
                        can_download = True

            if can_download:
                self.downloading = True
                download_item.accept()
                self.assetDownloadStarted.emit(download_item.downloadFileName())
                download_full_path = os.path.join(download_dir, download_item.downloadFileName())
                download_item.isFinishedChanged.connect(partial(self.__onAssetDownloadFinished, download_full_path))
                logger.info(f"Download asset: {download_item.downloadFileName()} {download_item.downloadDirectory()}")

    def __onAssetDownloadFinished(self, download_full_path: str):
        self.assetDownloadFinished.emit(download_full_path)
        logger.info(f"Download finished {download_full_path} ready to unpack")
        unpack_task = UnpackTask(download_full_path)
        unpack_task.signals.unpackFinished.connect(self.__onAssetDownloadExtracted, QtCore.Qt.ConnectionType.QueuedConnection)

        QtCore.QThreadPool.globalInstance().start(unpack_task)

    def __onAssetDownloadExtracted(self, extract_full_path: str):
        self.assetDataExtracted.emit(extract_full_path)

    def __onUrlChanged(self, url: 'QUrl'):
        self.__fab_store.resize(self.width() - 2, self.height() - 2)

    # Expose the necessary signals and methods from __fab_store
    @property
    def urlChanged(self):
        return self.__fab_store.urlChanged

    def back(self):
        return self.__fab_store.back()

    def forward(self):
        return self.__fab_store.forward()

    def page(self):
        return self.__fab_store.page()


class FabLibraryWidget(UnrealAssetWidgetAndTree):
    """
    This is the main widget displayed inside the tab.
    You don't need to extend it, unless you want to show custom buttons or menus.
    """
    LIBRARY_NAME = "Fab"
    LIBRARY_ID = "fab"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Allow to filter the view by vendors, with a megascan button
        self._quixel_filter = BaseIconWidget(icon="megascans", size=24, out_opacity=0.5, in_opacity=1.0)
        self._quixel_filter.setToolTip("Filter the visibility to show only megascans assets")
        self._quixel_filter.clicked.connect(self.__toggleQuixel)
        self.search_layout.insertWidget(2, self._quixel_filter)
        self._filter_quixel = False

        # Initialize web panel
        if WITH_WEB_VIEW:
            self.__fab_widget = FabWebPanel(parent=self)
            self.__fab_widget.setVisible(False)
            self.__fab_widget.assetDownloadStarted.connect(self.__onWebDownloadStarted)
            self.__fab_widget.assetDownloadFinished.connect(self.__onWebDownloadEnded)
            self.__fab_widget.assetDataExtracted.connect(self.__onWebDownloadExtracted)
            self.__fab_widget.revealDownloadedAsset.connect(self.__onWebDownloadFocusRequest)

            # Add all buttons to message layout in correct order
            self.message_layout.insertWidget(0, self.__fab_widget.previous_page_w)
            self.message_layout.insertWidget(1, self.__fab_widget.forward_page_w)
            self.message_layout.insertWidget(2, self.__fab_widget.quixel_categories_w)
            # self.message_layout.addWidget(self._fab_website_w)

        # Create containers and setup layouts
        self.web_container = QtWidgets.QWidget()
        self.web_layout = QtWidgets.QVBoxLayout(self.web_container)
        self.web_layout.setContentsMargins(0, 0, 0, 0)
        self.web_layout.setSpacing(0)
        
        # Add web panel to container
        if WITH_WEB_VIEW:
            self.web_layout.addWidget(self.__fab_widget)
        
        self.library_container = QtWidgets.QWidget()
        self.library_layout = QtWidgets.QVBoxLayout(self.library_container)
        self.library_layout.setContentsMargins(0, 0, 0, 0)
        self.library_layout.setSpacing(0)
        
        # Setup splitter replacement
        self.splitter.replaceWidget(1, self.library_container)
        self.library_layout.addWidget(self.library_widget)
        self.library_layout.addWidget(self.web_container)

        self._url_set = False

    def deleteLater(self):
        if WITH_WEB_VIEW:
            self.__fab_widget.deleteLater()
        super().deleteLater()

    def showEvent(self, event):
        super().showEvent(event)
        logger.debug("Showing Fab library, check for new downloads")
        self.model.loader.setSyncValue(True)
        self.model.loader.startSync()

    def hideEvent(self, event):
        super().hideEvent(event)
        logger.debug("Hiding Fab library, stop checking for new downloads")
        self.model.loader.setSyncValue(False)

    def __manualSync(self):
        self.model.loader.sync_obj.can_sync = True
        self.model.loader.sync_obj.syncCaches()

    def __onWebDownloadStarted(self, asset_name):
        logger.debug(f"Download started {asset_name}")
        self.model.loader.download_in_progress = True

    def __onWebDownloadEnded(self, asset_name):
        logger.debug(f"Download ended {asset_name}")

    def __onWebDownloadExtracted(self, extract_full_path):
        logger.debug(f"Download extracted {extract_full_path}")
        self.model.loader.download_in_progress = False
        self.model.loader.onDownloadExtracted()

    def __onWebDownloadFocusRequest(self, asset_identifier: str):
        asset_instance = ContentLibraryAsset.instances_cache.get(asset_identifier)
        if asset_instance:
            self.focusToAsset(asset_instance)
        else:
            logger.error(f"Unable to find {asset_identifier}")

    def __toggleQuixel(self):
        """Filter asset by quixel vendor"""
        if not self._filter_quixel:
            self._quixel_filter.changeIcon("megascans", out_opacity=1.0, in_opacity=0.8)
            self.proxyModel.setVendorFilter({"Quixel"})
            self._filter_quixel = True
        else:
            self._quixel_filter.changeIcon("megascans", out_opacity=1.0, in_opacity=0.5)
            self.proxyModel.setVendorFilter(set())
            self._filter_quixel = False

    def __toggleQuixelStore(self):
        # This ensures we can switch back and forth between the library and the fab store without clearing the url
        if WITH_WEB_VIEW:
            if not self._url_set:
                self.__fab_widget.setUrl("https://www.fab.com/sellers/Quixel?ui_filters=0", self.__fab_widget.on_url_finished)
                self._url_set = True

            if self.library_widget.isVisible():
                self.library_widget.setVisible(False)
                self.__fab_widget.setVisible(True)
                self.__fab_widget.showNavigationControls(True)
                l_size = self.web_layout.geometry().size()
                self.__fab_widget.resize(l_size)
                self.__fab_widget.update()
            else:
                self.library_widget.setVisible(True)
                self.__fab_widget.setVisible(False)
                self.__fab_widget.showNavigationControls(False)
                self.__fab_widget.open_search_input()
        self.updateHeaderMessage()

    def __openAssetCacheFolder(self):
        selected_assets = self.library_widget.getSelectedAssetObjets()
        for asset_object in selected_assets:  # type: FabAsset
            if asset_object.current_asset_dir:
                if os.path.exists(asset_object.current_asset_dir):
                    os.startfile(asset_object.current_asset_dir)

    def getQuickAccessIcon(self) -> str:
        return "fablibrary"

    def hasLibraryHeaderMessage(self) -> bool:
        return True

    def getLibraryHeaderMessage(self) -> Tuple[str, str, bool]:
        button_label = "Open Fab"
        if WITH_WEB_VIEW:
            if self.__fab_widget.isVisible():
                button_label = "Close Fab"
            else:
                button_label = "Open Fab"

        return "Unofficial Fab/Quixel Integration in Dash", button_label, True

    def getEmptyLibraryMessage(self) -> str:
        return "No assets downloaded in the Dash library for Fab"

    def runLibraryHeaderCallToAction(self):
        if WITH_WEB_VIEW:
            self.__toggleQuixelStore()
        else:
            unreal.DashLib.invoke_tab_by_name("FabTab")

    def openAssetInFab(self):
        if WITH_WEB_VIEW:
            selected = self.library_widget.getSelectedAssetObjets()
            if selected:
                first = selected[0]
                self.__fab_widget.setUrl(f"https://www.fab.com/listings/{first.identifier}", self.__fab_widget.on_url_finished)
                self._url_set = True
                self.__toggleQuixelStore()
            else:
                logger.warning("No asset selected to open in Fab")

    def getContextMenuEntries(self) -> list[ContextMenuEntry]:
        items = super().getContextMenuEntries()
        items.append(ContextMenuEntry(name="Open Cache Folder", callback=self.__openAssetCacheFolder))
        if WITH_WEB_VIEW:
            items.append(ContextMenuEntry(name="Open in Fab", callback=self.openAssetInFab))
        return items

    def getWidgetMenuEntries(self) -> list[WidgetMenuEntry or WidgetMenuSpacer]:
        base_entries = super().getWidgetMenuEntries()

        if DEV_MODE:
            base_entries.append(WidgetMenuEntry("DEV - Call Model Update", self.model.updateData, "Update the data"))

        base_entries.append(WidgetMenuSpacer())
        base_entries.append(WidgetMenuEntry("Set Fab Folders",
                                            open_project_paths_prefs,
                                            tooltip="Change the folder where the asset data is searched for"))
        base_entries.append(WidgetMenuEntry("Check New Assets",
                                            self.__manualSync,
                                            tooltip="Manually check if any new asset has been downloaded from FAB"))

        return base_entries

    def focusToAsset(self, asset_instance: ContentLibraryAsset):
        if WITH_WEB_VIEW:
            if not self.library_widget.isVisible():
                self.__toggleQuixelStore()

        super().focusToAsset(asset_instance)


# ------------------------------------------------- PREFERENCES ------------------------------------------------------ #
# Register a widget to edit the preferences data, and update the library if visible to reflect preferences changes
def refreshModels():
    fab_lib_widget = LIBRARIES_INSTANCES.get(FabLibraryWidget.LIBRARY_ID)
    if fab_lib_widget:
        fab_lib_widget.reloadData()


def acceptFabRootFolder():
    """When he user saves the the preferences, this function is called."""
    if Preferences.hasValueChanged(LIBRARY_PREFERENCES_GROUP, FAB_PREF_PATH_NAME):
        value = Preferences.getValue(LIBRARY_PREFERENCES_GROUP, FAB_PREF_PATH_NAME)
        FabAsset.setRootFolder(value, save=False)  # do not save if from the pref UI

        # Refresh the library window current view
        refreshModels()


def acceptFabCacheChange():
    if Preferences.hasValueChanged(LIBRARY_PREFERENCES_GROUP, FAB_UECACHE_PREF_PATH_NAME):
        value = Preferences.getValue(LIBRARY_PREFERENCES_GROUP, FAB_UECACHE_PREF_PATH_NAME)
        if not os.path.exists(value):
            logger.error(f"Cache path {value} does not exist")
            return

        # Edit the path in the loder
        fab_lib_widget = LIBRARIES_INSTANCES.get(FabLibraryWidget.LIBRARY_NAME)
        if fab_lib_widget:
            model = fab_lib_widget.model
            model.loader.sync_obj.changeEpicCachePath(value)
            model.loader.sync_obj.syncCaches()


def changeAutoSync(auto_sync: bool):
    fab_lib_widget = LIBRARIES_INSTANCES.get(FabLibraryWidget.LIBRARY_NAME)
    if fab_lib_widget:
        model = fab_lib_widget.model
        model.loader.setAutoSync(auto_sync)

        if fab_lib_widget.isVisible() and auto_sync is True:
            model.loader.startSync()


PREF_GROUP_UI = SettingGroup(name=PREF_GROUP_UI_STR,
                             description="Fab library preferences",
                             collapsed=False)
registerGroup(PREF_GROUP_UI, position=2)

fab_path_setting_data = SettingData(name=FAB_PREF_PATH_NAME,
                                    group=LIBRARY_PREFERENCES_GROUP,

                                    ui_name=FAB_PREF_PATH_NAME_UI_STR,
                                    description="Where download and store the library data",
                                    ui_group=PREF_GROUP_UI.name,
                                    type=SettingType.FolderPath,
                                    default_value=FabAsset.getRootFolder(),
                                    on_pre_data_saved=acceptFabRootFolder,
                                    )

fab_cache_path_setting_data = SettingData(name=FAB_UECACHE_PREF_PATH_NAME,
                                          group=LIBRARY_PREFERENCES_GROUP,

                                          ui_name=FAB_UECACHE_PREF_PATH_NAME_UI_STR,
                                          description="Where the Epic Fab cache is saved",
                                          ui_group=PREF_GROUP_UI.name,
                                          type=SettingType.FolderPath,
                                          default_value=CACHE_DEFAULT_VALUE,
                                          on_pre_data_saved=acceptFabCacheChange,
                                          )

fab_auto_sync_with_plugin = SettingData(name=FAB_PREF_AUTOSYNC_NAME,
                                        group=LIBRARY_PREFERENCES_GROUP,

                                        ui_name=FAB_PREF_AUTOSYNC_NAME_UI_STR,
                                        ui_group=PREF_GROUP_UI.name,
                                        description="Automatically add in the Dash Fab data assets from Epic Fab plugin",
                                        type=SettingType.Boolean,
                                        default_value=CACHE_AUTO_SYNC,
                                        on_pre_data_saved=changeAutoSync,
                                        )

registerSettings(fab_path_setting_data)
registerSettings(fab_cache_path_setting_data)
# registerSettings(fab_auto_sync_with_plugin)

if __name__ == "__main__":
    store = FabWebPanel()
    # store = FabView()
    store.setUrl("https://www.fab.com/listings/f3babfd2-cc5d-4f26-9462-b006176f775c")
    store.show()

    # asset = FabAsset.createFromIdentifier("7890adb8-3e51-44dd-9357-f169c1961b81")
    # asset.acquireResources()
    # print(asset.getCurrentSourceFolder())
    # current_source = asset.getCurrentSourceFolder()

