using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;

using Dalamud.Configuration.Internal;
using Dalamud.Console;
using Dalamud.Game;
using Dalamud.Game.Command;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;

using ImGuiNET;

using Serilog;
using Serilog.Events;

namespace Dalamud.Interface.Internal.Windows;

/// <summary>
/// The window that displays the Dalamud log file in-game.
/// </summary>
internal class ConsoleWindow : Window, IDisposable
{
    private const int LogLinesMinimum = 100;
    private const int LogLinesMaximum = 1000000;
    private const int HistorySize = 50;

    // Fields below should be touched only from the main thread.
    private readonly RollingList<LogEntry> logText;
    private readonly RollingList<LogEntry> filteredLogEntries;

    private readonly List<PluginFilterEntry> pluginFilters = new();

    private readonly DalamudConfiguration configuration;

    private int newRolledLines;
    private bool pendingRefilter;
    private bool pendingClearLog;

    private bool? lastCmdSuccess;

    private ImGuiListClipperPtr clipperPtr;
    private string commandText = string.Empty;
    private string textFilter = string.Empty;
    private string textHighlight = string.Empty;
    private string selectedSource = "DalamudInternal";
    private string pluginFilter = string.Empty;

    private Regex? compiledLogFilter;
    private Regex? compiledLogHighlight;
    private Exception? exceptionLogFilter;
    private Exception? exceptionLogHighlight;

    private bool filterShowUncaughtExceptions;
    private bool settingsPopupWasOpen;
    private bool showFilterToolbar;
    private bool copyMode;
    private bool killGameArmed;
    private bool autoScroll;
    private int logLinesLimit;
    private bool autoOpen;

    private int historyPos;
    private int copyStart = -1;

    private string? completionZipText = null;
    private int completionTabIdx = 0;

    private IActiveNotification? prevCopyNotification;

    /// <summary>Initializes a new instance of the <see cref="ConsoleWindow"/> class.</summary>
    /// <param name="configuration">An instance of <see cref="DalamudConfiguration"/>.</param>
    public ConsoleWindow(DalamudConfiguration configuration)
        : base("Dalamud Console", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)
    {
        this.configuration = configuration;

        this.autoScroll = configuration.LogAutoScroll;
        this.autoOpen = configuration.LogOpenAtStartup;

        Service<Framework>.GetAsync().ContinueWith(r => r.Result.Update += this.FrameworkOnUpdate);

        var cm = Service<ConsoleManager>.Get();
        cm.AddCommand("clear", "Clear the console log", () =>
        {
            this.QueueClear();
            return true;
        });
        cm.AddAlias("clear", "cls");

        this.Size = new Vector2(500, 400);
        this.SizeCondition = ImGuiCond.FirstUseEver;

        this.RespectCloseHotkey = false;

        this.logLinesLimit = configuration.LogLinesLimit;

        var limit = Math.Max(LogLinesMinimum, this.logLinesLimit);
        this.logText = new(limit);
        this.filteredLogEntries = new(limit);

        this.configuration.DalamudConfigurationSaved += this.OnDalamudConfigurationSaved;

        unsafe
        {
            this.clipperPtr = new(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
        }
    }

    /// <summary>Gets the queue where log entries that are not processed yet are stored.</summary>
    public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new();

    /// <summary>
    /// Gets or sets the current text filter.
    /// </summary>
    public string TextFilter
    {
        get => this.textFilter;
        set
        {
            this.textFilter = value;
            this.RecompileLogFilter();
        }
    }

    /// <inheritdoc/>
    public override void OnOpen()
    {
        this.killGameArmed = false;
        base.OnOpen();
    }

    /// <inheritdoc/>
    public void Dispose()
    {
        this.configuration.DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
        if (Service<Framework>.GetNullable() is { } framework)
            framework.Update -= this.FrameworkOnUpdate;

        this.clipperPtr.Destroy();
        this.clipperPtr = default;
    }

    /// <inheritdoc/>
    public override void Draw()
    {
        this.DrawOptionsToolbar();

        this.DrawFilterToolbar();

        if (this.exceptionLogFilter is not null)
        {
            ImGui.TextColored(
                ImGuiColors.DalamudRed,
                $"Regex Filter Error: {this.exceptionLogFilter.GetType().Name}");
            ImGui.TextUnformatted(this.exceptionLogFilter.Message);
        }

        if (this.exceptionLogHighlight is not null)
        {
            ImGui.TextColored(
                ImGuiColors.DalamudRed,
                $"Regex Highlight Error: {this.exceptionLogHighlight.GetType().Name}");
            ImGui.TextUnformatted(this.exceptionLogHighlight.Message);
        }

        var sendButtonSize = ImGui.CalcTextSize("Send") +
                             ((new Vector2(16, 0) + (ImGui.GetStyle().FramePadding * 2)) * ImGuiHelpers.GlobalScale);
        var scrollingHeight = ImGui.GetContentRegionAvail().Y - sendButtonSize.Y;
        ImGui.BeginChild(
            "scrolling",
            new Vector2(0, scrollingHeight),
            false,
            ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar);

        ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);

        ImGui.PushFont(InterfaceManager.MonoFont);

        var childPos = ImGui.GetWindowPos();
        var childDrawList = ImGui.GetWindowDrawList();
        var childSize = ImGui.GetWindowSize();

        var timestampWidth = ImGui.CalcTextSize("00:00:00.000").X;
        var levelWidth = ImGui.CalcTextSize("AAA").X;
        var separatorWidth = ImGui.CalcTextSize(" | ").X;
        var cursorLogLevel = timestampWidth + separatorWidth;
        var cursorLogLine = cursorLogLevel + levelWidth + separatorWidth;

        var lastLinePosY = 0.0f;
        var logLineHeight = 0.0f;

        this.clipperPtr.Begin(this.filteredLogEntries.Count);
        while (this.clipperPtr.Step())
        {
            for (var i = this.clipperPtr.DisplayStart; i < this.clipperPtr.DisplayEnd; i++)
            {
                var index = Math.Max(
                    i - this.newRolledLines,
                    0); // Prevents flicker effect. Also workaround to avoid negative indexes.
                var line = this.filteredLogEntries[index];

                if (!line.IsMultiline)
                    ImGui.Separator();

                if (line.SelectedForCopy)
                {
                    ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedGrey);
                    ImGui.PushStyleColor(ImGuiCol.HeaderActive, ImGuiColors.ParsedGrey);
                    ImGui.PushStyleColor(ImGuiCol.HeaderHovered, ImGuiColors.ParsedGrey);
                }
                else
                {
                    ImGui.PushStyleColor(ImGuiCol.Header, GetColorForLogEventLevel(line.Level));
                    ImGui.PushStyleColor(ImGuiCol.HeaderActive, GetColorForLogEventLevel(line.Level));
                    ImGui.PushStyleColor(ImGuiCol.HeaderHovered, GetColorForLogEventLevel(line.Level));
                }

                ImGui.Selectable(
                    "###console_null",
                    true,
                    ImGuiSelectableFlags.AllowItemOverlap | ImGuiSelectableFlags.SpanAllColumns);

                // This must be after ImGui.Selectable, it uses ImGui.IsItem... functions
                this.HandleCopyMode(i, line);

                ImGui.SameLine();

                ImGui.PopStyleColor(3);

                if (!line.IsMultiline)
                {
                    ImGui.TextUnformatted(line.TimestampString);
                    ImGui.SameLine();

                    ImGui.SetCursorPosX(cursorLogLevel);
                    ImGui.TextUnformatted(GetTextForLogEventLevel(line.Level));
                    ImGui.SameLine();
                }

                ImGui.SetCursorPosX(cursorLogLine);
                line.HighlightMatches ??= (this.compiledLogHighlight ?? this.compiledLogFilter)?.Matches(line.Line);
                if (line.HighlightMatches is { } matches)
                {
                    this.DrawHighlighted(
                        line.Line,
                        matches,
                        ImGui.GetColorU32(ImGuiCol.Text),
                        ImGui.GetColorU32(ImGuiColors.HealerGreen));
                }
                else
                {
                    ImGui.TextUnformatted(line.Line);
                }

                var currentLinePosY = ImGui.GetCursorPosY();
                logLineHeight = currentLinePosY - lastLinePosY;
                lastLinePosY = currentLinePosY;
            }
        }

        this.clipperPtr.End();

        ImGui.PopFont();

        ImGui.PopStyleVar();

        if (!this.autoScroll || ImGui.GetScrollY() < ImGui.GetScrollMaxY())
        {
            ImGui.SetScrollY(ImGui.GetScrollY() - (logLineHeight * this.newRolledLines));
        }

        if (this.autoScroll && ImGui.GetScrollY() >= ImGui.GetScrollMaxY())
        {
            ImGui.SetScrollHereY(1.0f);
        }

        // Draw dividing lines
        var div1Offset = MathF.Round((timestampWidth + (separatorWidth / 2)) - ImGui.GetScrollX());
        var div2Offset = MathF.Round((cursorLogLevel + levelWidth + (separatorWidth / 2)) - ImGui.GetScrollX());
        childDrawList.AddLine(
            new(childPos.X + div1Offset, childPos.Y),
            new(childPos.X + div1Offset, childPos.Y + childSize.Y),
            0x4FFFFFFF,
            1.0f);
        childDrawList.AddLine(
            new(childPos.X + div2Offset, childPos.Y),
            new(childPos.X + div2Offset, childPos.Y + childSize.Y),
            0x4FFFFFFF,
            1.0f);

        ImGui.EndChild();

        var hadColor = false;
        if (this.lastCmdSuccess.HasValue)
        {
            hadColor = true;
            if (this.lastCmdSuccess.Value)
            {
                ImGui.PushStyleColor(ImGuiCol.FrameBg, ImGuiColors.HealerGreen - new Vector4(0, 0, 0, 0.7f));
            }
            else
            {
                ImGui.PushStyleColor(ImGuiCol.FrameBg, ImGuiColors.DalamudRed - new Vector4(0, 0, 0, 0.7f));
            }
        }

        ImGui.SetNextItemWidth(
            ImGui.GetContentRegionAvail().X - sendButtonSize.X -
            (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale));

        var getFocus = false;
        unsafe
        {
            if (ImGui.InputText(
                    "##command_box",
                    ref this.commandText,
                    255,
                    ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion |
                    ImGuiInputTextFlags.CallbackHistory | ImGuiInputTextFlags.CallbackEdit,
                    this.CommandInputCallback))
            {
                NewLogEntries.Enqueue((this.commandText, new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, new MessageTemplate(string.Empty, []), [])));
                this.ProcessCommand();
                getFocus = true;
            }

            ImGui.SameLine();
        }

        ImGui.SetItemDefaultFocus();
        if (getFocus) ImGui.SetKeyboardFocusHere(-1); // Auto focus previous widget

        if (hadColor) ImGui.PopStyleColor();

        if (ImGui.Button("Send", sendButtonSize))
        {
            this.ProcessCommand();
        }
    }

    private static string GetTextForLogEventLevel(LogEventLevel level) => level switch
    {
        LogEventLevel.Error => "ERR",
        LogEventLevel.Verbose => "VRB",
        LogEventLevel.Debug => "DBG",
        LogEventLevel.Information => "INF",
        LogEventLevel.Warning => "WRN",
        LogEventLevel.Fatal => "FTL",
        _ => "???",
    };

    private static uint GetColorForLogEventLevel(LogEventLevel level) => level switch
    {
        LogEventLevel.Error => 0x800000EE,
        LogEventLevel.Verbose => 0x00000000,
        LogEventLevel.Debug => 0x00000000,
        LogEventLevel.Information => 0x00000000,
        LogEventLevel.Warning => 0x8A0070EE,
        LogEventLevel.Fatal => 0x800000EE,
        _ => 0x30FFFFFF,
    };

    private void FrameworkOnUpdate(IFramework framework)
    {
        if (this.pendingClearLog)
        {
            this.pendingClearLog = false;
            this.logText.Clear();
            this.filteredLogEntries.Clear();
            NewLogEntries.Clear();
        }

        if (this.pendingRefilter)
        {
            this.pendingRefilter = false;
            this.filteredLogEntries.Clear();
            foreach (var log in this.logText)
            {
                if (this.IsFilterApplicable(log))
                    this.filteredLogEntries.Add(log);
            }
        }

        var numPrevFilteredLogEntries = this.filteredLogEntries.Count;
        var addedLines = 0;
        while (NewLogEntries.TryDequeue(out var logLine))
            addedLines += this.HandleLogLine(logLine.Line, logLine.LogEvent);
        this.newRolledLines = addedLines - (this.filteredLogEntries.Count - numPrevFilteredLogEntries);
    }

    private void HandleCopyMode(int i, LogEntry line)
    {
        var selectionChanged = false;

        // If copyStart is -1, it means a drag has not been started yet, let's start one, and select the starting spot.
        if (this.copyMode && this.copyStart == -1 && ImGui.IsItemClicked())
        {
            this.copyStart = i;
            line.SelectedForCopy = !line.SelectedForCopy;

            selectionChanged = true;
        }

        // Update the selected range when dragging over entries
        if (this.copyMode && this.copyStart != -1 && ImGui.IsItemHovered() &&
            ImGui.IsMouseDragging(ImGuiMouseButton.Left))
        {
            if (!line.SelectedForCopy)
            {
                foreach (var index in Enumerable.Range(0, this.filteredLogEntries.Count))
                {
                    if (this.copyStart < i)
                    {
                        this.filteredLogEntries[index].SelectedForCopy = index >= this.copyStart && index <= i;
                    }
                    else
                    {
                        this.filteredLogEntries[index].SelectedForCopy = index >= i && index <= this.copyStart;
                    }
                }

                selectionChanged = true;
            }
        }

        // Finish the drag, we should have already marked all dragged entries as selected by now.
        if (this.copyMode && this.copyStart != -1 && ImGui.IsItemHovered() &&
            ImGui.IsMouseReleased(ImGuiMouseButton.Left))
        {
            this.copyStart = -1;
        }

        if (selectionChanged)
            this.CopyFilteredLogEntries(true);
    }

    private void CopyFilteredLogEntries(bool selectedOnly)
    {
        var sb = new StringBuilder();
        var n = 0;
        foreach (var entry in this.filteredLogEntries)
        {
            if (selectedOnly && !entry.SelectedForCopy)
                continue;

            n++;
            sb.AppendLine(entry.ToString());
        }

        if (n == 0)
            return;

        ImGui.SetClipboardText(sb.ToString());
        this.prevCopyNotification?.DismissNow();
        this.prevCopyNotification = Service<NotificationManager>.Get().AddNotification(
            new()
            {
                Title = this.WindowName,
                Content = $"{n:n0} line(s) copied.",
                Type = NotificationType.Success,
            });
    }

    private void DrawOptionsToolbar()
    {
        ImGui.PushItemWidth(150.0f * ImGuiHelpers.GlobalScale);
        if (ImGui.BeginCombo("##log_level", $"{EntryPoint.LogLevelSwitch.MinimumLevel}+"))
        {
            foreach (var value in Enum.GetValues<LogEventLevel>())
            {
                if (ImGui.Selectable(value.ToString(), value == EntryPoint.LogLevelSwitch.MinimumLevel))
                {
                    EntryPoint.LogLevelSwitch.MinimumLevel = value;
                    this.configuration.LogLevel = value;
                    this.configuration.QueueSave();
                    this.QueueRefilter();
                }
            }

            ImGui.EndCombo();
        }

        ImGui.SameLine();

        var settingsPopup = ImGui.BeginPopup("##console_settings");
        if (settingsPopup)
        {
            this.DrawSettingsPopup();
            ImGui.EndPopup();
        }
        else if (this.settingsPopupWasOpen)
        {
            // Prevent side effects in case Apply wasn't clicked
            this.logLinesLimit = this.configuration.LogLinesLimit;
        }

        this.settingsPopupWasOpen = settingsPopup;

        if (this.DrawToggleButtonWithTooltip("show_settings", "Show settings", FontAwesomeIcon.List, ref settingsPopup))
            ImGui.OpenPopup("##console_settings");

        ImGui.SameLine();

        if (this.DrawToggleButtonWithTooltip(
                "show_filters",
                "Show filter toolbar",
                FontAwesomeIcon.Search,
                ref this.showFilterToolbar))
        {
            this.showFilterToolbar = !this.showFilterToolbar;
        }

        ImGui.SameLine();

        if (this.DrawToggleButtonWithTooltip(
                "show_uncaught_exceptions",
                "Show uncaught exception while filtering",
                FontAwesomeIcon.Bug,
                ref this.filterShowUncaughtExceptions))
        {
            this.filterShowUncaughtExceptions = !this.filterShowUncaughtExceptions;
        }

        ImGui.SameLine();

        if (ImGuiComponents.IconButton("clear_log", FontAwesomeIcon.Trash))
        {
            this.QueueClear();
        }

        if (ImGui.IsItemHovered()) ImGui.SetTooltip("Clear Log");

        ImGui.SameLine();

        if (this.DrawToggleButtonWithTooltip(
                "copy_mode",
                "Enable Copy Mode\nRight-click to copy entire log",
                FontAwesomeIcon.Copy,
                ref this.copyMode))
        {
            this.copyMode = !this.copyMode;

            if (!this.copyMode)
            {
                foreach (var entry in this.filteredLogEntries)
                {
                    entry.SelectedForCopy = false;
                }
            }
        }

        if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
            this.CopyFilteredLogEntries(false);

        ImGui.SameLine();
        if (this.killGameArmed)
        {
            if (ImGuiComponents.IconButton(FontAwesomeIcon.ExclamationTriangle))
                Process.GetCurrentProcess().Kill();
        }
        else
        {
            if (ImGuiComponents.IconButton(FontAwesomeIcon.Stop))
                this.killGameArmed = true;
        }

        if (ImGui.IsItemHovered()) ImGui.SetTooltip("Kill game");

        ImGui.SameLine();

        var inputWidth = 200.0f * ImGuiHelpers.GlobalScale;
        var nextCursorPosX = ImGui.GetContentRegionMax().X - (2 * inputWidth) - ImGui.GetStyle().ItemSpacing.X;
        var breakInputLines = nextCursorPosX < 0;
        if (ImGui.GetCursorPosX() > nextCursorPosX)
        {
            ImGui.NewLine();
            inputWidth = ImGui.GetWindowWidth() - (ImGui.GetStyle().WindowPadding.X * 2);

            if (!breakInputLines)
                inputWidth = (inputWidth - ImGui.GetStyle().ItemSpacing.X) / 2;
        }
        else
        {
            ImGui.SetCursorPosX(nextCursorPosX);
        }

        ImGui.PushItemWidth(inputWidth);
        if (ImGui.InputTextWithHint(
                "##textHighlight",
                "regex highlight",
                ref this.textHighlight,
                2048,
                ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)
            || ImGui.IsItemDeactivatedAfterEdit())
        {
            this.compiledLogHighlight = null;
            this.exceptionLogHighlight = null;
            try
            {
                if (this.textHighlight != string.Empty)
                    this.compiledLogHighlight = new(this.textHighlight, RegexOptions.IgnoreCase);
            }
            catch (Exception e)
            {
                this.exceptionLogHighlight = e;
            }

            foreach (var log in this.logText)
                log.HighlightMatches = null;
        }

        if (!breakInputLines)
            ImGui.SameLine();

        ImGui.PushItemWidth(inputWidth);
        if (ImGui.InputTextWithHint(
                "##textFilter",
                "regex global filter",
                ref this.textFilter,
                2048,
                ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)
            || ImGui.IsItemDeactivatedAfterEdit())
        {
            this.RecompileLogFilter();
        }
    }

    private void RecompileLogFilter()
    {
        this.compiledLogFilter = null;
        this.exceptionLogFilter = null;
        try
        {
            this.compiledLogFilter = new(this.textFilter, RegexOptions.IgnoreCase);

            this.QueueRefilter();
        }
        catch (Exception e)
        {
            this.exceptionLogFilter = e;
        }

        foreach (var log in this.logText)
            log.HighlightMatches = null;
    }

    private void DrawSettingsPopup()
    {
        if (ImGui.Checkbox("Open at startup", ref this.autoOpen))
        {
            this.configuration.LogOpenAtStartup = this.autoOpen;
            this.configuration.QueueSave();
        }

        if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll))
        {
            this.configuration.LogAutoScroll = this.autoScroll;
            this.configuration.QueueSave();
        }

        ImGui.TextUnformatted("Logs buffer");
        ImGui.SliderInt("lines", ref this.logLinesLimit, LogLinesMinimum, LogLinesMaximum);
        if (ImGui.Button("Apply"))
        {
            this.logLinesLimit = Math.Max(LogLinesMinimum, this.logLinesLimit);

            this.configuration.LogLinesLimit = this.logLinesLimit;
            this.configuration.QueueSave();

            ImGui.CloseCurrentPopup();
        }
    }

    private void DrawFilterToolbar()
    {
        if (!this.showFilterToolbar) return;

        PluginFilterEntry? removalEntry = null;
        using var table = ImRaii.Table(
            "plugin_filter_entries",
            4,
            ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV);
        if (!table) return;

        ImGui.TableSetupColumn("##remove_button", ImGuiTableColumnFlags.WidthFixed, 25.0f * ImGuiHelpers.GlobalScale);
        ImGui.TableSetupColumn("##source_name", ImGuiTableColumnFlags.WidthFixed, 150.0f * ImGuiHelpers.GlobalScale);
        ImGui.TableSetupColumn("##log_level", ImGuiTableColumnFlags.WidthFixed, 150.0f * ImGuiHelpers.GlobalScale);
        ImGui.TableSetupColumn("##filter_text", ImGuiTableColumnFlags.WidthStretch);

        ImGui.TableNextColumn();
        if (ImGuiComponents.IconButton("add_entry", FontAwesomeIcon.Plus))
        {
            if (this.pluginFilters.All(entry => entry.Source != this.selectedSource))
            {
                this.pluginFilters.Add(
                    new PluginFilterEntry
                    {
                        Source = this.selectedSource,
                        Filter = string.Empty,
                        Level = LogEventLevel.Debug,
                    });
            }

            this.QueueRefilter();
        }

        ImGui.TableNextColumn();
        ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X);
        if (ImGui.BeginCombo("##Sources", this.selectedSource, ImGuiComboFlags.HeightLarge))
        {
            var sourceNames = Service<PluginManager>.Get().InstalledPlugins
                                                    .Select(p => p.Manifest.InternalName)
                                                    .OrderBy(s => s)
                                                    .Prepend("DalamudInternal")
                                                    .Where(
                                                        name => this.pluginFilter is "" || new FuzzyMatcher(
                                                                    this.pluginFilter.ToLowerInvariant(),
                                                                    MatchMode.Fuzzy).Matches(name.ToLowerInvariant()) !=
                                                                0)
                                                    .ToList();

            ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X);
            ImGui.InputTextWithHint("##PluginSearchFilter", "Filter Plugin List", ref this.pluginFilter, 2048);
            ImGui.Separator();

            if (!sourceNames.Any())
            {
                ImGui.TextColored(ImGuiColors.DalamudRed, "No Results");
            }

            foreach (var selectable in sourceNames)
            {
                if (ImGui.Selectable(selectable, this.selectedSource == selectable))
                {
                    this.selectedSource = selectable;
                }
            }

            ImGui.EndCombo();
        }

        ImGui.TableNextColumn();
        ImGui.TableNextColumn();

        foreach (var entry in this.pluginFilters)
        {
            ImGui.PushID(entry.Source);

            ImGui.TableNextColumn();
            if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash))
            {
                removalEntry = entry;
            }

            ImGui.TableNextColumn();
            ImGui.Text(entry.Source);

            ImGui.TableNextColumn();
            ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
            if (ImGui.BeginCombo("##levels", $"{entry.Level}+"))
            {
                foreach (var value in Enum.GetValues<LogEventLevel>())
                {
                    if (ImGui.Selectable(value.ToString(), value == entry.Level))
                    {
                        entry.Level = value;
                        this.QueueRefilter();
                    }
                }

                ImGui.EndCombo();
            }

            ImGui.TableNextColumn();
            ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
            var entryFilter = entry.Filter;
            if (ImGui.InputTextWithHint(
                    "##filter",
                    $"{entry.Source} regex filter",
                    ref entryFilter,
                    2048,
                    ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)
                || ImGui.IsItemDeactivatedAfterEdit())
            {
                entry.Filter = entryFilter;
                if (entry.FilterException is null)
                    this.QueueRefilter();
            }

            ImGui.PopID();
        }

        if (removalEntry is { } toRemove)
        {
            this.pluginFilters.Remove(toRemove);
            this.QueueRefilter();
        }
    }

    private void ProcessCommand()
    {
        try
        {
            if (string.IsNullOrEmpty(this.commandText))
                return;

            this.historyPos = -1;

            if (this.commandText != this.configuration.LogCommandHistory.LastOrDefault())
                this.configuration.LogCommandHistory.Add(this.commandText);

            if (this.configuration.LogCommandHistory.Count > HistorySize)
                this.configuration.LogCommandHistory.RemoveAt(0);

            this.configuration.QueueSave();

            this.lastCmdSuccess = Service<ConsoleManager>.Get().ProcessCommand(this.commandText);
            this.commandText = string.Empty;

            // TODO: Force scroll to bottom
        }
        catch (Exception ex)
        {
            Log.Error(ex, "Error during command dispatch");
            this.lastCmdSuccess = false;
        }
    }

    private unsafe int CommandInputCallback(ImGuiInputTextCallbackData* data)
    {
        var ptr = new ImGuiInputTextCallbackDataPtr(data);

        switch (data->EventFlag)
        {
            case ImGuiInputTextFlags.CallbackEdit:
                this.completionZipText = null;
                this.completionTabIdx = 0;
                break;

            case ImGuiInputTextFlags.CallbackCompletion:
                var textBytes = new byte[data->BufTextLen];
                Marshal.Copy((IntPtr)data->Buf, textBytes, 0, data->BufTextLen);
                var text = Encoding.UTF8.GetString(textBytes);

                var words = text.Split();

                // We can't do any completion for parameters at the moment since it just calls into CommandHandler
                if (words.Length > 1)
                    return 0;

                var wordToComplete = words[0];
                if (wordToComplete.IsNullOrWhitespace())
                    return 0;

                if (this.completionZipText is not null)
                    wordToComplete = this.completionZipText;

                // TODO: Improve this, add partial completion, arguments, description, etc.
                // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L6443-L6484
                var candidates = Service<ConsoleManager>.Get().Entries
                                                        .Where(x => x.Key.StartsWith(wordToComplete))
                                                        .Select(x => x.Key);

                candidates = candidates.Union(
                    Service<CommandManager>.Get().Commands
                                           .Where(x => x.Key.StartsWith(wordToComplete)).Select(x => x.Key))
                                       .ToArray();

                if (candidates.Any())
                {
                    string? toComplete = null;
                    if (this.completionZipText == null)
                    {
                        // Find the "common" prefix of all matches
                        toComplete = candidates.Aggregate(
                            (prefix, candidate) => string.Concat(prefix.Zip(candidate, (a, b) => a == b ? a : '\0')));

                        this.completionZipText = toComplete;
                    }
                    else
                    {
                        toComplete = candidates.ElementAt(this.completionTabIdx);
                        this.completionTabIdx = (this.completionTabIdx + 1) % candidates.Count();
                    }

                    if (toComplete != null)
                    {
                        ptr.DeleteChars(0, ptr.BufTextLen);
                        ptr.InsertChars(0, toComplete);
                    }
                }

                break;

            case ImGuiInputTextFlags.CallbackHistory:
                var prevPos = this.historyPos;

                if (ptr.EventKey == ImGuiKey.UpArrow)
                {
                    if (this.historyPos == -1)
                        this.historyPos = this.configuration.LogCommandHistory.Count - 1;
                    else if (this.historyPos > 0)
                        this.historyPos--;
                }
                else if (data->EventKey == ImGuiKey.DownArrow)
                {
                    if (this.historyPos != -1)
                    {
                        if (++this.historyPos >= this.configuration.LogCommandHistory.Count)
                        {
                            this.historyPos = -1;
                        }
                    }
                }

                if (prevPos != this.historyPos)
                {
                    var historyStr = this.historyPos >= 0 ? this.configuration.LogCommandHistory[this.historyPos] : string.Empty;

                    ptr.DeleteChars(0, ptr.BufTextLen);
                    ptr.InsertChars(0, historyStr);
                }

                break;
        }

        return 0;
    }

    /// <summary>Add a log entry to the display.</summary>
    /// <param name="line">The line to add.</param>
    /// <param name="logEvent">The Serilog event associated with this line.</param>
    /// <returns>Number of lines added to <see cref="filteredLogEntries"/>.</returns>
    private int HandleLogLine(string line, LogEvent logEvent)
    {
        ThreadSafety.DebugAssertMainThread();

        // These lines are too huge, and only useful for troubleshooting after the game exist.
        if (line.StartsWith("TROUBLESHOOTING:") || line.StartsWith("LASTEXCEPTION:"))
            return 0;

        // Create a log entry template.
        var entry = new LogEntry
        {
            Level = logEvent.Level,
            TimeStamp = logEvent.Timestamp,
            HasException = logEvent.Exception != null,
        };

        if (logEvent.Properties.ContainsKey("Dalamud.ModuleName"))
        {
            entry.Source = "DalamudInternal";
        }
        else if (logEvent.Properties.TryGetValue("Dalamud.PluginName", out var sourceProp) &&
                 sourceProp is ScalarValue { Value: string sourceValue })
        {
            entry.Source = sourceValue;
        }

        var ssp = line.AsSpan();
        var numLines = 0;
        while (true)
        {
            var next = ssp.IndexOfAny('\r', '\n');
            if (next == -1)
            {
                // Last occurrence; transfer the ownership of the new entry to the queue.
                entry.Line = ssp.ToString();
                numLines += this.AddAndFilter(entry);
                break;
            }

            // There will be more; create a clone of the entry with the current line.
            numLines += this.AddAndFilter(entry with { Line = ssp[..next].ToString() });

            // Mark further lines as multiline.
            entry.IsMultiline = true;

            // Skip the detected line break.
            ssp = ssp[next..];
            ssp = ssp.StartsWith("\r\n") ? ssp[2..] : ssp[1..];
        }

        return numLines;
    }

    /// <summary>Adds a line to the log list and the filtered log list accordingly.</summary>
    /// <param name="entry">The new log entry to add.</param>
    /// <returns>Number of lines added to <see cref="filteredLogEntries"/>.</returns>
    private int AddAndFilter(LogEntry entry)
    {
        ThreadSafety.DebugAssertMainThread();

        this.logText.Add(entry);

        if (!this.IsFilterApplicable(entry))
            return 0;

        this.filteredLogEntries.Add(entry);
        return 1;
    }

    /// <summary>Determines if a log entry passes the user-specified filter.</summary>
    /// <param name="entry">The entry to test.</param>
    /// <returns><c>true</c> if it passes the filter.</returns>
    private bool IsFilterApplicable(LogEntry entry)
    {
        ThreadSafety.DebugAssertMainThread();

        if (this.exceptionLogFilter is not null)
            return false;

        // If this entry is below a newly set minimum level, fail it
        if (EntryPoint.LogLevelSwitch.MinimumLevel > entry.Level)
            return false;

        // Show exceptions that weren't properly tagged with a Source (generally meaning they were uncaught)
        // After log levels because uncaught exceptions should *never* fall below Error.
        if (this.filterShowUncaughtExceptions && entry.HasException && entry.Source == null)
            return true;

        // (global filter) && (plugin filter) must be satisfied.
        var wholeCond = true;

        // If we have a global filter, check that first
        if (this.compiledLogFilter is { } logFilter)
        {
            // Someone will definitely try to just text filter a source without using the actual filters, should allow that.
            var matchesSource = entry.Source is not null && logFilter.IsMatch(entry.Source);
            var matchesContent = logFilter.IsMatch(entry.Line);

            wholeCond &= matchesSource || matchesContent;
        }

        // If this entry has a filter, check the filter
        if (this.pluginFilters.Count > 0)
        {
            var matchesAny = false;

            foreach (var filterEntry in this.pluginFilters)
            {
                if (!string.Equals(filterEntry.Source, entry.Source, StringComparison.InvariantCultureIgnoreCase))
                    continue;

                var allowedLevel = filterEntry.Level <= entry.Level;
                var matchesContent = filterEntry.FilterRegex?.IsMatch(entry.Line) is not false;

                matchesAny |= allowedLevel && matchesContent;
                if (matchesAny)
                    break;
            }

            wholeCond &= matchesAny;
        }

        return wholeCond;
    }

    /// <summary>Queues clearing the window of all log entries, before next call to <see cref="Draw"/>.</summary>
    private void QueueClear() => this.pendingClearLog = true;

    /// <summary>Queues filtering the log entries again, before next call to <see cref="Draw"/>.</summary>
    private void QueueRefilter() => this.pendingRefilter = true;

    private bool DrawToggleButtonWithTooltip(
        string buttonId, string tooltip, FontAwesomeIcon icon, ref bool enabledState)
    {
        var result = false;

        var buttonEnabled = enabledState;
        if (buttonEnabled) ImGui.PushStyleColor(ImGuiCol.Button, ImGuiColors.HealerGreen with { W = 0.25f });
        if (ImGuiComponents.IconButton(buttonId, icon))
        {
            result = true;
        }

        if (ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip);

        if (buttonEnabled) ImGui.PopStyleColor();

        return result;
    }

    private void OnDalamudConfigurationSaved(DalamudConfiguration dalamudConfiguration)
    {
        this.logLinesLimit = dalamudConfiguration.LogLinesLimit;
        var limit = Math.Max(LogLinesMinimum, this.logLinesLimit);
        this.logText.Size = limit;
        this.filteredLogEntries.Size = limit;
    }

    private unsafe void DrawHighlighted(
        ReadOnlySpan<char> line,
        MatchCollection matches,
        uint col,
        uint highlightCol)
    {
        Span<int> charOffsets = stackalloc int[(matches.Count * 2) + 2];
        var charOffsetsIndex = 1;
        for (var j = 0; j < matches.Count; j++)
        {
            var g = matches[j].Groups[0];
            charOffsets[charOffsetsIndex++] = g.Index;
            charOffsets[charOffsetsIndex++] = g.Index + g.Length;
        }

        charOffsets[charOffsetsIndex++] = line.Length;

        var screenPos = ImGui.GetCursorScreenPos();
        var drawList = ImGui.GetWindowDrawList().NativePtr;
        var font = ImGui.GetFont();
        var size = ImGui.GetFontSize();
        var scale = size / font.FontSize;
        var hotData = font.IndexedHotDataWrapped();
        var lookup = font.IndexLookupWrapped();
        var kern = (ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NoKerning) == 0;
        var lastc = '\0';
        for (var i = 0; i < charOffsetsIndex - 1; i++)
        {
            var begin = charOffsets[i];
            var end = charOffsets[i + 1];
            if (begin == end)
                continue;

            for (var j = begin; j < end; j++)
            {
                var currc = line[j];
                if (currc >= lookup.Length || lookup[currc] == ushort.MaxValue)
                    currc = (char)font.FallbackChar;

                if (kern)
                    screenPos.X += scale * ImGui.GetFont().GetDistanceAdjustmentForPair(lastc, currc);
                font.RenderChar(drawList, size, screenPos, i % 2 == 1 ? highlightCol : col, currc);

                screenPos.X += scale * hotData[currc].AdvanceX;
                lastc = currc;
            }
        }

        ImGui.Dummy(screenPos - ImGui.GetCursorScreenPos());
    }

    private record LogEntry
    {
        public string Line { get; set; } = string.Empty;

        public LogEventLevel Level { get; init; }

        public DateTimeOffset TimeStamp { get; init; }

        public bool IsMultiline { get; set; }

        /// <summary>
        /// Gets or sets the system responsible for generating this log entry. Generally will be a plugin's
        /// InternalName.
        /// </summary>
        public string? Source { get; set; }

        public bool SelectedForCopy { get; set; }

        public bool HasException { get; init; }

        public MatchCollection? HighlightMatches { get; set; }

        public string TimestampString => this.TimeStamp.ToString("HH:mm:ss.fff");

        public override string ToString() =>
            this.IsMultiline
                ? $"\t{this.Line}"
                : $"{this.TimestampString} | {GetTextForLogEventLevel(this.Level)} | {this.Line}";
    }

    private class PluginFilterEntry
    {
        private string filter = string.Empty;

        public string Source { get; init; } = string.Empty;

        public string Filter
        {
            get => this.filter;
            set
            {
                this.filter = value;
                this.FilterRegex = null;
                this.FilterException = null;
                if (value == string.Empty)
                    return;

                try
                {
                    this.FilterRegex = new(value, RegexOptions.IgnoreCase);
                }
                catch (Exception e)
                {
                    this.FilterException = e;
                }
            }
        }

        public LogEventLevel Level { get; set; }

        public Regex? FilterRegex { get; private set; }

        public Exception? FilterException { get; private set; }
    }
}
