using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Enum;
using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.TextureCompression;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using OtterTex;
using System.Buffers.Binary;
using System.Globalization;
using System.Numerics;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using ImageSharpImage = SixLabors.ImageSharp.Image;

namespace LightlessSync.UI;

public class DataAnalysisUi : WindowMediatorSubscriberBase
{
    private const float MinTextureFilterPaneWidth = 305f;
    private const float MaxTextureFilterPaneWidth = 405f;
    private const float MinTextureDetailPaneWidth = 480f;
    private const float MaxTextureDetailPaneWidth = 720f;
    private const float TextureFilterSplitterWidth = 8f;
    private const float TextureDetailSplitterWidth = 12f;
    private const float TextureDetailSplitterCollapsedWidth = 18f;
    private const float SelectedFilePanelLogicalHeight = 90f;
    private static readonly Vector4 SelectedTextureRowTextColor = new(0f, 0f, 0f, 1f);

    private readonly CharacterAnalyzer _characterAnalyzer;
    private readonly Progress<TextureConversionProgress> _conversionProgress = new();
    private readonly IpcManager _ipcManager;
    private readonly UiSharedService _uiSharedService;
    private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
    private readonly TransientResourceManager _transientResourceManager;
    private readonly TransientConfigService _transientConfigService;
    private readonly TextureCompressionService _textureCompressionService;
    private readonly TextureMetadataHelper _textureMetadataHelper;

    private readonly List<TextureRow> _textureRows = new();
    private readonly Dictionary<string, TextureCompressionTarget> _textureSelections = new(StringComparer.OrdinalIgnoreCase);
    private readonly HashSet<string> _selectedTextureKeys = new(StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<string, TexturePreviewState> _texturePreviews = new(StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<string, TextureResolutionInfo?> _textureResolutionCache = new(StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<ObjectKind, TextureWorkspaceTab> _textureWorkspaceTabs = new();
    private readonly List<string> _storedPathsToRemove = [];
    private readonly Dictionary<string, string> _filePathResolve = [];

    private Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>? _cachedAnalysis;
    private CancellationTokenSource _conversionCancellationTokenSource = new();
    private CancellationTokenSource _transientRecordCts = new();

    private Task? _conversionTask;
    private TextureConversionProgress? _lastConversionProgress;

    private float _textureFilterPaneWidth = 320f;
    private float _textureDetailPaneWidth = 360f;
    private float _textureDetailHeight = 360f;
    private float _texturePreviewSize = 360f;

    private string _conversionCurrentFileName = string.Empty;
    private string _selectedFileTypeTab = string.Empty;
    private string _selectedHash = string.Empty;
    private string _textureSearch = string.Empty;
    private string _textureSlotFilter = "All";
    private string _selectedTextureKey = string.Empty;
    private string _selectedStoredCharacter = string.Empty;
    private string _selectedJobEntry = string.Empty;
    private string _filterGamePath = string.Empty;
    private string _filterFilePath = string.Empty;

    private int _conversionCurrentFileProgress = 0;
    private int _conversionTotalJobs;

    private bool _hasUpdate = false;
    private bool _modalOpen = false;
    private bool _showModal = false;
    private bool _textureRowsDirty = true;
    private bool _textureDetailCollapsed = false;
    private bool _conversionFailed;
    private bool _showAlreadyAddedTransients = false;
    private bool _acknowledgeReview = false;

    private Task<TextureRowBuildResult>? _textureRowsBuildTask;
    private CancellationTokenSource? _textureRowsBuildCts;

    private ObjectKind _selectedObjectTab;

    private TextureUsageCategory? _textureCategoryFilter = null;
    private TextureMapKind? _textureMapFilter = null;
    private TextureCompressionTarget? _textureTargetFilter = null;

    public DataAnalysisUi(ILogger<DataAnalysisUi> logger, LightlessMediator mediator,
        CharacterAnalyzer characterAnalyzer, IpcManager ipcManager,
        PerformanceCollectorService performanceCollectorService, UiSharedService uiSharedService,
        PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager,
        TransientConfigService transientConfigService, TextureCompressionService textureCompressionService,
        TextureMetadataHelper textureMetadataHelper)
        : base(logger, mediator, "Lightless Character Data Analysis", performanceCollectorService)
    {
        _characterAnalyzer = characterAnalyzer;
        _ipcManager = ipcManager;
        _uiSharedService = uiSharedService;
        _playerPerformanceConfig = playerPerformanceConfig;
        _transientResourceManager = transientResourceManager;
        _transientConfigService = transientConfigService;
        _textureCompressionService = textureCompressionService;
        _textureMetadataHelper = textureMetadataHelper;
        Mediator.Subscribe<CharacterDataAnalyzedMessage>(this, (_) =>
        {
            _hasUpdate = true;
        });
        WindowBuilder.For(this)
            .SetSizeConstraints(new Vector2(1240, 680), new Vector2(3840, 2160))
            .Apply();

        _conversionProgress.ProgressChanged += ConversionProgress_ProgressChanged;
    }

    protected override void DrawInternal()
    {
        HandleConversionModal();
        RefreshAnalysisCache();
        DrawContentTabs();
    }

    private void HandleConversionModal()
    {
        if (_conversionTask == null)
        {
            return;
        }

        if (_conversionTask.IsCompleted)
        {
            ResetConversionModalState();
            return;
        }

        _showModal = true;
        if (ImGui.BeginPopupModal("Texture Compression in Progress", ImGuiWindowFlags.AlwaysAutoResize))
        {
            DrawConversionModalContent();
            ImGui.EndPopup();
        }
        else
        {
            _modalOpen = false;
        }

        if (_showModal && !_modalOpen)
        {
            ImGui.OpenPopup("Texture Compression in Progress");
            _modalOpen = true;
        }
    }

    private void DrawConversionModalContent()
    {
        var progress = _lastConversionProgress;
        var total = progress?.Total ?? Math.Max(_conversionTotalJobs, 1);
        var completed = progress != null
            ? Math.Min(progress.Completed + 1, total)
            : _conversionCurrentFileProgress;
        var currentLabel = !string.IsNullOrEmpty(_conversionCurrentFileName)
            ? _conversionCurrentFileName
            : "Preparing...";

        ImGui.TextUnformatted($"Compressing textures ({completed}/{total})");
        UiSharedService.TextWrapped("Current file: " + currentLabel);

        if (_conversionFailed)
        {
            UiSharedService.ColorText("Conversion encountered errors. Please review the log for details.", ImGuiColors.DalamudRed);
        }

        if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel conversion"))
        {
            _conversionCancellationTokenSource.Cancel();
        }

        UiSharedService.SetScaledWindowSize(520);
    }

    private void ResetConversionModalState()
    {
        _conversionTask = null;
        _showModal = false;
        _modalOpen = false;
        _lastConversionProgress = null;
        _conversionCurrentFileName = string.Empty;
        _conversionCurrentFileProgress = 0;
        _conversionTotalJobs = 0;
    }

    private void RefreshAnalysisCache()
    {
        if (!_hasUpdate)
        {
            return;
        }

        _cachedAnalysis = CloneAnalysis(_characterAnalyzer.LastAnalysis);
        _hasUpdate = false;
        InvalidateTextureRows();
    }

    private void DrawContentTabs()
    {
        using var tabBar = ImRaii.TabBar("analysisRecordingTabBar");
        DrawAnalysisTab();
        DrawTransientFilesTab();
    }

    private void DrawAnalysisTab()
    {
        using var tabItem = ImRaii.TabItem("Analysis");
        if (!tabItem)
        {
            return;
        }

        using var id = ImRaii.PushId("analysis");
        DrawAnalysis();
    }

    private void DrawTransientFilesTab()
    {
        using var tabItem = ImRaii.TabItem("Transient Files");
        if (!tabItem)
        {
            return;
        }

        using var tabbar = ImRaii.TabBar("transientData");

        using (var transientData = ImRaii.TabItem("Stored Transient File Data"))
        {
            using var id = ImRaii.PushId("data");
            if (transientData)
            {
                DrawStoredData();
            }
        }

        using (var transientRecord = ImRaii.TabItem("Record Transient Data"))
        {
            using var id = ImRaii.PushId("recording");
            if (transientRecord)
            {
                DrawRecording();
            }
        }
    }

    private void DrawStoredData()
    {
        UiSharedService.DrawTree("What is this? (Explanation / Help)", () =>
        {
            UiSharedService.TextWrapped("This tab allows you to see which transient files are attached to your character.");
            UiSharedService.TextWrapped("Transient files are files that cannot be resolved to your character permanently. Lightless gathers these files in the background while you execute animations, VFX, sound effects, etc.");
            UiSharedService.TextWrapped("When sending your character data to others, Lightless will combine the files listed in \"All Jobs\" and the corresponding currently used job.");
            UiSharedService.TextWrapped("The purpose of this tab is primarily informational for you to see which files you are carrying with you. You can remove added game paths, however if you are using the animations etc. again, "
                + "Lightless will automatically attach these after using them. If you disable associated mods in Penumbra, the associated entries here will also be deleted automatically.");
        });

        ImGuiHelpers.ScaledDummy(5);

        var config = _transientConfigService.Current.TransientConfigs;
        Vector2 availableContentRegion = Vector2.Zero;
        DrawCharacterColumn();

        ImGui.SameLine();

        bool selectedData = config.TryGetValue(_selectedStoredCharacter, out var transientStorage) && transientStorage != null;
        DrawJobColumn();

        ImGui.SameLine();
        DrawAttachedFilesColumn();

        return;

        void DrawCharacterColumn()
        {
            using (ImRaii.Group())
            {
                ImGui.TextUnformatted("Character");
                ImGui.Separator();
                ImGuiHelpers.ScaledDummy(3);
                availableContentRegion = ImGui.GetContentRegionAvail();
                using (ImRaii.ListBox("##characters", new Vector2(200, availableContentRegion.Y)))
                {
                    foreach (var entry in config)
                    {
                        var name = entry.Key.Split("_");
                        if (!_uiSharedService.WorldData.TryGetValue(ushort.Parse(name[1]), out var worldname))
                        {
                            continue;
                        }

                        bool isSelected = string.Equals(_selectedStoredCharacter, entry.Key, StringComparison.Ordinal);
                        if (ImGui.Selectable(name[0] + " (" + worldname + ")", isSelected))
                        {
                            _selectedStoredCharacter = entry.Key;
                            _selectedJobEntry = string.Empty;
                            ResetSelectionFilters();
                        }
                    }
                }
            }
        }

        void DrawJobColumn()
        {
            using (ImRaii.Group())
            {
                ImGui.TextUnformatted("Job");
                ImGui.Separator();
                ImGuiHelpers.ScaledDummy(3);
                using (ImRaii.ListBox("##data", new Vector2(150, availableContentRegion.Y)))
                {
                    if (!selectedData)
                    {
                        return;
                    }

                    if (ImGui.Selectable("All Jobs", string.Equals(_selectedJobEntry, "alljobs", StringComparison.Ordinal)))
                    {
                        _selectedJobEntry = "alljobs";
                    }

                    foreach (var job in transientStorage!.JobSpecificCache)
                    {
                        if (!_uiSharedService.JobData.TryGetValue(job.Key, out var jobName))
                        {
                            continue;
                        }

                        if (ImGui.Selectable(jobName, string.Equals(_selectedJobEntry, job.Key.ToString(), StringComparison.Ordinal)))
                        {
                            _selectedJobEntry = job.Key.ToString();
                            ResetSelectionFilters();
                        }
                    }
                }
            }
        }

        void DrawAttachedFilesColumn()
        {
            using (ImRaii.Group())
            {
                var selectedList = string.Equals(_selectedJobEntry, "alljobs", StringComparison.Ordinal)
                    ? config[_selectedStoredCharacter].GlobalPersistentCache
                    : (string.IsNullOrEmpty(_selectedJobEntry) ? [] : config[_selectedStoredCharacter].JobSpecificCache[uint.Parse(_selectedJobEntry)]);
                ImGui.TextUnformatted($"Attached Files (Total Files: {selectedList.Count})");
                ImGui.Separator();
                ImGuiHelpers.ScaledDummy(3);
                using (ImRaii.Disabled(string.IsNullOrEmpty(_selectedJobEntry)))
                {
                    var restContent = availableContentRegion.X - ImGui.GetCursorPosX();
                    using var group = ImRaii.Group();
                    if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Resolve Game Paths to used File Paths"))
                    {
                        _ = Task.Run(async () =>
                        {
                            if (!_ipcManager.Penumbra.APIAvailable)
                            {
                                return;
                            }

                            var paths = selectedList.ToArray();
                            var (forward, _) = await _ipcManager.Penumbra.ResolvePathsAsync(paths, Array.Empty<string>()).ConfigureAwait(false);
                            for (int i = 0; i < paths.Length && i < forward.Length; i++)
                            {
                                var result = forward[i];
                                if (string.IsNullOrEmpty(result))
                                {
                                    continue;
                                }

                                if (!_filePathResolve.TryAdd(paths[i], result))
                                {
                                    _filePathResolve[paths[i]] = result;
                                }
                            }
                        });
                    }
                    ImGui.SameLine();
                    if (_uiSharedService.IconTextButton(FontAwesomeIcon.Eraser, "Clear Game Path File Resolves"))
                    {
                        _filePathResolve.Clear();
                    }
                    ImGui.SameLine();
                    using (ImRaii.Disabled(!_storedPathsToRemove.Any()))
                    {
                        if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Selected Game Paths"))
                        {
                            foreach (var entry in _storedPathsToRemove)
                            {
                                config[_selectedStoredCharacter].GlobalPersistentCache.Remove(entry);
                                foreach (var job in config[_selectedStoredCharacter].JobSpecificCache)
                                {
                                    job.Value.Remove(entry);
                                }
                            }

                            _storedPathsToRemove.Clear();
                            _transientConfigService.Save();
                            _transientResourceManager.RebuildSemiTransientResources();
                            _filterFilePath = string.Empty;
                            _filterGamePath = string.Empty;
                        }
                    }
                    ImGui.SameLine();
                    using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
                    {
                        if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear ALL Game Paths"))
                        {
                            selectedList.Clear();
                            _transientConfigService.Save();
                            _transientResourceManager.RebuildSemiTransientResources();
                            _filterFilePath = string.Empty;
                            _filterGamePath = string.Empty;
                        }
                    }
                    UiSharedService.AttachToolTip("Hold CTRL to delete all game paths from the displayed list"
                        + UiSharedService.TooltipSeparator + "You usually do not need to do this. All animation and VFX data will be automatically handled through Lightless.");
                    ImGuiHelpers.ScaledDummy(5);
                    ImGuiHelpers.ScaledDummy(30);
                    ImGui.SameLine();
                    ImGui.SetNextItemWidth((restContent - 30) / 2f);
                    ImGui.InputTextWithHint("##filterGamePath", "Filter by Game Path", ref _filterGamePath, 255);
                    ImGui.SameLine();
                    ImGui.SetNextItemWidth((restContent - 30) / 2f);
                    ImGui.InputTextWithHint("##filterFilePath", "Filter by File Path", ref _filterFilePath, 255);

                    using (var dataTable = ImRaii.Table("##table", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg))
                    {
                        if (dataTable)
                        {
                            ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 30);
                            ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthFixed, (restContent - 30) / 2f);
                            ImGui.TableSetupColumn("File Path", ImGuiTableColumnFlags.WidthFixed, (restContent - 30) / 2f);
                            ImGui.TableSetupScrollFreeze(0, 1);
                            ImGui.TableHeadersRow();
                            int id = 0;
                            foreach (var entry in selectedList)
                            {
                                if (!string.IsNullOrWhiteSpace(_filterGamePath) && !entry.Contains(_filterGamePath, StringComparison.OrdinalIgnoreCase))
                                    continue;
                                bool hasFileResolve = _filePathResolve.TryGetValue(entry, out var filePath);

                                if (hasFileResolve && !string.IsNullOrEmpty(_filterFilePath) && !filePath!.Contains(_filterFilePath, StringComparison.OrdinalIgnoreCase))
                                    continue;

                                using var imguiid = ImRaii.PushId(id++);
                                ImGui.TableNextColumn();
                                bool isSelected = _storedPathsToRemove.Contains(entry, StringComparer.Ordinal);
                                if (ImGui.Checkbox("##", ref isSelected))
                                {
                                    if (isSelected)
                                        _storedPathsToRemove.Add(entry);
                                    else
                                        _storedPathsToRemove.Remove(entry);
                                }
                                ImGui.TableNextColumn();
                                ImGui.TextUnformatted(entry);
                                UiSharedService.AttachToolTip(entry + UiSharedService.TooltipSeparator + "Click to copy to clipboard");
                                if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
                                {
                                    ImGui.SetClipboardText(entry);
                                }
                                ImGui.TableNextColumn();
                                if (hasFileResolve)
                                {
                                    ImGui.TextUnformatted(filePath ?? "Unk");
                                    UiSharedService.AttachToolTip(filePath ?? "Unk" + UiSharedService.TooltipSeparator + "Click to copy to clipboard");
                                    if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
                                    {
                                        ImGui.SetClipboardText(filePath);
                                    }
                                }
                                else
                                {
                                    ImGui.TextUnformatted("-");
                                    UiSharedService.AttachToolTip("Resolve Game Paths to used File Paths to display the associated file paths.");
                                }
                            }
                        }
                    }
                }
            }
        }

        void ResetSelectionFilters()
        {
            _storedPathsToRemove.Clear();
            _filePathResolve.Clear();
            _filterFilePath = string.Empty;
            _filterGamePath = string.Empty;
        }
    }


    private void DrawRecording()
    {
        DrawRecordingHelpSection();
        DrawRecordingControlButtons();

        if (_transientResourceManager.IsTransientRecording)
        {
            DrawRecordingActiveWarning();
        }

        ImGuiHelpers.ScaledDummy(5);
        DrawRecordingReviewControls();
        ImGuiHelpers.ScaledDummy(5);
        DrawRecordedTransientsTable();
    }

    private static void DrawRecordingHelpSection()
    {
        UiSharedService.DrawTree("What is this? (Explanation / Help)", () =>
        {
            UiSharedService.TextWrapped("This tab allows you to attempt to fix mods that do not sync correctly, especially those with modded models and animations." + Environment.NewLine + Environment.NewLine
                + "To use this, start the recording, execute one or multiple emotes/animations you want to attempt to fix and check if new data appears in the table below." + Environment.NewLine
                + "If it doesn't, Lightless is not able to catch the data or already has recorded the animation files (check 'Show previously added transient files' to see if not all is already present)." + Environment.NewLine + Environment.NewLine
                + "For most animations, vfx, etc. it is enough to just run them once unless they have random variations. Longer animations do not require to play out in their entirety to be captured.");
            ImGuiHelpers.ScaledDummy(5);
            UiSharedService.DrawGroupedCenteredColorText("Important Note: If you need to fix an animation that should apply across multiple jobs, you need to repeat this process with at least one additional job, " +
                "otherwise the animation will only be fixed for the currently active job. This goes primarily for emotes that are used across multiple jobs.",
                UIColors.Get("LightlessYellow"), 800);
            ImGuiHelpers.ScaledDummy(5);
            UiSharedService.DrawGroupedCenteredColorText("WARNING: WHILE RECORDING TRANSIENT DATA, DO NOT CHANGE YOUR APPEARANCE, ENABLED MODS OR ANYTHING. JUST DO THE ANIMATION(S) OR WHATEVER YOU NEED DOING AND STOP THE RECORDING.",
                ImGuiColors.DalamudRed, 800);
            ImGuiHelpers.ScaledDummy(5);
        });
    }

    private void DrawRecordingControlButtons()
    {
        using (ImRaii.Disabled(_transientResourceManager.IsTransientRecording))
        {
            if (_uiSharedService.IconTextButton(FontAwesomeIcon.Play, "Start Transient Recording"))
            {
                _transientRecordCts.Cancel();
                _transientRecordCts.Dispose();
                _transientRecordCts = new();
                _transientResourceManager.StartRecording(_transientRecordCts.Token);
                _acknowledgeReview = false;
            }
        }
        ImGui.SameLine();
        using (ImRaii.Disabled(!_transientResourceManager.IsTransientRecording))
        {
            if (_uiSharedService.IconTextButton(FontAwesomeIcon.Stop, "Stop Transient Recording"))
            {
                _transientRecordCts.Cancel();
            }
        }
    }

    private void DrawRecordingActiveWarning()
    {
        ImGui.SameLine();
        UiSharedService.ColorText($"RECORDING - Time Remaining: {_transientResourceManager.RecordTimeRemaining.Value}", UIColors.Get("LightlessYellow"));
        ImGuiHelpers.ScaledDummy(5);
        UiSharedService.DrawGroupedCenteredColorText("DO NOT CHANGE YOUR APPEARANCE OR MODS WHILE RECORDING, YOU CAN ACCIDENTALLY MAKE SOME OF YOUR APPEARANCE RELATED MODS PERMANENT.", ImGuiColors.DalamudRed, 800);
    }

    private void DrawRecordingReviewControls()
    {
        ImGui.Checkbox("Show previously added transient files in the recording", ref _showAlreadyAddedTransients);
        _uiSharedService.DrawHelpText("Use this only if you want to see what was previously already caught by Lightless");
        ImGuiHelpers.ScaledDummy(5);

        using (ImRaii.Disabled(_transientResourceManager.IsTransientRecording || _transientResourceManager.RecordedTransients.All(k => !k.AddTransient) || !_acknowledgeReview))
        {
            if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Recorded Transient Data"))
            {
                _transientResourceManager.SaveRecording();
                _acknowledgeReview = false;
            }
        }
        ImGui.SameLine();
        ImGui.Checkbox("I acknowledge I have reviewed the recorded data", ref _acknowledgeReview);
        if (_transientResourceManager.RecordedTransients.Any(k => !k.AlreadyTransient))
        {
            ImGuiHelpers.ScaledDummy(5);
            UiSharedService.DrawGroupedCenteredColorText("Please review the recorded mod files before saving and deselect files that got into the recording on accident.", UIColors.Get("LightlessYellow"));
            ImGuiHelpers.ScaledDummy(5);
        }
    }

    private void DrawRecordedTransientsTable()
    {
        var width = ImGui.GetContentRegionAvail();
        using var table = ImRaii.Table("Recorded Transients", 4, ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
        if (!table)
        {
            return;
        }

        int id = 0;
        ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 30);
        ImGui.TableSetupColumn("Owner", ImGuiTableColumnFlags.WidthFixed, 100);
        ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthFixed, (width.X - 30 - 100) / 2f);
        ImGui.TableSetupColumn("File Path", ImGuiTableColumnFlags.WidthFixed, (width.X - 30 - 100) / 2f);
        ImGui.TableSetupScrollFreeze(0, 1);
        ImGui.TableHeadersRow();
        var transients = _transientResourceManager.RecordedTransients.ToList();
        transients.Reverse();
        foreach (var value in transients)
        {
            if (value.AlreadyTransient && !_showAlreadyAddedTransients)
                continue;

            using var imguiid = ImRaii.PushId(id++);
            if (value.AlreadyTransient)
            {
                ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
            }
            ImGui.TableNextColumn();
            bool addTransient = value.AddTransient;
            if (ImGui.Checkbox("##add", ref addTransient))
            {
                value.AddTransient = addTransient;
            }
            ImGui.TableNextColumn();
            ImGui.TextUnformatted(value.Owner.Name);
            ImGui.TableNextColumn();
            ImGui.TextUnformatted(value.GamePath);
            UiSharedService.AttachToolTip(value.GamePath);
            ImGui.TableNextColumn();
            ImGui.TextUnformatted(value.FilePath);
            UiSharedService.AttachToolTip(value.FilePath);
            if (value.AlreadyTransient)
            {
                ImGui.PopStyleColor();
            }
        }
    }

    private void DrawAnalysis()
    {
        UiSharedService.DrawTree("What is this? (Explanation / Help)", () =>
        {
            UiSharedService.TextWrapped("This tab shows you all files and their sizes that are currently in use through your character and associated entities in Lightless");
        });

        if (_cachedAnalysis!.Count == 0) return;

        EnsureTextureRows();

        bool isAnalyzing = _characterAnalyzer.IsAnalysisRunning;
        if (isAnalyzing)
        {
            UiSharedService.ColorTextWrapped($"Analyzing {_characterAnalyzer.CurrentFile}/{_characterAnalyzer.TotalFiles}",
                UIColors.Get("LightlessYellow"));
            if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis"))
            {
                _characterAnalyzer.CancelAnalyze();
            }
        }
        else
        {
            if (_cachedAnalysis!.Any(c => c.Value.Any(f => !f.Value.IsComputed)))
            {
                UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to analyze your current data",
                    UIColors.Get("LightlessYellow"));
                if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (missing entries)"))
                {
                    _ = _characterAnalyzer.ComputeAnalysis(print: false);
                }
            }
            else
            {
                if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (recalculate all entries)"))
                {
                    _ = _characterAnalyzer.ComputeAnalysis(print: false, recalculate: true);
                }
            }
        }

        ImGui.Separator();

        var totalFileCount = _cachedAnalysis!.Values.Sum(c => c.Values.Count);
        var totalActualSize = _cachedAnalysis.Sum(c => c.Value.Sum(entry => entry.Value.OriginalSize));
        var totalCompressedSize = _cachedAnalysis.Sum(c => c.Value.Sum(entry => entry.Value.CompressedSize));
        var totalTriangles = _cachedAnalysis.Sum(c => c.Value.Sum(entry => entry.Value.Triangles));
        var breakdown = string.Join(Environment.NewLine,
            _cachedAnalysis.Values
                .SelectMany(f => f.Values)
                .GroupBy(f => f.FileType, StringComparer.Ordinal)
                .OrderBy(f => f.Key, StringComparer.Ordinal)
                .Select(f => $"{f.Key}: {f.Count()} files, size: {UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))}, compressed: {UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))}"));

        DrawAnalysisOverview(totalFileCount, totalActualSize, totalCompressedSize, totalTriangles, breakdown);

        using var tabbar = ImRaii.TabBar("objectSelection");
        foreach (var kvp in _cachedAnalysis)
        {
            using var id = ImRaii.PushId(kvp.Key.ToString());
            string tabText = kvp.Key.ToString();
            if (kvp.Value.Any(f => !f.Value.IsComputed)) tabText += " (!)";
            using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString());
            if (tab.Success)
            {
                var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal).OrderBy(k => k.Key, StringComparer.Ordinal).ToList();

                DrawObjectOverview(kvp.Key, kvp.Value, groupedfiles);

                if (_selectedObjectTab != kvp.Key)
                {
                    _selectedHash = string.Empty;
                    _selectedObjectTab = kvp.Key;
                    _selectedFileTypeTab = string.Empty;
                }

                var otherFileGroups = groupedfiles
                    .Where(g => !string.Equals(g.Key, "tex", StringComparison.Ordinal))
                    .ToList();

                if (!string.IsNullOrEmpty(_selectedFileTypeTab) &&
                    otherFileGroups.TrueForAll(g => !string.Equals(g.Key, _selectedFileTypeTab, StringComparison.Ordinal)))
                {
                    _selectedFileTypeTab = string.Empty;
                }

                if (string.IsNullOrEmpty(_selectedFileTypeTab) && otherFileGroups.Count > 0)
                {
                    _selectedFileTypeTab = otherFileGroups[0].Key;
                }

                DrawTextureWorkspace(kvp.Key, otherFileGroups);
            }
        }
    }

    public override void OnOpen()
    {
        _hasUpdate = true;
        _selectedHash = string.Empty;
        _selectedTextureKey = string.Empty;
        _selectedTextureKeys.Clear();
        _textureSelections.Clear();
        ResetTextureFilters();
        InvalidateTextureRows();
        _conversionFailed = false;
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        foreach (var preview in _texturePreviews.Values)
        {
            preview.Texture?.Dispose();
        }
        _texturePreviews.Clear();
        _textureRowsBuildCts?.Cancel();
        _textureRowsBuildCts?.Dispose();
        _conversionProgress.ProgressChanged -= ConversionProgress_ProgressChanged;
    }

    private void ConversionProgress_ProgressChanged(object? sender, TextureConversionProgress e)
    {
        _lastConversionProgress = e;
        _conversionTotalJobs = e.Total;
        _conversionCurrentFileName = Path.GetFileName(e.CurrentJob.OutputFile);
        _conversionCurrentFileProgress = Math.Min(e.Completed + 1, e.Total);
    }

    private void EnsureTextureRows()
    {
        if (_cachedAnalysis == null)
        {
            return;
        }

        if (_textureRowsDirty && _textureRowsBuildTask == null)
        {
            _textureRowsBuildCts?.Dispose();
            _textureRowsBuildCts = new();
            var snapshot = _cachedAnalysis;
            _textureRowsBuildTask = Task.Run(() => BuildTextureRows(snapshot, _textureRowsBuildCts.Token), _textureRowsBuildCts.Token);
        }

        if (_textureRowsBuildTask == null || !_textureRowsBuildTask.IsCompleted)
        {
            return;
        }

        var completedTask = _textureRowsBuildTask;
        _textureRowsBuildTask = null;
        _textureRowsBuildCts?.Dispose();
        _textureRowsBuildCts = null;

        if (completedTask.IsCanceled)
        {
            return;
        }

        if (completedTask.IsFaulted)
        {
            _logger.LogWarning(completedTask.Exception, "Failed to build texture rows.");
            _textureRowsDirty = false;
            return;
        }

        ApplyTextureRowBuild(completedTask.Result);
        _textureRowsDirty = false;
    }

    private void ApplyTextureRowBuild(TextureRowBuildResult result)
    {
        _textureRows.Clear();
        _textureRows.AddRange(result.Rows);

        foreach (var row in _textureRows)
        {
            if (row.IsAlreadyCompressed)
            {
                _selectedTextureKeys.Remove(row.Key);
                _textureSelections.Remove(row.Key);
            }
        }

        _selectedTextureKeys.RemoveWhere(key => !result.ValidKeys.Contains(key));

        foreach (var key in _texturePreviews.Keys.ToArray())
        {
            if (!result.ValidKeys.Contains(key) && _texturePreviews.TryGetValue(key, out var preview))
            {
                preview.Texture?.Dispose();
                _texturePreviews.Remove(key);
            }
        }

        foreach (var key in _textureResolutionCache.Keys.ToArray())
        {
            if (!result.ValidKeys.Contains(key))
            {
                _textureResolutionCache.Remove(key);
            }
        }

        foreach (var key in _textureSelections.Keys.ToArray())
        {
            if (!result.ValidKeys.Contains(key))
            {
                _textureSelections.Remove(key);
                continue;
            }

            _textureSelections[key] = _textureCompressionService.NormalizeTarget(_textureSelections[key]);
        }

        if (!string.IsNullOrEmpty(_selectedTextureKey) && !result.ValidKeys.Contains(_selectedTextureKey))
        {
            _selectedTextureKey = string.Empty;
        }
    }

    private TextureRowBuildResult BuildTextureRows(
        Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> analysis,
        CancellationToken token)
    {
        var rows = new List<TextureRow>();
        HashSet<string> validKeys = new(StringComparer.OrdinalIgnoreCase);

        foreach (var (objectKind, entries) in analysis)
        {
            foreach (var entry in entries.Values)
            {
                token.ThrowIfCancellationRequested();

                if (!string.Equals(entry.FileType, "tex", StringComparison.Ordinal))
                {
                    continue;
                }

                if (entry.FilePaths.Count == 0)
                {
                    continue;
                }

                var primaryFile = entry.FilePaths[0];
                var duplicatePaths = entry.FilePaths.Skip(1).ToList();
                var primaryGamePath = entry.GamePaths.FirstOrDefault() ?? string.Empty;
                var classificationPath = string.IsNullOrEmpty(primaryGamePath) ? primaryFile : primaryGamePath;
                var mapKind = _textureMetadataHelper.DetermineMapKind(primaryGamePath, primaryFile);
                var category = TextureMetadataHelper.DetermineCategory(classificationPath);
                var slot = TextureMetadataHelper.DetermineSlot(category, classificationPath);
                var format = entry.Format.Value;
                var suggestion = TextureMetadataHelper.GetSuggestedTarget(format, mapKind, classificationPath);
                TextureCompressionTarget? currentTarget = TextureMetadataHelper.TryMapFormatToTarget(format, out var mappedTarget)
                    ? mappedTarget
                    : null;
                var displayName = Path.GetFileName(primaryFile);

                var row = new TextureRow(
                    objectKind,
                    entry,
                    primaryFile,
                    duplicatePaths,
                    entry.GamePaths.ToList(),
                    primaryGamePath,
                    format,
                    mapKind,
                    category,
                    slot,
                    displayName,
                    currentTarget,
                    suggestion?.Target,
                    suggestion?.Reason);

                validKeys.Add(row.Key);
                rows.Add(row);
            }
        }

        rows.Sort((a, b) =>
        {
            var comp = a.ObjectKind.CompareTo(b.ObjectKind);
            if (comp != 0)
                return comp;

            comp = string.Compare(a.Slot, b.Slot, StringComparison.OrdinalIgnoreCase);
            if (comp != 0)
                return comp;

            return string.Compare(a.DisplayName, b.DisplayName, StringComparison.OrdinalIgnoreCase);
        });

        return new TextureRowBuildResult(rows, validKeys);
    }

    private void InvalidateTextureRows()
    {
        _textureRowsDirty = true;
        _textureRowsBuildCts?.Cancel();
        _textureResolutionCache.Clear();
    }

    private static string MakeTextureKey(ObjectKind objectKind, string primaryFilePath) =>
        $"{objectKind}|{primaryFilePath}".ToLowerInvariant();

    private void ResetTextureFilters()
    {
        _textureCategoryFilter = null;
        _textureSlotFilter = "All";
        _textureMapFilter = null;
        _textureTargetFilter = null;
        _textureSearch = string.Empty;
    }

    private static Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> CloneAnalysis(
        Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> source)
    {
        var clone = new Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>(source.Count);

        foreach (var (objectKind, entries) in source)
        {
            var entryClone = new Dictionary<string, CharacterAnalyzer.FileDataEntry>(entries.Count, entries.Comparer);

            foreach (var (hash, entry) in entries)
            {
                entryClone[hash] = new CharacterAnalyzer.FileDataEntry(
                    hash: hash,
                    fileType: entry.FileType,
                    gamePaths: entry.GamePaths?.ToList() ?? [],
                    filePaths: entry.FilePaths?.ToList() ?? [],
                    originalSize: entry.OriginalSize,
                    compressedSize: entry.CompressedSize,
                    triangles: entry.Triangles,
                    cacheEntries: entry.CacheEntries
                );
            }

            clone[objectKind] = entryClone;
        }

        return clone;
    }

    private void DrawAnalysisOverview(int totalFiles, long totalActualSize, long totalCompressedSize, long totalTriangles, string breakdownTooltip)
    {
        var scale = ImGuiHelpers.GlobalScale;
        var accent = UIColors.Get("LightlessGreen");
        var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.18f);
        var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.4f);
        var infoColor = ImGuiColors.DalamudGrey;
        var diff = totalActualSize - totalCompressedSize;
        string? diffText = null;
        Vector4? diffColor = null;
        if (diff > 0)
        {
            diffText = $"Saved {UiSharedService.ByteToString(diff)}";
            diffColor = UIColors.Get("LightlessGreen");
        }
        else if (diff < 0)
        {
            diffText = $"Over by {UiSharedService.ByteToString(Math.Abs(diff))}";
            diffColor = UIColors.Get("DimRed");
        }

        var summaryHeight = MathF.Max(ImGui.GetTextLineHeightWithSpacing() * 2.4f, 44f * scale);

        using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f * scale))
        using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize)))
        using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(12f * scale, 6f * scale)))
        using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(18f * scale, 4f * scale)))
        using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(accentBg)))
        using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(accentBorder)))
        using (var child = ImRaii.Child("analysisOverview", new Vector2(-1f, summaryHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
        {
            if (child)
            {
                using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(8f * scale, 4f * scale)))
                {
                    if (ImGui.BeginTable("analysisOverviewTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.PadOuterX | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.NoHostExtendX))
                    {
                        ImGui.TableNextRow();
                        DrawSummaryCell(FontAwesomeIcon.ListUl, accent, $"{totalFiles:N0}", totalFiles == 1 ? "Tracked file" : "Tracked files", infoColor, scale, tooltip: breakdownTooltip);
                        DrawSummaryCell(FontAwesomeIcon.FileArchive, ImGuiColors.DalamudGrey, UiSharedService.ByteToString(totalActualSize), "Actual size", infoColor, scale);
                        DrawSummaryCell(FontAwesomeIcon.CompressArrowsAlt, UIColors.Get("LightlessYellow2"), UiSharedService.ByteToString(totalCompressedSize), "Compressed size", infoColor, scale, diffText, diffColor);
                        DrawSummaryCell(FontAwesomeIcon.ChartLine, UIColors.Get("LightlessPurple"), totalTriangles.ToString("N0", CultureInfo.InvariantCulture), "Modded triangles", infoColor, scale);
                        ImGui.EndTable();
                    }
                }
            }
        }

        ImGuiHelpers.ScaledDummy(6);
    }

    private void DrawObjectOverview(
        ObjectKind objectKind,
        IReadOnlyDictionary<string, CharacterAnalyzer.FileDataEntry> entries,
        IReadOnlyList<IGrouping<string, CharacterAnalyzer.FileDataEntry>> groupedFiles)
    {
        var scale = ImGuiHelpers.GlobalScale;
        var accent = UIColors.Get("LightlessPurple");
        var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.16f);
        var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.32f);
        var infoColor = ImGuiColors.DalamudGrey;
        var fileCount = entries.Count;
        var actualSize = entries.Sum(c => c.Value.OriginalSize);
        var compressedSize = entries.Sum(c => c.Value.CompressedSize);
        var triangles = entries.Sum(c => c.Value.Triangles);
        var breakdown = string.Join(Environment.NewLine,
            groupedFiles.Select(f =>
                $"{f.Key}: {f.Count()} files, size: {UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))}, compressed: {UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))}"));

        var savings = actualSize - compressedSize;
        string? compressedExtra = null;
        Vector4? compressedExtraColor = null;
        if (savings > 0)
        {
            compressedExtra = $"Saved {UiSharedService.ByteToString(savings)}";
            compressedExtraColor = UIColors.Get("LightlessGreen");
        }
        else if (savings < 0)
        {
            compressedExtra = $"Over by {UiSharedService.ByteToString(Math.Abs(savings))}";
            compressedExtraColor = UIColors.Get("DimRed");
        }

        long actualVram = 0;
        var vramGroup = groupedFiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal));
        if (vramGroup != null)
        {
            actualVram = vramGroup.Sum(f => f.OriginalSize);
        }

        string? vramExtra = null;
        Vector4? vramExtraColor = null;
        var vramSub = vramGroup != null ? "VRAM usage" : "VRAM usage (no textures)";
        var showThresholds = _playerPerformanceConfig.Current.WarnOnExceedingThresholds
                              || _playerPerformanceConfig.Current.ShowPerformanceIndicator;
        if (showThresholds && actualVram > 0)
        {
            var thresholdBytes = Math.Max(0, _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB) * 1024L * 1024L;
            if (thresholdBytes > 0)
            {
                if (actualVram > thresholdBytes)
                {
                    vramExtra = $"Over by {UiSharedService.ByteToString(actualVram - thresholdBytes)}";
                    vramExtraColor = UIColors.Get("LightlessYellow");
                }
                else
                {
                    vramExtra = $"Remaining {UiSharedService.ByteToString(thresholdBytes - actualVram)}";
                    vramExtraColor = UIColors.Get("LightlessGreen");
                }
            }
        }

        string? triExtra = null;
        Vector4? triExtraColor = null;
        if (showThresholds)
        {
            var triThreshold = Math.Max(0, _playerPerformanceConfig.Current.TrisWarningThresholdThousands) * 1000;
            if (triThreshold > 0)
            {
                if (triangles > triThreshold)
                {
                    triExtra = $"Over by {(triangles - triThreshold).ToString("N0", CultureInfo.InvariantCulture)}";
                    triExtraColor = UIColors.Get("LightlessYellow");
                }
                else
                {
                    triExtra = $"Remaining {(triThreshold - triangles).ToString("N0", CultureInfo.InvariantCulture)}";
                    triExtraColor = UIColors.Get("LightlessGreen");
                }
            }
        }

        var summaryHeight = MathF.Max(ImGui.GetTextLineHeightWithSpacing() * 2.4f, 46f * scale);
        var availableWidth = ImGui.GetContentRegionAvail().X;
        var summaryWidth = objectKind == ObjectKind.Player
            ? availableWidth
            : MathF.Min(availableWidth, 760f * scale);

        using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 5f * scale))
        using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize)))
        using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(12f * scale, 6f * scale)))
        using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(18f * scale, 4f * scale)))
        using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(accentBg)))
        using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(accentBorder)))
        using (var child = ImRaii.Child($"objectOverview##{objectKind}", new Vector2(summaryWidth, summaryHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
        {
            if (child)
            {
                using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(8f * scale, 4f * scale)))
                {
                    if (ImGui.BeginTable($"objectOverviewTable##{objectKind}", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.PadOuterX | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.NoHostExtendX))
                    {
                        ImGui.TableNextRow();
                        DrawSummaryCell(FontAwesomeIcon.ClipboardList, accent, $"{fileCount:N0}", $"{objectKind} files", infoColor, scale, tooltip: breakdown);
                        DrawSummaryCell(FontAwesomeIcon.FileArchive, ImGuiColors.DalamudGrey, UiSharedService.ByteToString(actualSize), "Actual size", infoColor, scale);
                        DrawSummaryCell(FontAwesomeIcon.CompressArrowsAlt, UIColors.Get("LightlessYellow2"), UiSharedService.ByteToString(compressedSize), "Compressed size", infoColor, scale, compressedExtra, compressedExtraColor);
                        DrawSummaryCell(FontAwesomeIcon.Memory, UIColors.Get("LightlessBlue"), UiSharedService.ByteToString(actualVram), vramSub, infoColor, scale, vramExtra, vramExtraColor);
                        DrawSummaryCell(FontAwesomeIcon.ProjectDiagram, UIColors.Get("LightlessPurple"), triangles.ToString("N0", CultureInfo.InvariantCulture), "Modded triangles", infoColor, scale, triExtra, triExtraColor);
                        ImGui.EndTable();
                    }
                }
            }
        }

        ImGuiHelpers.ScaledDummy(4);
    }

    private enum TextureWorkspaceTab
    {
        Textures,
        OtherFiles
    }

    private sealed record TextureRow(
        ObjectKind ObjectKind,
        CharacterAnalyzer.FileDataEntry Entry,
        string PrimaryFilePath,
        IReadOnlyList<string> DuplicateFilePaths,
        IReadOnlyList<string> GamePaths,
        string PrimaryGamePath,
        string Format,
        TextureMapKind MapKind,
        TextureUsageCategory Category,
        string Slot,
        string DisplayName,
        TextureCompressionTarget? CurrentTarget,
        TextureCompressionTarget? SuggestedTarget,
        string? SuggestionReason)
    {
        public string Key { get; } = MakeTextureKey(ObjectKind, PrimaryFilePath);
        public string Hash => Entry.Hash;
        public long OriginalSize => Entry.OriginalSize;
        public long CompressedSize => Entry.CompressedSize;
        public bool IsComputed => Entry.IsComputed;
        public bool IsAlreadyCompressed => CurrentTarget.HasValue;
    }

    private sealed record TextureRowBuildResult(
        List<TextureRow> Rows,
        HashSet<string> ValidKeys);

    private sealed class TexturePreviewState
    {
        public Task? LoadTask { get; set; }
        public IDalamudTextureWrap? Texture { get; set; }
        public string? ErrorMessage { get; set; }
        public DateTime LastAccessUtc { get; set; } = DateTime.UtcNow;
    }

    private readonly struct TextureResolutionInfo
    {
        public TextureResolutionInfo(ushort width, ushort height, ushort depth, ushort mipLevels)
        {
            Width = width;
            Height = height;
            Depth = depth;
            MipLevels = mipLevels;
        }

        public ushort Width { get; }
        public ushort Height { get; }
        public ushort Depth { get; }
        public ushort MipLevels { get; }
    }

    private void DrawTextureWorkspace(ObjectKind objectKind, IReadOnlyList<IGrouping<string, CharacterAnalyzer.FileDataEntry>> otherFileGroups)
    {
        if (!_textureWorkspaceTabs.ContainsKey(objectKind))
        {
            _textureWorkspaceTabs[objectKind] = TextureWorkspaceTab.Textures;
        }

        if (otherFileGroups.Count == 0)
        {
            _textureWorkspaceTabs[objectKind] = TextureWorkspaceTab.Textures;
            DrawTextureTabContent(objectKind);
            return;
        }

        using var tabBar = ImRaii.TabBar($"textureWorkspaceTabs##{objectKind}");

        using (var texturesTab = ImRaii.TabItem($"Textures###textures_{objectKind}"))
        {
            if (texturesTab)
            {
                if (_textureWorkspaceTabs[objectKind] != TextureWorkspaceTab.Textures)
                {
                    _textureWorkspaceTabs[objectKind] = TextureWorkspaceTab.Textures;
                }
                DrawTextureTabContent(objectKind);
            }
        }

        using (var otherFilesTab = ImRaii.TabItem($"Other file types###other_{objectKind}"))
        {
            if (otherFilesTab)
            {
                if (_textureWorkspaceTabs[objectKind] != TextureWorkspaceTab.OtherFiles)
                {
                    _textureWorkspaceTabs[objectKind] = TextureWorkspaceTab.OtherFiles;
                }
                DrawOtherFileWorkspace(otherFileGroups);
            }
        }
    }

    private void DrawTextureTabContent(ObjectKind objectKind)
    {
        var scale = ImGuiHelpers.GlobalScale;
        if (_textureRowsBuildTask != null && !_textureRowsBuildTask.IsCompleted && _textureRows.Count == 0)
        {
            UiSharedService.ColorText("Building texture list.", ImGuiColors.DalamudGrey);
            return;
        }
        var objectRows = _textureRows.Where(row => row.ObjectKind == objectKind).ToList();
        var hasAnyTextureRows = objectRows.Count > 0;
        var availableCategories = objectRows.Select(row => row.Category)
            .Distinct()
            .OrderBy(c => c.ToString(), StringComparer.Ordinal)
            .ToList();
        var availableSlots = objectRows
            .Select(row => row.Slot)
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .OrderBy(s => s, StringComparer.OrdinalIgnoreCase)
            .ToList();
        var availableMapKinds = objectRows.Select(row => row.MapKind)
            .Distinct()
            .OrderBy(m => m.ToString(), StringComparer.Ordinal)
            .ToList();

        var totalTextureCount = objectRows.Count;
        var totalTextureOriginal = objectRows.Sum(row => row.OriginalSize);
        var totalTextureCompressed = objectRows.Sum(row => row.CompressedSize);

        IEnumerable<TextureRow> filtered = objectRows;

        if (_textureCategoryFilter is { } categoryFilter)
        {
            filtered = filtered.Where(row => row.Category == categoryFilter);
        }

        if (!string.Equals(_textureSlotFilter, "All", StringComparison.Ordinal))
        {
            filtered = filtered.Where(row => string.Equals(row.Slot, _textureSlotFilter, StringComparison.OrdinalIgnoreCase));
        }

        if (_textureMapFilter is { } mapFilter)
        {
            filtered = filtered.Where(row => row.MapKind == mapFilter);
        }

        if (_textureTargetFilter is { } targetFilter)
        {
            filtered = filtered.Where(row =>
                (row.CurrentTarget != null && row.CurrentTarget == targetFilter) ||
                (row.CurrentTarget == null && row.SuggestedTarget == targetFilter));
        }

        if (!string.IsNullOrWhiteSpace(_textureSearch))
        {
            var term = _textureSearch.Trim();
            filtered = filtered.Where(row =>
                row.DisplayName.Contains(term, StringComparison.OrdinalIgnoreCase) ||
                row.PrimaryGamePath.Contains(term, StringComparison.OrdinalIgnoreCase) ||
                row.Hash.Contains(term, StringComparison.OrdinalIgnoreCase));
        }

        var rows = filtered.ToList();

        if (!string.IsNullOrEmpty(_selectedTextureKey) && rows.All(r => r.Key != _selectedTextureKey))
        {
            _selectedTextureKey = rows.FirstOrDefault()?.Key ?? string.Empty;
        }

        var totalOriginal = rows.Sum(r => r.OriginalSize);
        var totalCompressed = rows.Sum(r => r.CompressedSize);

        var availableSize = ImGui.GetContentRegionAvail();
        var windowPos = ImGui.GetWindowPos();
        var spacingX = ImGui.GetStyle().ItemSpacing.X;
        var filterSplitterWidth = TextureFilterSplitterWidth * scale;
        var detailSplitterWidth = (_textureDetailCollapsed ? TextureDetailSplitterCollapsedWidth : TextureDetailSplitterWidth) * scale;
        var totalSplitterWidth = filterSplitterWidth + detailSplitterWidth;
        var totalSpacing = 2 * spacingX;
        const float minFilterWidth = MinTextureFilterPaneWidth;
        const float minDetailWidth = MinTextureDetailPaneWidth;
        const float minCenterWidth = 340f;

        var detailMinForLayout = _textureDetailCollapsed ? 0f : minDetailWidth;
        var dynamicFilterMax = Math.Max(minFilterWidth, availableSize.X - detailMinForLayout - minCenterWidth - totalSplitterWidth - totalSpacing);
        var filterMaxBound = Math.Min(MaxTextureFilterPaneWidth, dynamicFilterMax);
        var filterWidth = Math.Clamp(_textureFilterPaneWidth, minFilterWidth, filterMaxBound);

        var dynamicDetailMax = Math.Max(detailMinForLayout, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing);
        var detailMaxBound = _textureDetailCollapsed ? 0f : Math.Min(MaxTextureDetailPaneWidth, dynamicDetailMax);
        var detailWidth = _textureDetailCollapsed ? 0f : Math.Clamp(_textureDetailPaneWidth, minDetailWidth, detailMaxBound);

        var centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing;

        if (centerWidth < minCenterWidth)
        {
            var deficit = minCenterWidth - centerWidth;
            if (!_textureDetailCollapsed)
            {
                detailWidth = Math.Clamp(detailWidth - deficit, minDetailWidth,
                    Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing)));
                centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing;
                if (centerWidth < minCenterWidth)
                {
                    deficit = minCenterWidth - centerWidth;
                    filterWidth = Math.Clamp(filterWidth - deficit, minFilterWidth,
                        Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - detailWidth - minCenterWidth - totalSplitterWidth - totalSpacing)));
                    detailWidth = Math.Clamp(detailWidth, minDetailWidth,
                        Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing)));
                    centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing;
                    if (centerWidth < minCenterWidth)
                    {
                        centerWidth = minCenterWidth;
                    }
                }
            }
            else
            {
                filterWidth = Math.Clamp(filterWidth - deficit, minFilterWidth,
                    Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - minCenterWidth - totalSplitterWidth - totalSpacing)));
                centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing;
                if (centerWidth < minCenterWidth)
                {
                    centerWidth = minCenterWidth;
                }
            }
        }

        _textureFilterPaneWidth = filterWidth;
        if (!_textureDetailCollapsed)
        {
            _textureDetailPaneWidth = detailWidth;
        }

        ImGui.BeginGroup();
        using (var filters = ImRaii.Child("textureFilters", new Vector2(filterWidth, 0), true))
        {
            if (filters)
            {
                DrawTextureFilters(
                    availableCategories,
                    availableSlots,
                    availableMapKinds,
                    totalTextureCount,
                    totalTextureOriginal,
                    totalTextureCompressed);
            }
        }
        ImGui.EndGroup();

        var filterMin = ImGui.GetItemRectMin();
        var filterMax = ImGui.GetItemRectMax();
        var filterHeight = filterMax.Y - filterMin.Y;
        var filterTopLocal = filterMin - windowPos;
        var maxFilterResize = Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - minCenterWidth - detailMinForLayout - totalSplitterWidth - totalSpacing));
        DrawVerticalResizeHandle("##textureFilterSplitter", filterTopLocal.Y, filterHeight, ref _textureFilterPaneWidth, minFilterWidth, maxFilterResize, out _);

        TextureRow? selectedRow;
        ImGui.BeginGroup();
        using (var tableChild = ImRaii.Child("textureTableArea", new Vector2(centerWidth, 0), false))
        {
            selectedRow = DrawTextureTable(rows, totalOriginal, totalCompressed, hasAnyTextureRows);
        }
        ImGui.EndGroup();

        var tableMin = ImGui.GetItemRectMin();
        var tableMax = ImGui.GetItemRectMax();
        var tableHeight = tableMax.Y - tableMin.Y;
        var tableTopLocal = tableMin - windowPos;
        var maxDetailResize = Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - _textureFilterPaneWidth - minCenterWidth - totalSplitterWidth - totalSpacing));
        var detailToggle = DrawVerticalResizeHandle(
            "##textureDetailSplitter",
            tableTopLocal.Y,
            tableHeight,
            ref _textureDetailPaneWidth,
            minDetailWidth,
            maxDetailResize,
            out var detailDragging,
            invert: true,
            showToggle: true,
            isCollapsed: _textureDetailCollapsed);
        if (detailToggle)
        {
            _textureDetailCollapsed = !_textureDetailCollapsed;
        }
        if (_textureDetailCollapsed && detailDragging)
        {
            _textureDetailCollapsed = false;
        }

        if (!_textureDetailCollapsed)
        {
            ImGui.BeginGroup();
            using (var detailChild = ImRaii.Child("textureDetailPane", new Vector2(detailWidth, 0), true))
            {
                DrawTextureDetail(selectedRow);
            }
            ImGui.EndGroup();
        }
    }

    private void DrawTextureFilters(
        IReadOnlyList<TextureUsageCategory> categories,
        IReadOnlyList<string> slots,
        IReadOnlyList<TextureMapKind> mapKinds,
        int totalTextureCount,
        long totalTextureOriginal,
        long totalTextureCompressed)
    {
        var scale = ImGuiHelpers.GlobalScale;
        var accent = UIColors.Get("LightlessBlue");
        var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.14f);
        var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.35f);
        var infoColor = ImGuiColors.DalamudGrey;

        using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f * scale))
        using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize)))
        using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(12f * scale, 6f * scale)))
        using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(18f * scale, 4f * scale)))
        using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(accentBg)))
        using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(accentBorder)))
        {
            var lineHeight = ImGui.GetTextLineHeightWithSpacing();
            var summaryHeight = MathF.Max(lineHeight * 2.4f, ImGui.GetFrameHeightWithSpacing() * 2.2f);
            using (var totals = ImRaii.Child("textureTotalsSummary", new Vector2(-1f, summaryHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
            {
                if (totals)
                {
                    using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(8f * scale, 4f * scale)))
                    {
                        if (ImGui.BeginTable("textureTotalsSummaryTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.PadOuterX | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.NoHostExtendX))
                        {
                            ImGui.TableNextRow();
                            DrawSummaryCell(FontAwesomeIcon.Images, accent, $"{totalTextureCount:N0}", totalTextureCount == 1 ? "tex file" : "tex files", infoColor, scale);
                            DrawSummaryCell(FontAwesomeIcon.FileArchive, ImGuiColors.DalamudGrey, UiSharedService.ByteToString(totalTextureOriginal), "Actual size", infoColor, scale);
                            DrawSummaryCell(FontAwesomeIcon.CompressArrowsAlt, UIColors.Get("LightlessYellow2"), UiSharedService.ByteToString(totalTextureCompressed), "Compressed size", infoColor, scale);
                            ImGui.EndTable();
                        }
                    }
                }
            }
        }

        ImGuiHelpers.ScaledDummy(1);

        ImGui.TextUnformatted("Filters");
        ImGui.Separator();

        ImGui.SetNextItemWidth(-1);
        ImGui.InputTextWithHint("##textureSearch", "Search hash, path, or file name...", ref _textureSearch, 256);

        ImGuiHelpers.ScaledDummy(6);

        DrawEnumFilterCombo("Category", "All Categories", ref _textureCategoryFilter, categories);
        DrawStringFilterCombo("Slot", ref _textureSlotFilter, slots, "All");
        DrawEnumFilterCombo("Map Type", "All Map Types", ref _textureMapFilter, mapKinds);

        if (_textureTargetFilter.HasValue && !_textureCompressionService.IsTargetSelectable(_textureTargetFilter.Value))
        {
            _textureTargetFilter = null;
        }

        DrawEnumFilterCombo("Compression", "Any Compression", ref _textureTargetFilter, _textureCompressionService.SelectableTargets);

        ImGuiHelpers.ScaledDummy(8);

        if (_uiSharedService.IconTextButton(FontAwesomeIcon.Undo, "Reset filters"))
        {
            ResetTextureFilters();
        }

        ImGuiHelpers.ScaledDummy(6);
        ImGui.Separator();
        ImGuiHelpers.ScaledDummy(4);
        UiSharedService.ColorText("Texture row colors", UIColors.Get("LightlessPurple"));
        DrawTextureRowLegendItem("Selected", UIColors.Get("LightlessYellow"), "This row is selected in the texture table.");
        DrawTextureRowLegendItem("Already compressed", UIColors.Get("LightlessGreenDefault"), "Texture is already stored in a compressed format.");
        DrawTextureRowLegendItem("Missing analysis data", UIColors.Get("DimRed"), "File size data has not been computed yet.");
    }

    private static void DrawTextureRowLegendItem(string label, Vector4 color, string description)
    {
        var scale = ImGuiHelpers.GlobalScale;
        var swatchSize = new Vector2(12f * scale, 12f * scale);
        ImGui.ColorButton($"##textureRowLegend{label}", color, ImGuiColorEditFlags.NoTooltip | ImGuiColorEditFlags.NoDragDrop, swatchSize);
        ImGui.SameLine(0f, 6f * scale);
        var wrapPos = ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X;
        UiSharedService.TextWrapped($"{label}: {description}", wrapPos);
    }

    private static void DrawEnumFilterCombo<T>(
        string label,
        string allLabel,
        ref T? currentSelection,
        IEnumerable<T> options)
        where T : struct, Enum
    {
        var displayLabel = currentSelection?.ToString() ?? allLabel;
        if (!ImGui.BeginCombo(label, displayLabel))
        {
            return;
        }

        bool noSelection = !currentSelection.HasValue;
        if (ImGui.Selectable(allLabel, noSelection))
        {
            currentSelection = null;
        }
        if (noSelection)
        {
            ImGui.SetItemDefaultFocus();
        }

        var comparer = EqualityComparer<T>.Default;
        foreach (var option in options)
        {
            bool selected = currentSelection.HasValue && comparer.Equals(currentSelection.Value, option);
            if (ImGui.Selectable(option.ToString(), selected))
            {
                currentSelection = option;
            }
            if (selected)
            {
                ImGui.SetItemDefaultFocus();
            }
        }

        ImGui.EndCombo();
    }

    private static void DrawStringFilterCombo(
        string label,
        ref string currentSelection,
        IEnumerable<string> options,
        string allLabel)
    {
        var displayLabel = string.IsNullOrEmpty(currentSelection) || string.Equals(currentSelection, allLabel, StringComparison.Ordinal)
            ? allLabel
            : currentSelection;
        if (!ImGui.BeginCombo(label, displayLabel))
        {
            return;
        }

        bool allSelected = string.Equals(currentSelection, allLabel, StringComparison.Ordinal);
        if (ImGui.Selectable(allLabel, allSelected))
        {
            currentSelection = allLabel;
        }
        if (allSelected)
        {
            ImGui.SetItemDefaultFocus();
        }

        foreach (var option in options)
        {
            bool selected = string.Equals(currentSelection, option, StringComparison.OrdinalIgnoreCase);
            if (ImGui.Selectable(option, selected))
            {
                currentSelection = option;
            }
            if (selected)
            {
                ImGui.SetItemDefaultFocus();
            }
        }

        ImGui.EndCombo();
    }

    private void DrawSummaryCell(
        FontAwesomeIcon icon,
        Vector4 iconColor,
        string mainText,
        string subText,
        Vector4 subColor,
        float scale,
        string? extraText = null,
        Vector4? extraColor = null,
        string? tooltip = null)
    {
        ImGui.TableNextColumn();
        var spacing = new Vector2(6f * scale, 2f * scale);
        using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing))
        {
            ImGui.BeginGroup();
            _uiSharedService.IconText(icon, iconColor);
            ImGui.SameLine(0f, 4f * scale);
            using (ImRaii.PushColor(ImGuiCol.Text, iconColor))
            {
                ImGui.TextUnformatted(mainText);
            }
            using (ImRaii.PushColor(ImGuiCol.Text, subColor))
            {
                ImGui.TextUnformatted(subText);
            }
            if (!string.IsNullOrWhiteSpace(extraText))
            {
                ImGui.SameLine(0f, 4f * scale);
                using (ImRaii.PushColor(ImGuiCol.Text, extraColor ?? subColor))
                {
                    ImGui.TextUnformatted(extraText);
                }
            }
            ImGui.EndGroup();
        }

        if (!string.IsNullOrWhiteSpace(tooltip) && ImGui.IsItemHovered())
        {
            ImGui.SetTooltip(tooltip);
        }
    }

    private void DrawOtherFileWorkspace(IReadOnlyList<IGrouping<string, CharacterAnalyzer.FileDataEntry>> otherFileGroups)
    {
        if (otherFileGroups.Count == 0)
        {
            return;
        }

        var scale = ImGuiHelpers.GlobalScale;

        ImGuiHelpers.ScaledDummy(8);
        var accent = UIColors.Get("LightlessBlue");
        var sectionAvail = ImGui.GetContentRegionAvail().Y;
        IGrouping<string, CharacterAnalyzer.FileDataEntry>? activeGroup = null;

        using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize)))
        using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f * scale))
        using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(12f * scale, 6f * scale)))
        using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(12f * scale, 4f * scale)))
        using (var child = ImRaii.Child("otherFileTypes", new Vector2(-1f, sectionAvail - (SelectedFilePanelLogicalHeight * scale) - 12f * scale), true))
        {
            if (child)
            {
                UiSharedService.ColorText("Other file types", UIColors.Get("LightlessPurple"));
                ImGuiHelpers.ScaledDummy(4);

                using var tabBar = ImRaii.TabBar("otherFileTabs", ImGuiTabBarFlags.NoCloseWithMiddleMouseButton);
                foreach (var fileGroup in otherFileGroups)
                {
                    string tabLabel = $"{fileGroup.Key} [{fileGroup.Count()}]";
                    var requiresCompute = fileGroup.Any(k => !k.IsComputed);
                    using var tabCol = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.Color(UIColors.Get("LightlessYellow")), requiresCompute);
                    if (requiresCompute)
                    {
                        tabLabel += " (!)";
                    }

                    ImRaii.IEndObject tabItem;
                    using (var textCol = ImRaii.PushColor(ImGuiCol.Text, UiSharedService.Color(new Vector4(0, 0, 0, 1)),
                               requiresCompute && !string.Equals(_selectedFileTypeTab, fileGroup.Key, StringComparison.Ordinal)))
                    {
                        tabItem = ImRaii.TabItem(tabLabel + "###other_" + fileGroup.Key);
                    }

                    if (!tabItem)
                    {
                        tabItem.Dispose();
                        continue;
                    }

                    activeGroup = fileGroup;

                    if (!string.Equals(_selectedFileTypeTab, fileGroup.Key, StringComparison.Ordinal))
                    {
                        _selectedFileTypeTab = fileGroup.Key;
                        _selectedHash = string.Empty;
                    }

                    var originalTotal = fileGroup.Sum(c => c.OriginalSize);
                    var compressedTotal = fileGroup.Sum(c => c.CompressedSize);

                    var badgeBg = new Vector4(accent.X, accent.Y, accent.Z, 0.18f);
                    var badgeBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.35f);
                    var summaryHeight = MathF.Max(ImGui.GetTextLineHeightWithSpacing() * 2.6f, 36f * scale);
                    var summaryWidth = MathF.Min(420f * scale, ImGui.GetContentRegionAvail().X);
                    using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 4f * scale))
                    using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize)))
                    using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(badgeBg)))
                    using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(badgeBorder)))
                    using (var summaryChild = ImRaii.Child($"otherFileSummary##{fileGroup.Key}", new Vector2(summaryWidth, summaryHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
                    {
                        if (summaryChild)
                        {
                            var infoColor = ImGuiColors.DalamudGrey;
                            var countColor = UIColors.Get("LightlessBlue");
                            var actualColor = ImGuiColors.DalamudGrey;
                            var compressedColor = UIColors.Get("LightlessYellow2");

                            using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(10f * scale, 4f * scale)))
                            using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(12f * scale, 2f * scale)))
                            {
                                using var summaryTable = ImRaii.Table("otherFileSummaryTable", 3,
                                    ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.PadOuterX | ImGuiTableFlags.NoHostExtendX,
                                    new Vector2(-1f, -1f));
                                if (summaryTable)
                                {
                                    ImGui.TableNextRow();
                                    DrawSummaryCell(FontAwesomeIcon.LayerGroup, countColor,
                                        fileGroup.Count().ToString("N0", CultureInfo.InvariantCulture),
                                        $"{fileGroup.Key} files", infoColor, scale);
                                    DrawSummaryCell(FontAwesomeIcon.FileArchive, actualColor,
                                        UiSharedService.ByteToString(originalTotal),
                                        "Actual size", infoColor, scale);
                                    DrawSummaryCell(FontAwesomeIcon.CompressArrowsAlt, compressedColor,
                                        UiSharedService.ByteToString(compressedTotal),
                                        "Compressed size", infoColor, scale);
                                }
                            }
                        }
                    }

                    using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(12f * scale, 6f * scale)))
                    using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(18f * scale, 4f * scale)))
                    using (ImRaii.PushStyle(ImGuiStyleVar.FramePadding, new Vector2(4f * scale, 3f * scale)))
                    using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 3f * scale)))
                    {
                        DrawTable(fileGroup);
                    }

                    tabItem.Dispose();
            }
            }
        }

        if (activeGroup == null && otherFileGroups.Count > 0)
        {
            activeGroup = otherFileGroups[0];
        }

        DrawSelectedFileDetails(activeGroup);
    }

    private void DrawSelectedFileDetails(IGrouping<string, CharacterAnalyzer.FileDataEntry>? fileGroup)
    {
        var hasGroup = fileGroup != null;
        var selectionInGroup = hasGroup && !string.IsNullOrEmpty(_selectedHash) &&
            fileGroup!.Any(entry => string.Equals(entry.Hash, _selectedHash, StringComparison.Ordinal));

        var scale = ImGuiHelpers.GlobalScale;
        var accent = UIColors.Get("LightlessBlue");
        var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.12f);
        var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.3f);

        using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 5f * scale))
        using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize)))
        using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(10f * scale, 6f * scale)))
        using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(accentBg)))
        using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(accentBorder)))
        using (var child = ImRaii.Child("selectedFileDetails", new Vector2(-1f, SelectedFilePanelLogicalHeight * scale), true))
        {
            if (!child)
            {
                return;
            }

            if (!selectionInGroup ||
                _cachedAnalysis == null ||
                !_cachedAnalysis.TryGetValue(_selectedObjectTab, out var objectEntries) ||
                !objectEntries.TryGetValue(_selectedHash, out var item))
            {
                UiSharedService.ColorText("Select a file row to view details.", ImGuiColors.DalamudGrey);
                return;
            }

            UiSharedService.ColorText("Selected file:", UIColors.Get("LightlessBlue"));
            ImGui.SameLine();
            UiSharedService.ColorText(_selectedHash, UIColors.Get("LightlessYellow"));

            ImGuiHelpers.ScaledDummy(2);

            UiSharedService.ColorText("Local file path:", UIColors.Get("LightlessBlue"));
            ImGui.SameLine();
            UiSharedService.TextWrapped(item.FilePaths[0]);
            if (item.FilePaths.Count > 1)
            {
                ImGui.SameLine();
                ImGui.TextUnformatted($"(and {item.FilePaths.Count - 1} more)");
                ImGui.SameLine();
                _uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
                UiSharedService.AttachToolTip(string.Join(Environment.NewLine, item.FilePaths.Skip(1)));
            }

            UiSharedService.ColorText("Used by game path:", UIColors.Get("LightlessBlue"));
            ImGui.SameLine();
            UiSharedService.TextWrapped(item.GamePaths[0]);
            if (item.GamePaths.Count > 1)
            {
                ImGui.SameLine();
                ImGui.TextUnformatted($"(and {item.GamePaths.Count - 1} more)");
                ImGui.SameLine();
                _uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
                UiSharedService.AttachToolTip(string.Join(Environment.NewLine, item.GamePaths.Skip(1)));
            }
        }
    }

    private TextureRow? DrawTextureTable(IReadOnlyList<TextureRow> rows, long totalOriginal, long totalCompressed, bool hasAnyTextureRows)
    {
        var scale = ImGuiHelpers.GlobalScale;
        if (rows.Count > 0)
        {
            var accent = UIColors.Get("LightlessBlue");
            var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.14f);
            var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.35f);
            var originalColor = ImGuiColors.DalamudGrey;
            var compressedColor = UIColors.Get("LightlessYellow2");
            var infoColor = ImGuiColors.DalamudGrey;
            var countLabel = rows.Count == 1 ? "Matching texture" : "Matching textures";
            var diff = totalOriginal - totalCompressed;
            var diffMagnitude = Math.Abs(diff);
            var diffColor = diff > 0 ? UIColors.Get("LightlessGreen")
                : diff < 0 ? UIColors.Get("DimRed")
                : ImGuiColors.DalamudGrey;
            var diffLabel = diff > 0 ? "Saved" : diff < 0 ? "Overhead" : "Lossless";
            var diffPercent = diffMagnitude > 0 && totalOriginal > 0
                ? ((diffMagnitude * 100d) / totalOriginal).ToString("0", CultureInfo.InvariantCulture) + "%"
                : null;

            using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f * scale))
            using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize)))
            using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(12f * scale, 6f * scale)))
            using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(18f * scale, 4f * scale)))
            using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(accentBg)))
            using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(accentBorder)))
            {
                var lineHeight = ImGui.GetTextLineHeightWithSpacing();
                var summaryHeight = MathF.Max(lineHeight * 2.4f, ImGui.GetFrameHeightWithSpacing() * 2.2f);
                using (var summary = ImRaii.Child("textureCompressionSummary", new Vector2(-1f, summaryHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
                {
                    if (summary)
                    {
                        using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(8f * scale, 4f * scale)))
                        {
                            if (ImGui.BeginTable("textureCompressionSummaryTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.PadOuterX | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.NoHostExtendX))
                            {
                                ImGui.TableNextRow();
                                DrawSummaryStat(FontAwesomeIcon.Images, accent, $"{rows.Count:N0}", countLabel, infoColor);
                                DrawSummaryStat(FontAwesomeIcon.FileArchive, originalColor, UiSharedService.ByteToString(totalOriginal), "Original total", infoColor);
                                DrawSummaryStat(FontAwesomeIcon.CompressArrowsAlt, compressedColor, UiSharedService.ByteToString(totalCompressed), "Compressed total", infoColor);
                                DrawSummaryStat(
                                    FontAwesomeIcon.ChartLine,
                                    diffColor,
                                    diffMagnitude > 0 ? UiSharedService.ByteToString(diffMagnitude) : "No change",
                                    diffLabel,
                                    diffColor,
                                    diffPercent,
                                    diffColor);

                                ImGui.EndTable();
                            }
                        }
                    }
                }
            }
        }
        else
        {
            if (hasAnyTextureRows)
            {
                UiSharedService.TextWrapped("No textures match the current filters. Try clearing filters or refreshing the analysis session.");
            }
            else
            {
                UiSharedService.ColorText("No textures recorded for this object.", ImGuiColors.DalamudGrey);
            }
        }

        UiSharedService.TextWrapped("Mark textures using the checkbox to add them to the compression queue. You can adjust the target format for each texture before starting the batch compression.");

        bool conversionRunning = _conversionTask != null && !_conversionTask.IsCompleted;
        var activeSelectionCount = _textureRows.Count(row => _selectedTextureKeys.Contains(row.Key) && !row.IsAlreadyCompressed);
        bool hasSelection = activeSelectionCount > 0;
        using (ImRaii.Disabled(conversionRunning || !hasSelection))
        {
            var label = hasSelection ? $"Compress {activeSelectionCount} selected" : "Compress selected";
            if (_uiSharedService.IconTextButton(FontAwesomeIcon.CompressArrowsAlt, label, 220f * scale))
            {
                StartTextureConversion();
            }
        }
        ImGui.SameLine();
        using (ImRaii.Disabled(_selectedTextureKeys.Count == 0 && _textureSelections.Count == 0))
        {
            if (_uiSharedService.IconTextButton(FontAwesomeIcon.Broom, "Clear marks", 160f * scale))
            {
                _selectedTextureKeys.Clear();
                _textureSelections.Clear();
            }
        }
        ImGui.SameLine();
        if (_uiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Refresh", 130f * scale))
        {
            InvalidateTextureRows();
        }

        TextureRow? lastSelected = null;
        using (var table = ImRaii.Table("textureDataTable", 9,
                   ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.Sortable,
                   new Vector2(-1, 0)))
        {
            if (table)
            {
                ImGui.TableSetupColumn("##select", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoSort, 32f * scale);
                ImGui.TableSetupColumn("Texture", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoSort, 310f * scale);
                ImGui.TableSetupColumn("Slot", ImGuiTableColumnFlags.NoSort);
                ImGui.TableSetupColumn("Map", ImGuiTableColumnFlags.NoSort);
                ImGui.TableSetupColumn("Format", ImGuiTableColumnFlags.NoSort);
                ImGui.TableSetupColumn("Recommended", ImGuiTableColumnFlags.NoSort);
                ImGui.TableSetupColumn("Target", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoSort, 140f * scale);
                ImGui.TableSetupColumn("Original", ImGuiTableColumnFlags.PreferSortDescending);
                ImGui.TableSetupColumn("Compressed", ImGuiTableColumnFlags.PreferSortDescending);
                ImGui.TableSetupScrollFreeze(0, 1);
                ImGui.TableHeadersRow();

                var targets = _textureCompressionService.SelectableTargets;

                IEnumerable<TextureRow> orderedRows = rows;
                var sortSpecs = ImGui.TableGetSortSpecs();
                if (sortSpecs.SpecsCount > 0)
                {
                    var spec = sortSpecs.Specs[0];
                    orderedRows = spec.ColumnIndex switch
                    {
                        7 => spec.SortDirection == ImGuiSortDirection.Ascending
                            ? rows.OrderBy(r => r.OriginalSize)
                            : rows.OrderByDescending(r => r.OriginalSize),
                        8 => spec.SortDirection == ImGuiSortDirection.Ascending
                            ? rows.OrderBy(r => r.CompressedSize)
                            : rows.OrderByDescending(r => r.CompressedSize),
                        _ => rows
                    };

                    sortSpecs.SpecsDirty = false;
                }

                var index = 0;
                foreach (var row in orderedRows)
                {
                    DrawTextureRow(row, targets, index);
                    index++;
                }
            }
        }

        return rows.FirstOrDefault(r => r.Key == _selectedTextureKey);

        void DrawSummaryStat(FontAwesomeIcon icon, Vector4 iconColor, string mainText, string subText, Vector4 subColor, string? extraText = null, Vector4? extraColor = null)
        {
            ImGui.TableNextColumn();
            using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(6f * scale, 2f * scale)))
            using (ImRaii.Group())
            {
                _uiSharedService.IconText(icon, iconColor);
                ImGui.SameLine(0f, 6f * scale);
                using (ImRaii.PushColor(ImGuiCol.Text, iconColor))
                {
                    ImGui.TextUnformatted(mainText);
                }
                using (ImRaii.PushColor(ImGuiCol.Text, subColor))
                {
                    ImGui.TextUnformatted(subText);
                }
                if (!string.IsNullOrWhiteSpace(extraText))
                {
                    ImGui.SameLine(0f, 6f * scale);
                    using (ImRaii.PushColor(ImGuiCol.Text, extraColor ?? iconColor))
                    {
                        ImGui.TextUnformatted(extraText);
                    }
                }
            }
        }
    }
    private void StartTextureConversion()
    {
        if (_conversionTask != null && !_conversionTask.IsCompleted)
        {
            return;
        }

        var selectedRows = _textureRows
            .Where(row => _selectedTextureKeys.Contains(row.Key) && !row.IsAlreadyCompressed)
            .ToList();
        if (selectedRows.Count == 0)
        {
            return;
        }

        var requests = selectedRows.Select(row =>
        {
            var desiredTarget = _textureSelections.TryGetValue(row.Key, out var selection)
                ? selection
                : row.SuggestedTarget ?? row.CurrentTarget ?? _textureCompressionService.DefaultTarget;
            var normalizedTarget = _textureCompressionService.NormalizeTarget(desiredTarget);
            _textureSelections[row.Key] = normalizedTarget;

            return new TextureCompressionRequest(row.PrimaryFilePath, row.DuplicateFilePaths, normalizedTarget);
        }).ToList();

        _conversionCancellationTokenSource = _conversionCancellationTokenSource.CancelRecreate();
        _conversionTotalJobs = requests.Count;
        _lastConversionProgress = null;
        _conversionCurrentFileName = string.Empty;
        _conversionCurrentFileProgress = 0;
        _conversionFailed = false;

        _conversionTask = RunTextureConversionAsync(requests, _conversionCancellationTokenSource.Token);
        _showModal = true;
    }

    private async Task RunTextureConversionAsync(List<TextureCompressionRequest> requests, CancellationToken token)
    {
        try
        {
            await _textureCompressionService.ConvertTexturesAsync(requests, _conversionProgress, token).ConfigureAwait(false);

            if (!token.IsCancellationRequested)
            {
                var affectedPaths = requests
                    .SelectMany(static request =>
                    {
                        IEnumerable<string> paths = request.DuplicateFilePaths;
                        return new[] { request.PrimaryFilePath }.Concat(paths);
                    })
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToList();

                if (affectedPaths.Count > 0)
                {
                    try
                    {
                        await _characterAnalyzer.UpdateFileEntriesAsync(affectedPaths, token).ConfigureAwait(false);
                        _hasUpdate = true;
                    }
                    catch (OperationCanceledException)
                    {
                        throw;
                    }
                    catch (Exception updateEx)
                    {
                        _logger.LogWarning(updateEx, "Failed to refresh compressed size data after texture conversion.");
                    }
                }
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Texture compression was cancelled.");
        }
        catch (Exception ex)
        {
            _conversionFailed = true;
            _logger.LogError(ex, "Texture compression failed.");
        }
        finally
        {
            _selectedTextureKeys.Clear();
            _textureSelections.Clear();
            InvalidateTextureRows();
        }
    }

    private bool DrawVerticalResizeHandle(
        string id,
        float topY,
        float height,
        ref float leftWidth,
        float minWidth,
        float maxWidth,
        out bool isDragging,
        bool invert = false,
        bool showToggle = false,
        bool isCollapsed = false)
    {
        var scale = ImGuiHelpers.GlobalScale;
        var splitterWidth = (showToggle
            ? (isCollapsed ? TextureDetailSplitterCollapsedWidth : TextureDetailSplitterWidth)
            : TextureFilterSplitterWidth) * scale;
        ImGui.SameLine();
        var cursor = ImGui.GetCursorPos();
        var contentMin = ImGui.GetWindowContentRegionMin();
        var contentMax = ImGui.GetWindowContentRegionMax();
        var clampedTop = MathF.Max(topY, contentMin.Y);
        var clampedBottom = MathF.Min(topY + height, contentMax.Y);
        var clampedHeight = MathF.Max(0f, clampedBottom - clampedTop);
        var splitterRounding = ImGui.GetStyle().FrameRounding;
        ImGui.SetCursorPos(new Vector2(cursor.X, clampedTop));
        if (clampedHeight <= 0f)
        {
            isDragging = false;
            ImGui.SetCursorPos(new Vector2(cursor.X + splitterWidth + ImGui.GetStyle().ItemSpacing.X, cursor.Y));
            return false;
        }

        ImGui.InvisibleButton(id, new Vector2(splitterWidth, clampedHeight));
        var drawList = ImGui.GetWindowDrawList();
        var rectMin = ImGui.GetItemRectMin();
        var rectMax = ImGui.GetItemRectMax();
        var windowPos = ImGui.GetWindowPos();
        var clipMin = windowPos + contentMin;
        var clipMax = windowPos + contentMax;
        drawList.PushClipRect(clipMin, clipMax, true);
        var clipInset = 1f * scale;
        var drawMin = new Vector2(
            MathF.Max(rectMin.X, clipMin.X),
            MathF.Max(rectMin.Y, clipMin.Y));
        var drawMax = new Vector2(
            MathF.Min(rectMax.X, clipMax.X - clipInset),
            MathF.Min(rectMax.Y, clipMax.Y));
        var hovered = ImGui.IsItemHovered();
        isDragging = ImGui.IsItemActive();
        var baseColor = UIColors.Get("ButtonDefault");
        var hoverColor = UIColors.Get("LightlessPurple");
        var activeColor = UIColors.Get("LightlessPurpleActive");
        var handleColor = isDragging ? activeColor : hovered ? hoverColor : baseColor;
        drawList.AddRectFilled(drawMin, drawMax, UiSharedService.Color(handleColor), splitterRounding);
        drawList.AddRect(drawMin, drawMax, UiSharedService.Color(new Vector4(1f, 1f, 1f, 0.12f)), splitterRounding);

        bool toggleHovered = false;
        bool toggleClicked = false;
        if (showToggle)
        {
            var icon = !isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft;
            Vector2 iconSize;
            using (_uiSharedService.IconFont.Push())
            {
                iconSize = ImGui.CalcTextSize(icon.ToIconString());
            }

            var toggleHeight = MathF.Min(clampedHeight, 64f * scale);
            var toggleMin = new Vector2(
                drawMin.X,
                drawMin.Y + (drawMax.Y - drawMin.Y - toggleHeight) / 2f);
            var toggleMax = new Vector2(
                drawMax.X,
                toggleMin.Y + toggleHeight);
            var toggleColorBase = UIColors.Get("LightlessPurple");
            toggleHovered = ImGui.IsMouseHoveringRect(toggleMin, toggleMax);
            var toggleBg = toggleHovered
                ? new Vector4(toggleColorBase.X, toggleColorBase.Y, toggleColorBase.Z, 0.65f)
                : new Vector4(toggleColorBase.X, toggleColorBase.Y, toggleColorBase.Z, 0.35f);
            if (toggleHovered)
            {
                UiSharedService.AttachToolTip(isCollapsed ? "Show texture details." : "Hide texture details.");
            }

            drawList.AddRectFilled(toggleMin, toggleMax, UiSharedService.Color(toggleBg), splitterRounding);
            drawList.AddRect(toggleMin, toggleMax, UiSharedService.Color(toggleColorBase), splitterRounding);

            var iconPos = new Vector2(
                drawMin.X + (drawMax.X - drawMin.X - iconSize.X) / 2f,
                drawMin.Y + (drawMax.Y - drawMin.Y - iconSize.Y) / 2f);
            using (_uiSharedService.IconFont.Push())
            {
                drawList.AddText(iconPos, ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString());
            }

            if (toggleHovered && ImGui.IsMouseReleased(ImGuiMouseButton.Left) && !ImGui.IsMouseDragging(ImGuiMouseButton.Left))
            {
                toggleClicked = true;
            }
        }

        if (isDragging && !toggleHovered)
        {
            var delta = ImGui.GetIO().MouseDelta.X / scale;
            leftWidth += invert ? -delta : delta;
            leftWidth = Math.Clamp(leftWidth, minWidth, maxWidth);
        }

        drawList.PopClipRect();

        ImGui.SetCursorPos(new Vector2(cursor.X + splitterWidth + ImGui.GetStyle().ItemSpacing.X, cursor.Y));
        return toggleClicked;
    }

    private (IDalamudTextureWrap? Texture, bool IsLoading, string? Error) GetTexturePreview(TextureRow row)
    {
        var key = row.Key;
        if (!_texturePreviews.TryGetValue(key, out var state))
        {
            state = new TexturePreviewState();
            _texturePreviews[key] = state;
        }

        state.LastAccessUtc = DateTime.UtcNow;

        if (state.Texture != null)
        {
            return (state.Texture, false, state.ErrorMessage);
        }

        if (state.LoadTask == null)
        {
            state.LoadTask = Task.Run(async () =>
            {
                try
                {
                    var texture = await BuildPreviewAsync(row, CancellationToken.None).ConfigureAwait(false);
                    state.Texture = texture;
                    state.ErrorMessage = texture == null ? "Preview unavailable." : null;
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Failed to load preview for {File}", row.PrimaryFilePath);
                    state.ErrorMessage = "Failed to load preview.";
                }
            });
        }

        if (state.LoadTask.IsCompleted)
        {
            state.LoadTask = null;
            return (state.Texture, false, state.ErrorMessage);
        }

        return (null, true, state.ErrorMessage);
    }

    private async Task<IDalamudTextureWrap?> BuildPreviewAsync(TextureRow row, CancellationToken token)
    {
        const int PreviewMaxDimension = 1024;

        token.ThrowIfCancellationRequested();

        if (!File.Exists(row.PrimaryFilePath))
        {
            return null;
        }

        try
        {
            using var scratch = TexFileHelper.Load(row.PrimaryFilePath);
            using var rgbaScratch = scratch.GetRGBA(out var rgbaInfo).ThrowIfError(rgbaInfo);

            var meta = rgbaInfo.Meta;
            var width = meta.Width;
            var height = meta.Height;
            var bytesPerPixel = meta.Format.BitsPerPixel() / 8;
            var requiredLength = width * height * bytesPerPixel;

            token.ThrowIfCancellationRequested();

            var rgbaPixels = rgbaScratch.Pixels[..requiredLength].ToArray();
            using var image = ImageSharpImage.LoadPixelData<Rgba32>(rgbaPixels, width, height);

            if (Math.Max(width, height) > PreviewMaxDimension)
            {
                var dominant = Math.Max(width, height);
                var scale = PreviewMaxDimension / (float)dominant;
                var targetWidth = Math.Max(1, (int)MathF.Round(width * scale));
                var targetHeight = Math.Max(1, (int)MathF.Round(height * scale));
                image.Mutate(ctx => ctx.Resize(targetWidth, targetHeight, KnownResamplers.Lanczos3));
            }

            using var ms = new MemoryStream();
            await image.SaveAsPngAsync(ms, cancellationToken: token).ConfigureAwait(false);
            return _uiSharedService.LoadImage(ms.ToArray());
        }
        catch (OperationCanceledException)
        {
            return null;
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "Preview generation failed for {File}", row.PrimaryFilePath);
            return null;
        }
    }

    private void ResetPreview(string key)
    {
        if (_texturePreviews.TryGetValue(key, out var state))
        {
            state.Texture?.Dispose();
            _texturePreviews.Remove(key);
        }
    }

    private TextureResolutionInfo? GetTextureResolution(TextureRow row)
    {
        if (_textureResolutionCache.TryGetValue(row.Key, out var cached))
        {
            return cached;
        }

        var info = TryReadTextureResolution(row.PrimaryFilePath, out var resolved)
            ? resolved
            : (TextureResolutionInfo?)null;
        _textureResolutionCache[row.Key] = info;
        return info;
    }

    private static bool TryReadTextureResolution(string path, out TextureResolutionInfo info)
    {
        info = default;

        try
        {
            Span<byte> buffer = stackalloc byte[16];
            using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
            var read = stream.Read(buffer);
            if (read < buffer.Length)
            {
                return false;
            }

            var width = BinaryPrimitives.ReadUInt16LittleEndian(buffer[8..10]);
            var height = BinaryPrimitives.ReadUInt16LittleEndian(buffer[10..12]);
            var depth = BinaryPrimitives.ReadUInt16LittleEndian(buffer[12..14]);
            var mipLevels = BinaryPrimitives.ReadUInt16LittleEndian(buffer[14..16]);

            if (width == 0 || height == 0)
            {
                return false;
            }

            if (depth == 0)
            {
                depth = 1;
            }

            if (mipLevels == 0)
            {
                mipLevels = 1;
            }

            info = new TextureResolutionInfo(width, height, depth, mipLevels);
            return true;
        }
        catch
        {
            return false;
        }
    }

    private static string FormatTextureResolution(TextureResolutionInfo info)
        => info.Depth > 1
            ? $"{info.Width} x {info.Height} x {info.Depth}"
            : $"{info.Width} x {info.Height}";

    private void DrawTextureRow(TextureRow row, IReadOnlyList<TextureCompressionTarget> targets, int index)
    {
        var key = row.Key;
        bool isSelected = string.Equals(_selectedTextureKey, key, StringComparison.Ordinal);

        ImGui.TableNextRow(ImGuiTableRowFlags.None, 0);
        ApplyTextureRowBackground(row, isSelected);

        bool canCompress = !row.IsAlreadyCompressed;
        if (!canCompress)
        {
            _selectedTextureKeys.Remove(key);
            _textureSelections.Remove(key);
        }

        ImGui.TableNextColumn();
        if (canCompress)
        {
            bool marked = _selectedTextureKeys.Contains(key);
            if (UiSharedService.CheckboxWithBorder($"##select{index}", ref marked, UIColors.Get("LightlessPurple"), 1.5f))
            {
                if (marked)
                {
                    _selectedTextureKeys.Add(key);
                }
                else
                {
                    _selectedTextureKeys.Remove(key);
                }
            }
            UiSharedService.AttachToolTip("Mark texture for batch compression.");
        }
        else
        {
            _uiSharedService.IconText(FontAwesomeIcon.Check, ImGuiColors.DalamudWhite);
            UiSharedService.AttachToolTip("Already stored in a compressed format; additional compression is disabled.");
        }

        DrawSelectableColumn(isSelected, () =>
        {
            var selectableLabel = $"{row.DisplayName}##texName{index}";
            if (ImGui.Selectable(selectableLabel, isSelected))
            {
                _selectedTextureKey = isSelected ? string.Empty : key;
            }

            return () => UiSharedService.AttachToolTip($"{row.PrimaryFilePath}{UiSharedService.TooltipSeparator}{string.Join(Environment.NewLine, row.GamePaths)}");
        });

        DrawSelectableColumn(isSelected, () =>
        {
            ImGui.TextUnformatted(row.Slot);
            return null;
        });
        DrawSelectableColumn(isSelected, () =>
        {
            ImGui.TextUnformatted(row.MapKind.ToString());
            return null;
        });
        DrawSelectableColumn(isSelected, () =>
        {
            Action? tooltipAction = null;
            ImGui.TextUnformatted(row.Format);
            if (!row.IsAlreadyCompressed)
            {
                ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale);
                var iconColor = isSelected ? SelectedTextureRowTextColor : UIColors.Get("LightlessYellow");
                _uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, iconColor);
                tooltipAction = () => UiSharedService.AttachToolTip("Run compression to reduce file size.");
            }
            return tooltipAction;
        });

        DrawSelectableColumn(isSelected, () =>
        {
            if (row.SuggestedTarget.HasValue)
            {
                ImGui.TextUnformatted(row.SuggestedTarget.Value.ToString());
                if (!string.IsNullOrEmpty(row.SuggestionReason))
                {
                    var reason = row.SuggestionReason;
                    return () => UiSharedService.AttachToolTip(reason);
                }
            }
            else
            {
                ImGui.TextUnformatted("-");
            }

            return null;
        });

        ImGui.TableNextColumn();
        if (canCompress)
        {
            var desiredTarget = _textureSelections.TryGetValue(key, out var storedTarget)
                ? storedTarget
                : row.SuggestedTarget ?? row.CurrentTarget ?? _textureCompressionService.DefaultTarget;
            var currentSelection = _textureCompressionService.NormalizeTarget(desiredTarget);
            if (!_textureSelections.TryGetValue(key, out _) || storedTarget != currentSelection)
            {
                _textureSelections[key] = currentSelection;
            }
            var comboLabel = currentSelection.ToString();
            ImGui.SetNextItemWidth(-1);
            if (ImGui.BeginCombo($"##target{index}", comboLabel))
            {
                foreach (var target in targets)
                {
                    bool targetSelected = currentSelection == target;
                    if (ImGui.Selectable(target.ToString(), targetSelected))
                    {
                        _textureSelections[key] = target;
                        currentSelection = target;
                    }
                    if (TextureMetadataHelper.TryGetRecommendationInfo(target, out var targetInfo))
                    {
                        UiSharedService.AttachToolTip($"{targetInfo.Title}{UiSharedService.TooltipSeparator}{targetInfo.Description}");
                    }
                    if (targetSelected)
                    {
                        ImGui.SetItemDefaultFocus();
                    }
                }

                ImGui.EndCombo();
            }
        }
        else
        {
            var label = row.CurrentTarget?.ToString() ?? row.Format;
            ImGui.TextUnformatted(label);
            UiSharedService.AttachToolTip("This texture is already compressed and cannot be processed again.");
        }

        DrawSelectableColumn(isSelected, () =>
        {
            ImGui.TextUnformatted(UiSharedService.ByteToString(row.OriginalSize));
            return null;
        });
        DrawSelectableColumn(isSelected, () =>
        {
            ImGui.TextUnformatted(UiSharedService.ByteToString(row.CompressedSize));
            return null;
        });
    }

    private static void DrawSelectableColumn(bool isSelected, Func<Action?> draw)
    {
        ImGui.TableNextColumn();
        if (isSelected)
        {
            ImGui.PushStyleColor(ImGuiCol.Text, SelectedTextureRowTextColor);
        }

        var after = draw();

        if (isSelected)
        {
            ImGui.PopStyleColor();
        }

        after?.Invoke();
    }

    private static void ApplyTextureRowBackground(TextureRow row, bool isSelected)
    {
        if (isSelected)
        {
            var highlight = UiSharedService.Color(UIColors.Get("LightlessYellow"));
            ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, highlight);
            ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, highlight);
        }
        else if (row.IsAlreadyCompressed)
        {
            var compressedColor = UiSharedService.Color(UIColors.Get("LightlessGreenDefault"));
            ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, compressedColor);
            ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, compressedColor);
        }
        else if (!row.IsComputed)
        {
            var warning = UiSharedService.Color(UIColors.Get("DimRed"));
            ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, warning);
            ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, warning);
        }
    }

    private void DrawTextureDetail(TextureRow? row)
    {
        var scale = ImGuiHelpers.GlobalScale;

        UiSharedService.ColorText("Texture Details", UIColors.Get("LightlessPurple"));
        if (row != null)
        {
            ImGui.SameLine();
            ImGui.TextUnformatted(row.DisplayName);
            UiSharedService.AttachToolTip("Source file: " + row.PrimaryFilePath);
        }
        ImGui.Separator();

        if (row == null)
        {
            UiSharedService.ColorText("Select a texture to view details.", ImGuiColors.DalamudGrey);
            return;
        }

        var (previewTexture, previewLoading, previewError) = GetTexturePreview(row);
        var previewSize = new Vector2(_texturePreviewSize * 0.85f, _texturePreviewSize * 0.85f) * scale;

        if (previewTexture != null)
        {
            ImGui.Image(previewTexture.Handle, previewSize);
            if (_uiSharedService.IconTextButton(FontAwesomeIcon.SyncAlt, "Refresh preview", 180f * ImGuiHelpers.GlobalScale))
            {
                ResetPreview(row.Key);
            }
        }
        else
        {
            using (ImRaii.Child("previewPlaceholder", previewSize, true))
            {
                UiSharedService.TextWrapped(previewLoading ? "Generating preview..." : previewError ?? "Preview unavailable.");
            }
            if (!previewLoading && _uiSharedService.IconTextButton(FontAwesomeIcon.SyncAlt, "Retry preview", 180f * ImGuiHelpers.GlobalScale))
            {
                ResetPreview(row.Key);
            }
        }

        var desiredDetailTarget = _textureSelections.TryGetValue(row.Key, out var userTarget)
            ? userTarget
            : row.SuggestedTarget ?? row.CurrentTarget ?? _textureCompressionService.DefaultTarget;
        var selectedTarget = _textureCompressionService.NormalizeTarget(desiredDetailTarget);
        if (!_textureSelections.TryGetValue(row.Key, out _) || userTarget != selectedTarget)
        {
            _textureSelections[row.Key] = selectedTarget;
        }
        var hasSelectedInfo = TextureMetadataHelper.TryGetRecommendationInfo(selectedTarget, out var selectedInfo);

        using (ImRaii.Child("textureDetailInfo", new Vector2(-1, 0), true, ImGuiWindowFlags.AlwaysVerticalScrollbar))
        {
            using var detailSpacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(6f * scale, 2f * scale));
            DrawMetaOverview();

            ImGuiHelpers.ScaledDummy(4);
            DrawSizeSummary();

            ImGuiHelpers.ScaledDummy(4);
            DrawCompressionInsights();

            ImGuiHelpers.ScaledDummy(6);
            DrawExpandableList("Duplicate Files", row.DuplicateFilePaths, "No duplicate files detected.");
            DrawExpandableList("Game Paths", row.GamePaths, "No game paths recorded.");
        }

        void DrawMetaOverview()
        {
            var labelColor = ImGuiColors.DalamudGrey;
            using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(5f * scale, 2f * scale)))
            {
                var metaFlags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.PadOuterX;
                if (ImGui.BeginTable("textureMetaOverview", 2, metaFlags))
                {
                    MetaRow(FontAwesomeIcon.Cube, "Object", row.ObjectKind.ToString());
                    MetaRow(FontAwesomeIcon.Tag, "Slot", row.Slot);
                    MetaRow(FontAwesomeIcon.LayerGroup, "Map Type", row.MapKind.ToString());
                    MetaRow(FontAwesomeIcon.Fingerprint, "Hash", row.Hash, UIColors.Get("LightlessBlue"));
                    MetaRow(FontAwesomeIcon.InfoCircle, "Current Format", row.Format);
                    var resolution = GetTextureResolution(row);
                    var resolutionLabel = resolution.HasValue ? FormatTextureResolution(resolution.Value) : "Unknown";
                    MetaRow(FontAwesomeIcon.Images, "Resolution", resolutionLabel);

                    var selectedLabel = hasSelectedInfo ? selectedInfo!.Title : selectedTarget.ToString();
                    var selectionColor = hasSelectedInfo ? UIColors.Get("LightlessYellow") : UIColors.Get("LightlessGreen");
                    MetaRow(FontAwesomeIcon.Bullseye, "Selected Target", selectedLabel, selectionColor);

                    ImGui.EndTable();
                }
            }

            void MetaRow(FontAwesomeIcon icon, string label, string value, Vector4? valueColor = null)
            {
                ImGui.TableNextRow();
                ImGui.TableNextColumn();
                _uiSharedService.IconText(icon, labelColor);
                ImGui.SameLine(0f, 4f * scale);
                using (ImRaii.PushColor(ImGuiCol.Text, labelColor))
                {
                    ImGui.TextUnformatted(label);
                }

                ImGui.TableNextColumn();
                if (valueColor.HasValue)
                {
                    using (ImRaii.PushColor(ImGuiCol.Text, valueColor.Value))
                    {
                        ImGui.TextUnformatted(value);
                    }
                }
                else
                {
                    ImGui.TextUnformatted(value);
                }
            }
        }

        void DrawSizeSummary()
        {
            var savedBytes = row.OriginalSize - row.CompressedSize;
            var savedMagnitude = Math.Abs(savedBytes);
            var savedColor = savedBytes > 0 ? UIColors.Get("LightlessGreen") : savedBytes < 0 ? UIColors.Get("DimRed") : ImGuiColors.DalamudGrey;
            var savedLabel = savedBytes > 0 ? "Saved" : savedBytes < 0 ? "Over" : "Delta";
            var savedPercent = row.OriginalSize > 0 && savedMagnitude > 0
                ? $"{savedMagnitude * 100d / row.OriginalSize:0.#}%"
                : null;

            using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale)))
            {
                var statFlags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.PadOuterX;
                if (ImGui.BeginTable("textureSizeSummary", 3, statFlags))
                {
                    ImGui.TableNextRow();
                    StatCell(FontAwesomeIcon.Database, ImGuiColors.DalamudGrey, UiSharedService.ByteToString(row.OriginalSize), "Original");
                    StatCell(FontAwesomeIcon.CompressArrowsAlt, UIColors.Get("LightlessYellow2"), UiSharedService.ByteToString(row.CompressedSize), "Compressed");
                    StatCell(FontAwesomeIcon.ChartLine, savedColor, savedMagnitude > 0 ? UiSharedService.ByteToString(savedMagnitude) : "No change", savedLabel, savedPercent, savedColor);
                    ImGui.EndTable();
                }
            }

            void StatCell(FontAwesomeIcon icon, Vector4 iconColor, string mainText, string caption, string? extra = null, Vector4? extraColor = null)
            {
                ImGui.TableNextColumn();
                _uiSharedService.IconText(icon, iconColor);
                ImGui.SameLine(0f, 4f * scale);
                using (ImRaii.PushColor(ImGuiCol.Text, iconColor))
                {
                    ImGui.TextUnformatted(mainText);
                }
                using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
                {
                    ImGui.TextUnformatted(caption);
                }
                if (!string.IsNullOrEmpty(extra))
                {
                    ImGui.SameLine(0f, 4f * scale);
                    using (ImRaii.PushColor(ImGuiCol.Text, extraColor ?? iconColor))
                    {
                        ImGui.TextUnformatted(extra);
                    }
                }
            }
        }

        void DrawCompressionInsights()
        {
            var matchesRecommendation = row.SuggestedTarget.HasValue && row.SuggestedTarget.Value == selectedTarget;
            var columnCount = row.SuggestedTarget.HasValue ? 2 : 1;
            using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale)))
            {
                var cardFlags = ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.PadOuterX;
                if (ImGui.BeginTable("textureCompressionCards", columnCount, cardFlags))
                {
                    ImGui.TableNextRow();
                    var selectedTitleColor = matchesRecommendation ? UIColors.Get("LightlessGreen") : UIColors.Get("LightlessBlue");
                    var selectedDescription = hasSelectedInfo
                        ? selectedInfo!.Description
                        : matchesRecommendation
                            ? "Selected target matches the automatic recommendation."
                            : "Manual selection without additional guidance.";
                    DrawCompressionCard("Selected Target", selectedLabelText(), selectedTitleColor, selectedDescription);

                    if (row.SuggestedTarget.HasValue)
                    {
                        var recommendedTarget = row.SuggestedTarget.Value;
                        var hasRecommendationInfo = TextureMetadataHelper.TryGetRecommendationInfo(recommendedTarget, out var recommendedInfo);
                        var recommendedTitle = hasRecommendationInfo ? recommendedInfo!.Title : recommendedTarget.ToString();
                        var recommendedDescription = hasRecommendationInfo
                            ? recommendedInfo!.Description
                            : string.IsNullOrEmpty(row.SuggestionReason) ? "No additional context provided." : row.SuggestionReason;

                        DrawCompressionCard("Recommended Target", recommendedTitle, UIColors.Get("LightlessYellow"), recommendedDescription);
                    }

                    ImGui.EndTable();
                }
            }

            using (ImRaii.PushIndent(12f * scale))
            {
                if (!row.SuggestedTarget.HasValue)
                {
                    UiSharedService.ColorTextWrapped("No automatic recommendation available.", UIColors.Get("LightlessYellow"));
                }

                if (!matchesRecommendation && row.SuggestedTarget.HasValue)
                {
                    UiSharedService.ColorTextWrapped("Selected compression differs from the recommendation. Review before running batch conversion.", UIColors.Get("DimRed"));
                }
            }

            string selectedLabelText()
            {
                if (hasSelectedInfo)
                {
                    return selectedInfo!.Title;
                }

                return selectedTarget.ToString();
            }

            void DrawCompressionCard(string header, string title, Vector4 titleColor, string body)
            {
                ImGui.TableNextColumn();
                UiSharedService.ColorText(header, UIColors.Get("LightlessPurple"));
                using (ImRaii.PushColor(ImGuiCol.Text, titleColor))
                {
                    ImGui.TextUnformatted(title);
                }
                UiSharedService.TextWrapped(body, 0, ImGuiColors.DalamudGrey);
            }
        }

        void DrawExpandableList(string title, IReadOnlyList<string> entries, string emptyMessage)
        {
            _ = emptyMessage;
            var count = entries.Count;
            using var headerDefault = ImRaii.PushColor(ImGuiCol.Header, UiSharedService.Color(new Vector4(0.15f, 0.15f, 0.18f, 0.95f)));
            using var headerHover = ImRaii.PushColor(ImGuiCol.HeaderHovered, UiSharedService.Color(new Vector4(0.2f, 0.2f, 0.25f, 1f)));
            using var headerActive = ImRaii.PushColor(ImGuiCol.HeaderActive, UiSharedService.Color(new Vector4(0.25f, 0.25f, 0.3f, 1f)));
            var label = $"{title} ({count})";
            if (!ImGui.CollapsingHeader(label, count == 0 ? ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.None))
            {
                return;
            }

            if (count == 0)
            {
                return;
            }

            var tableFlags = ImGuiTableFlags.PadOuterX | ImGuiTableFlags.NoHostExtendX | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.BordersOuter;
            if (ImGui.BeginTable($"{title}Table", 2, tableFlags))
            {
                ImGui.TableSetupColumn("#", ImGuiTableColumnFlags.WidthFixed, 28f * scale);
                ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthStretch);
                ImGui.TableHeadersRow();

                for (int i = 0; i < entries.Count; i++)
                {
                    ImGui.TableNextRow();
                    ImGui.TableNextColumn();
                    ImGui.TextUnformatted($"{i + 1}.");

                    ImGui.TableNextColumn();
                    var wrapPos = ImGui.GetCursorPosX() + ImGui.GetColumnWidth();
                    ImGui.PushTextWrapPos(wrapPos);
                    ImGui.TextUnformatted(entries[i]);
                    ImGui.PopTextWrapPos();
                }

                ImGui.EndTable();
            }
        }
    }

    private void SortCachedAnalysis<TKey>(
        ObjectKind objectKind,
        Func<KeyValuePair<string, CharacterAnalyzer.FileDataEntry>, TKey> selector,
        bool ascending,
        IComparer<TKey>? comparer = null)
    {
        if (_cachedAnalysis == null || !_cachedAnalysis.TryGetValue(objectKind, out var current))
        {
            return;
        }

        var ordered = ascending
            ? (comparer != null ? current.OrderBy(selector, comparer) : current.OrderBy(selector))
            : (comparer != null ? current.OrderByDescending(selector, comparer) : current.OrderByDescending(selector));

        _cachedAnalysis[objectKind] = ordered.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
    }

    private void DrawTable(IGrouping<string, CharacterAnalyzer.FileDataEntry> fileGroup)
    {
        var tableColumns = string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) ? 6 : 5;
        var scale = ImGuiHelpers.GlobalScale;
        using var table = ImRaii.Table("Analysis", tableColumns,
            ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.BordersInnerV,
            new Vector2(-1f, 0f));
        if (!table.Success)
        {
            return;
        }

        ImGui.TableSetupColumn("Hash");
        ImGui.TableSetupColumn("Filepaths");
        ImGui.TableSetupColumn("Gamepaths");
        ImGui.TableSetupColumn("Original Size");
        ImGui.TableSetupColumn("Compressed Size");
        if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal))
        {
            ImGui.TableSetupColumn("Triangles");
        }

        ImGui.TableSetupScrollFreeze(0, 1);
        ImGui.TableHeadersRow();

        var sortSpecs = ImGui.TableGetSortSpecs();
        if (sortSpecs.SpecsDirty && sortSpecs.SpecsCount > 0)
        {
            var spec = sortSpecs.Specs[0];
            bool ascending = spec.SortDirection == ImGuiSortDirection.Ascending;

            switch (spec.ColumnIndex)
            {
                case 0:
                    SortCachedAnalysis(_selectedObjectTab, pair => pair.Key, ascending, StringComparer.Ordinal);
                    break;
                case 1:
                    SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.FilePaths.Count, ascending);
                    break;
                case 2:
                    SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.GamePaths.Count, ascending);
                    break;
                case 3:
                    SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.OriginalSize, ascending);
                    break;
                case 4:
                    SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.CompressedSize, ascending);
                    break;
                case 5 when string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal):
                    SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.Triangles, ascending);
                    break;
            }

            sortSpecs.SpecsDirty = false;
        }

        foreach (var item in fileGroup)
        {
            using var textColor = ImRaii.PushColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1), string.Equals(item.Hash, _selectedHash, StringComparison.Ordinal));
            using var missingColor = ImRaii.PushColor(ImGuiCol.Text, new Vector4(1, 1, 1, 1), !item.IsComputed);
            ImGui.TableNextColumn();
            if (!item.IsComputed)
            {
                var warning = UiSharedService.Color(UIColors.Get("DimRed"));
                ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, warning);
                ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, warning);
            }
            if (string.Equals(_selectedHash, item.Hash, StringComparison.Ordinal))
            {
                var highlight = UiSharedService.Color(UIColors.Get("LightlessYellow"));
                ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, highlight);
                ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, highlight);
            }
            ImGui.TextUnformatted(item.Hash);
            if (ImGui.IsItemClicked())
            {
                _selectedHash = string.Equals(_selectedHash, item.Hash, StringComparison.Ordinal)
                    ? string.Empty
                    : item.Hash;
            }

            ImGui.TableNextColumn();
            ImGui.TextUnformatted(item.FilePaths.Count.ToString());

            ImGui.TableNextColumn();
            ImGui.TextUnformatted(item.GamePaths.Count.ToString());

            ImGui.TableNextColumn();
            ImGui.TextUnformatted(UiSharedService.ByteToString(item.OriginalSize));

            ImGui.TableNextColumn();
            ImGui.TextUnformatted(UiSharedService.ByteToString(item.CompressedSize));

            if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal))
            {
                ImGui.TableNextColumn();
                ImGui.TextUnformatted(item.Triangles.ToString());
            }
        }
    }
}
