import Clutter from "gi://Clutter";
import GLib from "gi://GLib";
import St from "gi://St";
import * as Main from "resource:///org/gnome/shell/ui/main.js";
import * as PanelMenu from "resource:///org/gnome/shell/ui/panelMenu.js";
import { MainPanel } from "../types/index.js";
import { logger } from "../utils/logger.js";
import { AnimationManager } from "./animationManager.js";
import { shouldOfferRevealForItem } from "./panelRevealRules.js";


class PanelManager {
  settings;
  veilIndicator;
  animationManager;
  addedHandlerId = null;
  removedHandlerId = null;
  onItemsChangedCallback;
  stateManager;
  initialSetupComplete = false;
  hoverHideTimerId = null;
  onHoverCompleteCallback;
  onPanelLeaveCallback;
  items = [];
  hoverInteractionZone = "none";
  /** True between button-press and release on a right-box item (avoids spurious leave on click). */
  pointerDownOnPanelItem = false;
  /** Per-tray-applet hover/press signals on `actor.firstChild` (enter, leave, press, release). */
  itemHoverHandlers = /* @__PURE__ */ new Map();
  /** `open-state-changed` on each tray `PanelMenu.Button.menu`, keyed by the button actor */
  menuOpenStateHandlers = /* @__PURE__ */ new Map();
  // Watch for late accessible_name changes
  nameChangeHandlers = /* @__PURE__ */ new Map();
  firstChildHandlers = /* @__PURE__ */ new Map();
  /** Pending `idle_add` from tray menu `open-state-changed` (removed in `destroy()`). */
  menuCloseIdleSourceIds = /* @__PURE__ */ new Set();

  constructor(settings, veilIndicator, stateManager) {
    this.settings = settings;
    this.veilIndicator = veilIndicator;
    this.stateManager = stateManager;
    this.animationManager = new AnimationManager(settings);
    this.setupListeners();
    this.updateAllItemsList();
    this.attachHoverHandlersToExistingItems();
  }

  setupListeners() {
    this.addedHandlerId = MainPanel._rightBox.connect(
      "child-added",
      this._onItemAdded.bind(this)
    );

    this.removedHandlerId = MainPanel._rightBox.connect(
      "child-removed",
      this._onItemRemoved.bind(this)
    );

    logger.debug("Panel listeners setup complete");
  }

  attachHoverHandlersToExistingItems() {
    const items = MainPanel._rightBox.get_children();
    items.forEach((actor) => {
      const child = actor.firstChild;
      if (!child) return;
      if (child === MainPanel.statusArea.quickSettings) return;
      if (child === this.veilIndicator) return;
      this.attachHoverHandlers(actor, child);
    });
  }

  detachHoverHandlersFromChild(child) {
    const ids = this.itemHoverHandlers.get(child);
    if (!ids) return;

    for (const id of ids) {
      child.disconnect(id);
    }

    this.itemHoverHandlers.delete(child);
  }

  attachHoverHandlers(_actor, child) {
    this.detachHoverHandlersFromChild(child);

    const ids = [];

    ids.push(
      child.connect("enter-event", () => {
        if (this.settings.get_string("interaction-mode") !== "hover") {
          return Clutter.EVENT_PROPAGATE;
        }
        this.setHoverInteractionZone("panel");
        return Clutter.EVENT_PROPAGATE;
      })
    );

    ids.push(
      child.connect("leave-event", () => {
        if (this.settings.get_string("interaction-mode") !== "hover") {
          return Clutter.EVENT_PROPAGATE;
        }
        const shellMenu = child instanceof PanelMenu.Button ? child.menu : null;
        if (shellMenu?.isOpen) {
          return Clutter.EVENT_PROPAGATE;
        }
        if (this.pointerDownOnPanelItem) {
          return Clutter.EVENT_PROPAGATE;
        }
        this.setHoverInteractionZone("none");
        this.onPanelLeaveCallback?.();
        return Clutter.EVENT_PROPAGATE;
      })
    );

    ids.push(
      child.connect("button-press-event", () => {
        this.pointerDownOnPanelItem = true;
        return Clutter.EVENT_PROPAGATE;
      })
    );

    ids.push(
      child.connect("button-release-event", () => {
        this.pointerDownOnPanelItem = false;
        return Clutter.EVENT_PROPAGATE;
      })
    );

    this.itemHoverHandlers.set(child, ids);
    this.attachMenuOpenStateHandlerIfNeeded(child);
  }

  /**
   * Resets the press guard used to ignore spurious `leave-event` during a click.
   *
   * Dismissing a tray menu with an outside click often never delivers
   * `button-release-event` to the applet. Without this reset, the guard stays
   * set and later real leaves are ignored, so hover hide never runs.
   */
  clearPointerDownHoverGuard() {
    if (!this.pointerDownOnPanelItem) return;
    this.pointerDownOnPanelItem = false;
  }

  attachMenuOpenStateHandlerIfNeeded(child) {
    if (this.menuOpenStateHandlers.has(child)) return;
    if (!(child instanceof PanelMenu.Button)) return;

    const menu = child.menu;
    if (!menu) return;

    const handlerId = menu.connect("open-state-changed", () => {
      if (menu.isOpen) {
        this.clearPointerDownHoverGuard();
        return void 0;
      }
      this.clearPointerDownHoverGuard();
      this.scheduleSyncHoverAfterMenuCloseIdle();
      return void 0;
    });
    this.menuOpenStateHandlers.set(child, handlerId);
  }

  /** Cursor still over the top panel or a Shell popup (e.g. tray menu under uiGroup). */
  pointerInHoverSafeZone() {
    const [x, y] = global.get_pointer();
    let actor = global.stage.get_actor_at_pos(
      Clutter.PickMode.REACTIVE,
      x,
      y
    );

    while (actor) {
      if (actor === Main.panel) return true;
      if (actor instanceof St.Widget) {
        const cls = actor.get_style_class_name() ?? "";
        if (cls.split(/\s+/).includes("popup-menu")) return true;
      }
      actor = actor.get_parent();
    }
    return false;
  }

  scheduleSyncHoverAfterMenuCloseIdle() {
    const id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
      this.menuCloseIdleSourceIds.delete(id);
      this.syncHoverAfterPanelMenuClose();
      return GLib.SOURCE_REMOVE;
    });

    this.menuCloseIdleSourceIds.add(id);
  }

  syncHoverAfterPanelMenuClose() {
    if (this.settings.get_string("interaction-mode") !== "hover") return;
    if (this.pointerInHoverSafeZone()) return;

    this.setHoverInteractionZone("none");
    this.onPanelLeaveCallback?.();
  }

  _onItemAdded(_container, actor) {
    logger.debug("Panel item added", { actor });

    const child = actor.firstChild;
    const panelRevealed = this.stateManager.isPanelRevealed();
    if (child) {
      const itemName = this.getItemName(child);
      if (itemName) {
        const existing = this.items.find((i) => i.name === itemName);
        if (existing && existing.originalVisible !== false) {
          existing.originalVisible = this.mergeOriginalVisibleSnapshot(
            existing.originalVisible,
            actor.visible,
            panelRevealed
          );
        }
      }
    }

    this.updateAllItemsList();

    if (this.initialSetupComplete) {
      this.applyNewItemVisibility(actor);
    }

    this.onItemsChangedCallback?.(this.getAllItemNames());
  }

  /**
   * `false` = hidden while tray expanded (treat as another extension’s hide).
   * When collapsed, `container.visible === false` is often Veil’s hide — use `undefined`.
   */
  mergeOriginalVisibleSnapshot(prior, containerVisible, panelRevealed) {
    if (prior === false) {
      return false;
    }
    if (prior === true) {
      return true;
    }
    if (!containerVisible && !panelRevealed) {
      return void 0;
    }
    if (!containerVisible && panelRevealed) {
      return false;
    }
    return true;
  }

  applyNewItemVisibility(actor) {
    const child = actor.firstChild;

    if (!child) {
      const handlerId = actor.connect("notify::first-child", () => {
        actor.disconnect(handlerId);
        this.firstChildHandlers.delete(actor);
        this.applyNewItemVisibility(actor);
      });
      this.firstChildHandlers.set(actor, handlerId);
      return;
    }

    if (child === MainPanel.statusArea.quickSettings || child === this.veilIndicator) {
      return;
    }

    this.attachHoverHandlers(actor, child);

    const itemName = this.getItemName(child);
    if (itemName) {
      this.handleNewItemVisibility(itemName, actor);
    }

    this.watchForNameChange(child, actor);
  }

  watchForNameChange(child, actor) {
    if (this.nameChangeHandlers.has(child)) {
      return;
    }

    const handlerId = child.connect("notify::accessible-name", () => {
      const itemName = this.getItemName(child);
      if (itemName) {
        this.handleNewItemVisibility(itemName, actor);
      }
      child.disconnect(handlerId);
      this.nameChangeHandlers.delete(child);
    });
    this.nameChangeHandlers.set(child, handlerId);
  }

  _onItemRemoved(_container, actor) {
    logger.debug("Panel item removed", { actor });

    const firstChildHandler = this.firstChildHandlers.get(actor);
    if (firstChildHandler !== void 0) {
      actor.disconnect(firstChildHandler);
      this.firstChildHandlers.delete(actor);
    }

    const child = actor.firstChild;
    if (child) {
      this.detachHoverHandlersFromChild(child);

      const nameHandler = this.nameChangeHandlers.get(child);
      if (nameHandler !== void 0) {
        child.disconnect(nameHandler);
        this.nameChangeHandlers.delete(child);
      }

      const menuHandlerId = this.menuOpenStateHandlers.get(child);
      if (menuHandlerId !== void 0) {
        const button = child;
        if (button.menu) button.menu.disconnect(menuHandlerId);
        this.menuOpenStateHandlers.delete(child);
      }
    }

    this.updateAllItemsList();
    this.onItemsChangedCallback?.(this.getAllItemNames());
  }

  updateAllItemsList() {
    this.items = this.getAllPanelItems();
    const itemNames = this.getAllItemNames();
    this.settings.set_strv("all-items", itemNames);
    logger.debug("Updated all-items list", { count: itemNames.length });
  }

  getAllItemNames() {
    const rightBoxItems = MainPanel._rightBox.get_children();
    const itemNames = [];

    rightBoxItems.forEach(
      (item) => {
        const child = item.firstChild;
        if (!child) return;

        if (child === MainPanel.statusArea.quickSettings || child === this.veilIndicator) {
          return;
        }

        const name = this.getItemName(child);

        if (name) {
          itemNames.push(name);
        }
      }
    );

    return itemNames;
  }

  getAllPanelItems() {
    const rightBoxItems = MainPanel._rightBox.get_children();
    const items = [];

    rightBoxItems.forEach(
      (item) => {
        const child = item.firstChild;
        if (!child) return;

        if (child === MainPanel.statusArea.quickSettings || child === this.veilIndicator) {
          return;
        }

        const name = this.getItemName(child);

        if (name) {
          const existing = this.items.find((i) => i.name === name);
          const panelRevealed = this.stateManager.isPanelRevealed();
          const originalVis = this.mergeOriginalVisibleSnapshot(
            existing?.originalVisible,
            item.visible,
            panelRevealed
          );

          items.push({
            name,
            actor: child,
            container: item,
            originalVisible: originalVis
          });
        }
      }
    );

    return items;
  }

  getItemName(item) {
    if (item.accessible_name && item.accessible_name !== "") {
      return item.accessible_name;
    }

    if (item.constructor && "name" in item.constructor) {
      return item.constructor.name;
    }

    return null;
  }

  isItemVisible(item) {
    const visibleItems = this.settings.get_strv("visible-items");
    return visibleItems.includes(item.name);
  }

  setPermanentVisibility(revealed) {
    this.initialSetupComplete = true;

    const panelItems = this.getAllPanelItems();
    const visibleItems = this.settings.get_strv("visible-items");
    const animationEnabled = this.settings.get_boolean("animation-enabled");
    const animateAllItems = this.settings.get_boolean("animate-all-items");

    const itemsToAnimate = animateAllItems ? panelItems : panelItems.filter((item) => !visibleItems.includes(item.name));

    const itemsToShowInstantly = animateAllItems ? panelItems.filter((item) => visibleItems.includes(item.name)) : [];

    if (revealed) {
      if (animationEnabled) {
        this.fadeInItems(itemsToAnimate);
      } else {
        this.showItemsInstantly([...itemsToAnimate, ...itemsToShowInstantly]);
      }
    } else {
      panelItems.forEach((item) => {
        this.watchForNameChange(item.actor, item.container);
      });

      if (animationEnabled) {
        this.fadeOutItemsAndRestore(itemsToAnimate, itemsToShowInstantly);
      } else {
        this.hideItemsInstantly(itemsToAnimate);
        this.showItemsInstantly(itemsToShowInstantly);
      }
    }

    logger.debug("Set permanent panel visibility", {
      revealed,
      totalItems: panelItems.length,
      visibleItemsCount: visibleItems.length,
      animated: animationEnabled
    });
  }

  showItemsInstantly(items) {
    const visibleItems = this.settings.get_strv("visible-items");

    items.forEach((item) => {
      if (!shouldOfferRevealForItem(item, visibleItems)) {
        return;
      }
      item.container.visible = true;
      item.container.opacity = 255;
    });
  }

  hideItemsInstantly(items) {
    items.forEach((item) => {
      item.container.visible = false;
    });
  }

  fadeInItems(items) {
    const visibleItems = this.settings.get_strv("visible-items");

    items.forEach((item) => {
      if (!shouldOfferRevealForItem(item, visibleItems)) {
        return;
      }
      this.animationManager.fadeIn(item.container);
    });
  }

  fadeOutItemsAndRestore(itemsToHide, itemsToRestore) {
    const fadeOutPromises = itemsToHide.map(
      (item) => this.animationManager.fadeOut(item.container)
    );

    Promise.all(fadeOutPromises).then(() => {
      this.fadeInItems(itemsToRestore);
    });
  }

  restoreVisibility() {
    const panelItems = this.getAllPanelItems();
    const visibleItems = this.settings.get_strv("visible-items");

    panelItems.forEach((item) => {
      const shouldBeVisible = visibleItems.includes(item.name);
      item.container.visible = shouldBeVisible;
    });

    logger.debug("Restored panel visibility", {
      totalItems: panelItems.length,
      visibleItemsCount: visibleItems.length
    });
  }

  showAllItems() {
    const panelItems = this.getAllPanelItems();
    panelItems.forEach((item) => {
      item.container.visible = true;
    });
    logger.debug("Showed all panel items", { count: panelItems.length });
  }

  setOnItemsChanged(callback) {
    this.onItemsChangedCallback = callback;
  }

  setOnHoverComplete(callback) {
    this.onHoverCompleteCallback = callback;
  }

  setOnPanelLeave(callback) {
    this.onPanelLeaveCallback = callback;
  }

  /**
   * Call when switching to hover interaction mode. Tray handlers update the hover
   * zone even in click mode, so stale `panel` can block `scheduleTemporaryHide`;
   * pending hover timers from a previous session should not run after the switch.
   */
  resetHoverStateForModeEntry() {
    this.cancelHoverHideTimer();
    this.setHoverInteractionZone("none");
  }

  setHoverInteractionZone(zone) {
    this.hoverInteractionZone = zone;
    logger.debug("Hover interaction zone changed", {
      zone: this.hoverInteractionZone
    });
  }

  setTemporaryVisibility() {
    this.cancelHoverHideTimer();

    const panelItems = this.getAllPanelItems();
    const animationEnabled = this.settings.get_boolean("animation-enabled");

    if (animationEnabled) {
      const itemsNeedingReveal = panelItems.filter(
        (item) => !item.container.visible
      );
      this.fadeInItems(itemsNeedingReveal);

      const itemsToFix = panelItems.filter(
        (item) => item.originalVisible !== false && item.container.visible && item.container.opacity < 255
      );
      itemsToFix.forEach((item) => {
        item.container.opacity = 255;
      });
    } else {
      this.showItemsInstantly(panelItems);
      panelItems.forEach((item) => {
        if (item.container.visible) {
          item.container.set_translation(0, 0, 0);
        }
      });
    }
  }

  scheduleTemporaryHide() {
    if (this.settings.get_string("interaction-mode") !== "hover") {
      return;
    }
    if (this.hoverInteractionZone !== "none") {
      return;
    }

    const hideOnLeave = this.settings.get_boolean("hover-hide-on-leave");

    if (hideOnLeave) {
      this.restoreVisibilityToSavedState();
      this.onHoverCompleteCallback?.();
    } else {
      this.cancelHoverHideTimer();

      const hoverDuration = this.settings.get_int("hover-duration");

      this.hoverHideTimerId = GLib.timeout_add_seconds(
        GLib.PRIORITY_DEFAULT,
        hoverDuration,
        () => {
          this.hoverHideTimerId = null;
          if (this.hoverInteractionZone !== "none") {
            return GLib.SOURCE_REMOVE;
          }
          this.restoreVisibilityToSavedState();
          this.onHoverCompleteCallback?.();
          return GLib.SOURCE_REMOVE;
        }
      );
    }
  }

  cancelHoverHideTimer() {
    if (this.hoverHideTimerId !== null) {
      GLib.Source.remove(this.hoverHideTimerId);
      this.hoverHideTimerId = null;
    }
  }

  restoreVisibilityToSavedState() {
    const revealed = this.stateManager.isPanelRevealed();
    this.setPermanentVisibility(revealed);
    logger.debug("Restored visibility to saved state", {
      revealed
    });
  }

  handleNewItemVisibility(itemName, container) {
    const panelRevealed = this.stateManager.isPanelRevealed();
    const visibleItems = this.settings.get_strv("visible-items");

    if (panelRevealed) {
      container.visible = true;
      container.opacity = 255;
      logger.debug("New item shown (panel revealed)", { itemName });
    } else {
      const shouldBeVisible = visibleItems.includes(itemName);
      container.visible = shouldBeVisible;
      container.opacity = 255;
      logger.debug("New item visibility set based on visible-items", {
        itemName,
        visible: shouldBeVisible
      });
    }
  }

  destroy() {
    this.cancelHoverHideTimer();
    for (const id of this.menuCloseIdleSourceIds) {
      GLib.Source.remove(id);
    }
    this.menuCloseIdleSourceIds.clear();
    this.animationManager.destroy();

    for (const [child, ids] of this.itemHoverHandlers) {
      for (const id of ids) {
        child.disconnect(id);
      }
    }

    this.itemHoverHandlers.clear();

    for (const [child, handlerId] of this.menuOpenStateHandlers) {
      const button = child;
      if (button.menu) button.menu.disconnect(handlerId);
    }
    this.menuOpenStateHandlers.clear();

    if (this.addedHandlerId !== null) {
      MainPanel._rightBox.disconnect(this.addedHandlerId);
      this.addedHandlerId = null;
    }

    if (this.removedHandlerId !== null) {
      MainPanel._rightBox.disconnect(this.removedHandlerId);
      this.removedHandlerId = null;
    }

    logger.debug("PanelManager destroyed");
  }
}

export {
  PanelManager
};
