import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.Dock
import qs.Modules.MainScreen

SmartPanel {
  id: root
  panelBackgroundColor: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity)

  readonly property string dockPosition: Settings.data.dock.position
  readonly property bool isVertical: dockPosition === "left" || dockPosition === "right"
  readonly property bool hasBar: modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
  readonly property bool barAtSameEdge: hasBar && Settings.getBarPositionForScreen(modelData?.name) === dockPosition
  readonly property bool isFramed: Settings.data.bar.barType === "framed" && hasBar
  property bool isDockHovered: false
  property bool panelHovered: false
  readonly property int iconSize: Math.round(12 + 24 * (Settings.data.dock.size ?? 1))
  readonly property int maxWidth: screen ? screen.width * 0.8 : 1000
  readonly property int maxHeight: screen ? screen.height * 0.8 : 1000
  readonly property bool autoHide: false
  readonly property int hideDelay: 1000
  readonly property int showDelay: 100

  // Shared state with dock content
  property bool dockHovered: false
  property bool anyAppHovered: false
  property bool menuHovered: false
  property bool hidden: false
  property bool peekHovered: false

  // Track the currently open context menu
  property var currentContextMenu: null

  // Combined model of running apps and pinned apps
  property var dockApps: []
  property var groupCycleIndices: ({})

  // Track the session order of apps (transient reordering)
  property var sessionAppOrder: []

  // Drag and Drop state for visual feedback
  property int dragSourceIndex: -1
  property int dragTargetIndex: -1

  // Revision counter to force icon re-evaluation
  property int iconRevision: 0

  property alias hideTimer: hideTimer
  property alias showTimer: showTimer

  onMenuHoveredChanged: {
    if (!menuHovered && !panelHovered) {
      hoverCloseTimer.restart();
    }
  }

  onAnyAppHoveredChanged: {
    if (anyAppHovered) {
      hoverCloseTimer.stop();
    } else if (!panelHovered && !menuHovered && !dockHovered && !isDockHovered) {
      hoverCloseTimer.restart();
    }
  }

  onClosed: {
    hoverCloseTimer.stop();
    isDockHovered = false;
  }

  onOpened: {
    // Don't auto-start close timer here — the peek zone's onExited in Dock.qml
    // starts hideTimer which checks panel.isDockHovered, giving the user time
    // to move into the panel without it closing prematurely.
  }

  panelAnchorTop: dockPosition === "top"
  panelAnchorBottom: dockPosition === "bottom"
  panelAnchorLeft: dockPosition === "left"
  panelAnchorRight: dockPosition === "right"
  panelAnchorHorizontalCenter: !isVertical
  panelAnchorVerticalCenter: isVertical

  forceAttachToBar: hasBar
  exclusiveKeyboard: false

  // when dragging ended but the cursor is outside the dock area, restart the timer
  onDragSourceIndexChanged: {
    if (dragSourceIndex === -1) {
      if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) {
        hideTimer.restart();
      }
    }
  }

  // Function to close any open context menu
  function closeAllContextMenus() {
    if (currentContextMenu && currentContextMenu.visible) {
      currentContextMenu.hide();
    }
  }

  function getAppKey(appData) {
    if (!appData)
      return null;

    if (Settings.data.dock.groupApps) {
      return appData.appId;
    }

    // Use stable appId for pinned apps to maintain their slot regardless of running state
    if (appData.type === "pinned" || appData.type === "pinned-running") {
      return appData.appId;
    }

    // prefer toplevel object identity for unpinned running apps to distinguish instances
    if (appData.toplevel)
      return appData.toplevel;

    // fallback to appId
    return appData.appId;
  }

  function sortDockApps(apps) {
    if (!sessionAppOrder || sessionAppOrder.length === 0) {
      return apps;
    }

    const sorted = [];
    const remaining = [...apps];

    // Pick apps that are in the session order
    for (let i = 0; i < sessionAppOrder.length; i++) {
      const key = sessionAppOrder[i];

      // Pick ALL matching apps (e.g. all instances of a pinned app)
      while (true) {
        const idx = remaining.findIndex(app => getAppKey(app) === key);
        if (idx !== -1) {
          sorted.push(remaining[idx]);
          remaining.splice(idx, 1);
        } else {
          break;
        }
      }
    }

    // Append any new/remaining apps
    remaining.forEach(app => sorted.push(app));

    return sorted;
  }

  function reorderApps(fromIndex, toIndex) {
    if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= dockApps.length || toIndex >= dockApps.length)
      return;

    const list = [...dockApps];
    const item = list.splice(fromIndex, 1)[0];
    list.splice(toIndex, 0, item);

    dockApps = list;
    sessionAppOrder = dockApps.map(getAppKey);
    savePinnedOrder();
  }

  function savePinnedOrder() {
    const currentPinned = Settings.data.dock.pinnedApps || [];
    const newPinned = [];
    const seen = new Set();

    // Extract pinned apps in their current visual order
    dockApps.forEach(app => {
                       if (app.appId && !seen.has(app.appId)) {
                         const isPinned = currentPinned.some(p => normalizeAppId(p) === normalizeAppId(app.appId));

                         if (isPinned) {
                           newPinned.push(app.appId);
                           seen.add(app.appId);
                         }
                       }
                     });

    // Check if any pinned apps were missed (unlikely if dockApps is correct)
    currentPinned.forEach(p => {
                            if (!seen.has(p)) {
                              newPinned.push(p);
                              seen.add(p);
                            }
                          });

    if (JSON.stringify(currentPinned) !== JSON.stringify(newPinned)) {
      Settings.data.dock.pinnedApps = newPinned;
    }
  }

  // Helper function to normalize app IDs for case-insensitive matching
  function normalizeAppId(appId) {
    if (!appId || typeof appId !== 'string')
      return "";
    let id = appId.toLowerCase().trim();
    if (id.endsWith(".desktop"))
      id = id.substring(0, id.length - 8);
    return id;
  }

  // Helper function to check if an app ID matches a pinned app (case-insensitive)
  function isAppIdPinned(appId, pinnedApps) {
    if (!appId || !pinnedApps || pinnedApps.length === 0)
      return false;
    const normalizedId = normalizeAppId(appId);
    // Direct match
    if (pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId))
      return true;
    // Resolve via desktop entry lookup (handles StartupWMClass != .desktop filename)
    const resolved = resolveToDesktopEntryId(appId);
    if (resolved !== appId) {
      const normalizedResolved = normalizeAppId(resolved);
      return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedResolved);
    }
    return false;
  }

  // Desktop entry ID resolution cache (cleared when DesktopEntries change)
  property var _desktopEntryIdCache: ({})

  // Resolve a toplevel appId to its canonical .desktop entry ID via heuristic lookup.
  function resolveToDesktopEntryId(appId) {
    if (!appId)
      return appId;
    if (_desktopEntryIdCache.hasOwnProperty(appId))
      return _desktopEntryIdCache[appId];
    try {
      if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) {
        const entry = DesktopEntries.heuristicLookup(appId);
        if (entry && entry.id) {
          _desktopEntryIdCache[appId] = entry.id;
          return entry.id;
        }
      }
    } catch (e) {}
    _desktopEntryIdCache[appId] = appId;
    return appId;
  }

  // Helper function to get app name from desktop entry
  function getAppNameFromDesktopEntry(appId) {
    if (!appId)
      return appId;

    try {
      if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) {
        const entry = DesktopEntries.heuristicLookup(appId);
        if (entry && entry.name) {
          return entry.name;
        }
      }

      if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId) {
        const entry = DesktopEntries.byId(appId);
        if (entry && entry.name) {
          return entry.name;
        }
      }
    } catch (e)
      // Fall through to return original appId
    {}

    // Return original appId if we can't find a desktop entry
    return appId;
  }

  function getToplevelsForEntry(appData) {
    if (!appData)
      return [];

    if (appData.toplevels && appData.toplevels.length > 0) {
      return appData.toplevels.filter(toplevel => toplevel && (!Settings.data.dock.onlySameOutput || !toplevel.screens || toplevel.screens.includes(screen)));
    }

    if (!appData.toplevel)
      return [];

    if (Settings.data.dock.onlySameOutput && appData.toplevel.screens && !appData.toplevel.screens.includes(screen))
      return [];

    return [appData.toplevel];
  }

  function getPrimaryToplevelForEntry(appData) {
    const toplevels = getToplevelsForEntry(appData);
    if (toplevels.length === 0)
      return null;

    if (ToplevelManager && ToplevelManager.activeToplevel && toplevels.includes(ToplevelManager.activeToplevel))
      return ToplevelManager.activeToplevel;

    return toplevels[0];
  }

  // Build grouped render model without mutating the raw toplevel list.
  function buildGroupedDockApps(apps) {
    if (!Settings.data.dock.groupApps) {
      return apps.map(app => {
                        const entry = Object.assign({}, app);
                        entry.toplevels = getToplevelsForEntry(app);
                        return entry;
                      });
    }

    const grouped = [];
    const groupedById = new Map();

    apps.forEach(app => {
                   const appId = app.appId;
                   const toplevels = getToplevelsForEntry(app);
                   const existing = groupedById.get(appId);

                   if (existing) {
                     toplevels.forEach(toplevel => {
                                         if (!existing.toplevels.includes(toplevel)) {
                                           existing.toplevels.push(toplevel);
                                         }
                                       });
                     if (app.type === "pinned" || app.type === "pinned-running") {
                       existing.isPinned = true;
                     }
                   } else {
                     const entry = {
                       "type": app.type,
                       "appId": appId,
                       "title": app.title,
                       "toplevels": toplevels.slice(),
                       "isPinned": app.type === "pinned" || app.type === "pinned-running"
                     };
                     grouped.push(entry);
                     groupedById.set(appId, entry);
                   }
                 });

    grouped.forEach(entry => {
                      entry.toplevel = getPrimaryToplevelForEntry(entry);
                      if (entry.toplevels.length > 0 && entry.isPinned) {
                        entry.type = "pinned-running";
                      } else if (entry.toplevels.length > 0) {
                        entry.type = "running";
                      } else {
                        entry.type = "pinned";
                      }
                      if (entry.toplevel && entry.toplevel.title && entry.toplevel.title.trim() !== "") {
                        entry.title = entry.toplevel.title;
                      }
                    });

    return grouped;
  }

  // Function to update the combined dock apps model
  function updateDockApps() {
    const runningApps = ToplevelManager ? (ToplevelManager.toplevels.values || []) : [];
    const pinnedApps = Settings.data.dock.pinnedApps || [];
    const combined = [];
    const processedToplevels = new Set();
    const processedPinnedAppIds = new Set();

    // push an app onto combined with the given appType
    function pushApp(appType, toplevel, appId, title) {
      // Use canonical ID for pinned apps to ensure key stability
      const canonicalId = isAppIdPinned(appId, pinnedApps) ? (pinnedApps.find(p => normalizeAppId(p) === normalizeAppId(appId)) || appId) : appId;

      // For running apps, track by toplevel object to allow multiple instances
      if (toplevel) {
        if (processedToplevels.has(toplevel)) {
          return;
        }
        if (Settings.data.dock.onlySameOutput && toplevel.screens && !toplevel.screens.includes(screen)) {
          return;
        }
        combined.push({
                        "type": appType,
                        "toplevel": toplevel,
                        "toplevels": toplevel ? [toplevel] : [],
                        "appId": canonicalId,
                        "title": title
                      });
        processedToplevels.add(toplevel);
      } else {
        // For pinned apps that aren't running, track by appId to avoid duplicates
        if (processedPinnedAppIds.has(canonicalId)) {
          return;
        }
        combined.push({
                        "type": appType,
                        "toplevel": toplevel,
                        "toplevels": [],
                        "appId": canonicalId,
                        "title": title
                      });
        processedPinnedAppIds.add(canonicalId);
      }
    }

    function pushRunning(first) {
      runningApps.forEach(toplevel => {
                            if (toplevel) {
                              // Use robust matching to check if pinned
                              const isPinned = isAppIdPinned(toplevel.appId, pinnedApps);
                              if (!first && isPinned && processedToplevels.has(toplevel)) {
                                return; // Already added by pushPinned()
                              }
                              pushApp((first && isPinned) ? "pinned-running" : "running", toplevel, toplevel.appId, toplevel.title);
                            }
                          });
    }

    function pushPinned() {
      pinnedApps.forEach(pinnedAppId => {
                           // Find all running instances of this pinned app using robust matching
                           // Also resolve toplevel appId via desktop entry lookup to handle
                           // StartupWMClass != .desktop filename (e.g. zen -> zen-browser-bin)
                           const normalizedPinned = normalizeAppId(pinnedAppId);
                           const matchingToplevels = runningApps.filter(app => {
                                                                          if (!app)
                                                                          return false;
                                                                          if (normalizeAppId(app.appId) === normalizedPinned)
                                                                          return true;
                                                                          const resolved = resolveToDesktopEntryId(app.appId);
                                                                          return resolved !== app.appId && normalizeAppId(resolved) === normalizedPinned;
                                                                        });

                           if (matchingToplevels.length > 0) {
                             // Add all running instances as pinned-running
                             matchingToplevels.forEach(toplevel => {
                                                         pushApp("pinned-running", toplevel, pinnedAppId, toplevel.title);
                                                       });
                           } else {
                             // App is pinned but not running - add once
                             pushApp("pinned", null, pinnedAppId, getAppNameFromDesktopEntry(pinnedAppId) || pinnedAppId);
                           }
                         });
    }

    // if pinnedStatic then push all pinned and then all remaining running apps
    if (Settings.data.dock.pinnedStatic) {
      pushPinned();
      pushRunning(false);

      // else add all running apps and then remaining pinned apps
    } else {
      pushRunning(true);
      pushPinned();
    }

    const sortedApps = sortDockApps(combined);
    dockApps = buildGroupedDockApps(sortedApps);
    const cycleState = root.groupCycleIndices || {};
    const nextCycleState = {};
    dockApps.forEach(app => {
                       if (app && app.appId && cycleState[app.appId] !== undefined) {
                         nextCycleState[app.appId] = cycleState[app.appId];
                       }
                     });
    root.groupCycleIndices = nextCycleState;

    // Sync session order if needed
    if (!sessionAppOrder || sessionAppOrder.length === 0) {
      sessionAppOrder = dockApps.map(getAppKey);
    } else {
      const currentKeys = new Set(dockApps.map(getAppKey));
      const existingKeys = new Set();
      const newOrder = [];

      // Keep existing keys that are still present
      sessionAppOrder.forEach(key => {
                                if (currentKeys.has(key)) {
                                  newOrder.push(key);
                                  existingKeys.add(key);
                                }
                              });

      // Add new keys at the end
      dockApps.forEach(app => {
                         const key = getAppKey(app);
                         if (!existingKeys.has(key)) {
                           newOrder.push(key);
                           existingKeys.add(key);
                         }
                       });

      if (JSON.stringify(newOrder) !== JSON.stringify(sessionAppOrder)) {
        sessionAppOrder = newOrder;
      }
    }
  }

  // Update dock apps when toplevels change
  Connections {
    target: ToplevelManager ? ToplevelManager.toplevels : null
    function onValuesChanged() {
      updateDockApps();
    }
  }

  // Update dock apps when pinned apps change
  Connections {
    target: Settings.data.dock
    function onPinnedAppsChanged() {
      updateDockApps();
    }
    function onOnlySameOutputChanged() {
      updateDockApps();
    }
    function onGroupAppsChanged() {
      updateDockApps();
    }
  }

  // Initial update when component is ready
  Component.onCompleted: {
    if (ToplevelManager) {
      updateDockApps();
    }
  }

  // Refresh icons when DesktopEntries becomes available
  Connections {
    target: DesktopEntries.applications
    function onValuesChanged() {
      root.iconRevision++;
      root._desktopEntryIdCache = {};
    }
  }

  Timer {
    id: hideTimer
    interval: hideDelay
    onTriggered: {}
  }

  Timer {
    id: showTimer
    interval: showDelay
    onTriggered: {}
  }

  Timer {
    id: hoverCloseTimer
    interval: hideDelay
    onTriggered: {
      if (root.dockHovered || root.isDockHovered || root.anyAppHovered || root.menuHovered || (root.currentContextMenu && root.currentContextMenu.visible)) {
        restart();
        return;
      }
      root.isDockHovered = false;
      root.close();
    }
  }

  panelContent: Item {
    id: panelContent

    property bool allowAttach: true
    property real frameThickness: isFramed && !barAtSameEdge && !Settings.data.dock.sitOnFrame ? Settings.data.bar.frameThickness : 0
    property real contentPreferredWidth: Math.round(dockContainerWrapper.width) - (isVertical ? frameThickness : 0)
    property real contentPreferredHeight: Math.round(dockContainerWrapper.height) - (!isVertical ? frameThickness : 0)

    Item {
      id: hoverArea
      anchors.fill: dockContainerWrapper
      anchors.margins: -frameThickness

      // Detect hover over dock area including frame thickness
      HoverHandler {
        id: dockHoverArea
        acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
        onHoveredChanged: {
          root.panelHovered = hovered;
          if (hovered) {
            root.isDockHovered = true;
            hoverCloseTimer.stop();
          } else {
            root.isDockHovered = false;
            if (root.menuHovered || (root.currentContextMenu && root.currentContextMenu.visible)) {
              hoverCloseTimer.stop();
            } else {
              hoverCloseTimer.restart();
            }
          }
        }
      }
    }

    Item {
      id: dockContainerWrapper
      readonly property real frameThickness: isFramed ? Settings.data.bar.frameThickness : 0
      width: dockContent.dockContainer.width
      height: dockContent.dockContainer.height
      anchors.top: root.dockPosition === "bottom" ? parent.top : undefined
      anchors.bottom: root.dockPosition === "top" ? parent.bottom : undefined
      anchors.left: root.dockPosition === "right" ? parent.left : undefined
      anchors.right: root.dockPosition === "left" ? parent.right : undefined

      DockContent {
        id: dockContent
        anchors.fill: parent
        dockRoot: root
        extraTop: 0
        extraBottom: 0
        extraLeft: 0
        extraRight: 0
      }
    }
  }
}
