import ApplicationV2 from "../api/application.mjs";
import HandlebarsApplicationMixin from "../api/handlebars-application.mjs";
import Hooks from "../../helpers/hooks.mjs";

/**
 * The Scene Navigation UI element.
 * @extends ApplicationV2
 * @mixes HandlebarsApplication
 */
export default class SceneNavigation extends HandlebarsApplicationMixin(ApplicationV2) {

  /** @inheritDoc */
  static DEFAULT_OPTIONS = {
    id: "scene-navigation",
    classes: ["faded-ui", "flexcol"],
    tag: "nav",
    window: {
      frame: false,
      positioned: false
    },
    actions: {
      cycleLevel: SceneNavigation.#onCycleLevel,
      viewLevel: SceneNavigation.#onViewLevel,
      viewScene: SceneNavigation.#onViewScene,
      toggleExpand: SceneNavigation.#onToggleExpand
    }
  };

  /** @override */
  static PARTS = {
    scenes: {
      root: true,
      template: "templates/ui/scene-navigation.hbs",
      scrollable: [".scene-levels"]
    }
  };

  /**
   * A reference to the Scene currently being dragged.
   * @type {Scene}
   */
  #dragScene;

  /**
   * An HTMLElement which is temporarily the drop target for a drag-and-drop operation.
   * @type {HTMLElement}
   */
  #dropTarget;

  /* -------------------------------------------- */
  /*  Properties                                  */
  /* -------------------------------------------- */

  /**
   * Whether the scene navigation is currently expanded.
   * @type {boolean}
   */
  get expanded() {
    return this.element.classList.contains("expanded");
  }

  /* -------------------------------------------- */
  /*  Rendering                                   */
  /* -------------------------------------------- */

  /** @override */
  async _prepareContext(_options) {
    const scenes = this.#prepareScenes();
    return {
      scenes,
      canExpand: scenes.inactive.length
    };
  }

  /* -------------------------------------------- */

  /**
   * Prepare Scene data for rendering.
   * @returns {{inactive: object[], active: object[]}}
   */
  #prepareScenes() {
    const userScenes = this.#prepareUserPips(u => u.viewedScene);
    const scenes = {active: [], inactive: []};
    for ( const scene of game.scenes ) {
      const {active, isView} = scene;
      const visible = active || isView || (scene.navigation && scene.visible);
      if ( !visible ) continue;
      const icons = [];
      if ( isView ) icons.push({class: "eye", tooltip: _loc("SCENE_NAVIGATION.SCENE.Viewing")});
      if ( active ) icons.push({class: "bullseye", tooltip: _loc("SCENE_NAVIGATION.SCENE.Active")});
      if ( game.user.isGM && (scene.ownership.default === 0) && !active ) {
        icons.push({class: "eye-slash", tooltip: _loc("SCENE_NAVIGATION.SCENE.Hidden")});
      }
      const context = {
        id: scene.id,
        active, isView, icons,
        navOrder: scene.navOrder,
        name: scene.navName || scene.name,
        tooltip: (scene.navName && game.user.isGM) ? scene.name : "",
        users: userScenes[scene.id]
      };
      if ( isView ) {
        scenes.viewed = context;
        scenes.levels = this.#prepareLevels(scene);
      }
      else if ( active || context.users?.length ) scenes.active.push(context);
      else scenes.inactive.push(context);
    }
    scenes.active.sort((a, b) => (b.active - a.active) || (a.navOrder - b.navOrder));
    scenes.inactive.sort((a, b) => a.navOrder - b.navOrder);
    return scenes;
  }

  /* -------------------------------------------- */

  /**
   * Prepare render context for a given scene's levels.
   * @param {Scene} scene  The scene.
   * @returns {object[]|null}
   */
  #prepareLevels(scene) {
    const availableLevels = Array.from(scene.availableLevels);
    if ( availableLevels.length < 2 ) return null;
    const userLevels = this.#prepareUserPips(u => u.viewedScene === scene.id ? u.viewedLevel : null);
    return availableLevels.toReversed().map(level => ({
      id: level.id, sceneId: level.parent.id, name: level.name, css: level.isView ? "view" : "",
      users: userLevels[level.id]
    }));
  }

  /* -------------------------------------------- */

  /**
   * Prepare a map of user pip data grouped by a given key.
   * @param {(User) => string|null} keyFn  Returns the grouping key for each active user, or null to exclude.
   * @returns {Record<string, object[]>}
   */
  #prepareUserPips(keyFn) {
    return game.users.reduce((obj, u) => {
      if ( !u.active ) return obj;
      const key = keyFn(u);
      if ( !key ) return obj;
      obj[key] ||= [];
      obj[key].push({ name: u.name, letter: u.name[0], color: u.color.multiply(0.5).css, border: u.color });
      return obj;
    }, {});
  }

  /* -------------------------------------------- */

  /** @override */
  async _onFirstRender(_context, _options) {
    game.scenes.apps.push(this);
    if ( !game.user.isGM ) return;
    /** @fires {hookEvents:getSceneContextOptions} */
    this._createContextMenu(this._getContextMenuOptions, ".scene", {
      fixed: true,
      hookName: "getSceneContextOptions",
      parentClassHooks: false
    });
  }

  /* -------------------------------------------- */

  /** @override */
  async _onRender(_context, options) {
    if ( options.scrollToActiveLevel ) {
      this.element.querySelector(".scene-level.active")?.scrollIntoView({ block: "nearest" });
    }
    this.#setCollapseTooltip(`SCENE_NAVIGATION.${this.expanded ? "COLLAPSE" : "EXPAND"}`);
    if ( !game.user.isGM ) return;

    // Drag and Drop
    new foundry.applications.ux.DragDrop.implementation({
      dragSelector: ".scene",
      dropSelector: "#scene-navigation-inactive",
      callbacks: {
        dragstart: this.#onDragStart.bind(this),
        dragover: this.#onDragOver.bind(this),
        drop: this.#onDragDrop.bind(this)
      }
    }).bind(this.element);
  }

  /* -------------------------------------------- */

  /**
   * Toggle the tooltip and aria-label properties of the collapse/expand button.
   * @param {string} tooltip
   */
  #setCollapseTooltip(tooltip) {
    const button = this.element.querySelector("#scene-navigation-expand");
    if ( !button ) return;
    button.dataset.tooltip = tooltip;
    button.setAttribute("aria-label", _loc(tooltip));
  }

  /* -------------------------------------------- */

  /**
   * Get the set of ContextMenu options which should be applied for Scenes in the menu.
   * @returns {ContextMenuEntry[]}   The Array of context options passed to the ContextMenu instance
   * @protected
   */
  _getContextMenuOptions() {
    return [
      {
        label: "SCENE.View",
        icon: "fa-solid fa-eye",
        visible: li => {
          if ( !li.dataset.sceneId ) return false;
          if ( li.dataset.sceneId !== canvas.scene?.id ) return true;
          if ( !li.dataset.levelId ) return false;
          return li.dataset.levelId !== canvas.level?.id;
        },
        onClick: (event, li) => {
          const scene = game.scenes.get(li.dataset.sceneId);
          scene?.view({level: li.dataset.levelId});
        }
      },
      {
        label: "SCENE.Activate",
        icon: "fa-solid fa-bullseye",
        visible: li => {
          if ( !li.dataset.sceneId || li.dataset.levelId || !game.user.isGM ) return false;
          const scene = game.scenes.get(li.dataset.sceneId);
          return scene && !scene.active;
        },
        onClick: (event, li) => {
          const scene = game.scenes.get(li.dataset.sceneId);
          scene.activate();
        }
      },
      {
        label: "SCENE.PullEveryone",
        icon: "fa-solid fa-diamond-turn-right",
        visible: li => {
          if ( !li.dataset.sceneId || !game.user.isGM ) return false;
          for ( const user of game.users ) {
            if ( !user.active ) continue;
            if ( user.viewedScene !== li.dataset.sceneId ) return true;
            if ( !li.dataset.levelId ) continue;
            if ( user.viewedLevel !== li.dataset.levelId ) return true;
          }
          return false;
        },
        onClick: (event, li) => {
          const scene = game.scenes.get(li.dataset.sceneId);
          scene?.pullUsers(game.users, {level: li.dataset.levelId});
        }
      },
      {
        label: "SIDEBAR.Edit",
        icon: "fa-solid fa-edit",
        visible: li => li.dataset.sceneId && game.user.isGM,
        onClick: (event, li) => {
          const scene = game.scenes.get(li.dataset.sceneId);
          if ( !scene ) return;
          if ( li.dataset.levelId ) {
            const level = scene.levels.get(li.dataset.levelId);
            level?.sheet.render(true);
          }
          else scene.sheet.render(true);
        }
      },
      {
        label: "SCENE.Notes",
        icon: "fa-solid fa-scroll",
        visible: li => {
          if ( !li.dataset.sceneId || li.dataset.levelId || !game.user.isGM ) return false;
          const scene = game.scenes.get(li.dataset.sceneId);
          return !!scene?.journal;
        },
        onClick: (event, li) => {
          const scene = game.scenes.get(li.dataset.sceneId);
          const entry = scene.journal;
          if ( entry ) {
            const sheet = entry.sheet;
            const options = {};
            if ( scene.journalEntryPage ) options.pageId = scene.journalEntryPage;
            sheet.render(true, options);
          }
        }
      },
      {
        label: "SCENE.Preload",
        icon: "fa-solid fa-download",
        visible: li => li.dataset.sceneId && game.user.isGM,
        onClick: (event, li) => {
          const sceneId = li.dataset.sceneId;
          game.scenes.preload(sceneId, {level: li.dataset.levelId, broadcast: true});
        }
      },
      {
        label: "SCENE.ToggleNav",
        icon: "fa-solid fa-compass",
        visible: li => li.dataset.sceneId && !li.dataset.levelId && game.user.isGM,
        onClick: (event, li) => {
          const scene = game.scenes.get(li.dataset.sceneId);
          scene.update({navigation: !scene.navigation});
        }
      }
    ];
  }

  /* -------------------------------------------- */
  /*  Public API                                 */
  /* -------------------------------------------- */

  /**
   * Expand Scene Navigation, displaying inactive Scenes.
   * @fires {hookEvents:collapseSceneNavigation}
   */
  expand() {
    this.toggleExpanded(true);
  }

  /* -------------------------------------------- */

  /**
   * Collapse Scene Navigation, hiding inactive Scenes.
   * @fires {hookEvents:collapseSceneNavigation}
   */
  async collapse() {
    this.toggleExpanded(false);
  }

  /* -------------------------------------------- */

  /**
   * Toggle the expanded state of scene navigation.
   * @param {boolean} [expanded]  Force the expanded state to the provided value, otherwise toggle the state.
   * @fires {hookEvents:collapseSceneNavigation}
   */
  toggleExpanded(expanded) {
    expanded ??= !this.expanded;
    this.element.classList.toggle("expanded");
    this.#setCollapseTooltip(`SCENE_NAVIGATION.${expanded ? "COLLAPSE" : "EXPAND"}`);
    Hooks.callAll("collapseSceneNavigation", this, !expanded);
  }

  /* -------------------------------------------- */
  /*  Action Event Handlers                       */
  /* -------------------------------------------- */

  /**
   * Handle cycling through available levels.
   * @this {SceneNavigation}
   * @param {PointerEvent} event        The triggering event.
   * @param {HTMLButtonElement} target  The action target.
   */
  static #onCycleLevel(event, target) {
    const { direction } = target.closest("[data-direction]")?.dataset ?? {};
    if ( Number.isNumeric(direction) ) return canvas.scene.cycleLevel(Number(direction));
  }

  /* -------------------------------------------- */

  /**
   * Handle switching to a specific level.
   * @this {SceneNavigation}
   * @param {PointerEvent} event  The triggering event.
   * @param {HTMLElement} target  The action target.
   */
  static #onViewLevel(event, target) {
    const { levelId } = target.closest("[data-level-id]")?.dataset ?? {};
    return canvas.scene.view({ level: levelId });
  }

  /* -------------------------------------------- */

  /**
   * Handle a click event to view a certain Scene.
   * @this {SceneNavigation}
   * @param {PointerEvent} event
   * @returns {Promise<void>}
   */
  static async #onViewScene(event) {
    const sceneId = event.target.closest(".scene").dataset.sceneId;
    const scene = game.scenes.get(sceneId);
    if ( !scene ) return;
    await scene.view();
  }

  /* -------------------------------------------- */

  /**
   * Handle a click event to view a certain Scene.
   * @this {SceneNavigation}
   */
  static #onToggleExpand() {
    this.toggleExpanded();
  }

  /* -------------------------------------------- */

  /**
   * Begin dragging a Scene to change its navigation order.
   * @param {DragEvent} event
   */
  #onDragStart(event) {
    const target = event.target.closest(".scene");
    this.#dragScene = game.scenes.get(target.dataset.sceneId);
  }

  /* -------------------------------------------- */

  /**
   * Highlight the Scene which becomes the drop target for drag-and-drop handling.
   * @param {DragEvent} event
   */
  #onDragOver(event) {
    const target = event.target.closest(".scene");
    if ( target === this.#dropTarget ) return;

    // Remove drop target highlight
    if ( this.#dropTarget ) this.#dropTarget.classList.remove("drop-target-before", "drop-target-after");
    this.#dropTarget = target;
    if ( !target || ( target.dataset.sceneId === this.#dragScene.id) ) return;

    // Add drop target highlight
    const scene = game.scenes.get(target.dataset.sceneId);
    const dropClass = this.#dragScene.navOrder < scene.navOrder ? "drop-target-after" : "drop-target-before";
    target.classList.add(dropClass);
  }

  /* -------------------------------------------- */

  /**
   * Conclude dragging a Scene to change its navigation order.
   * @param {DragEvent} event
   */
  async #onDragDrop(event) {
    if ( this.#dropTarget ) {
      this.#dropTarget.classList.remove("drop-target-before", "drop-target-after");
      this.#dropTarget = undefined;
    }

    // Retrieve the drag target Scene
    const scene = this.#dragScene;
    this.#dragScene = undefined;
    if ( !scene ) return;

    // Retrieve the drop target Scene
    const li = event.target.closest(".scene");
    const target = game.scenes.get(li?.dataset.sceneId);
    if ( !target || (target === scene) ) return;

    // Sort Scenes on navOrder relative to siblings
    const siblings = game.scenes.filter(s => s !== scene);
    await scene.sortRelative({sortKey: "navOrder", target, siblings});
  }

  /* -------------------------------------------- */
  /*  Deprecations and Compatibility              */
  /* -------------------------------------------- */

  /**
   * @deprecated since v13
   * @ignore
   */
  static displayProgressBar({label, pct} = {}) {
    foundry.utils.logCompatibilityWarning("SceneNavigation.displayProgressBar is deprecated in favor of "
      + "Notifications#notify using the {progress: true} option", {since: 13, until: 15});
    let bar = SceneNavigation.#loadingBar;
    if ( !bar || !ui.notifications.has(bar) ) {
      bar = SceneNavigation.#loadingBar = ui.notifications.info(label, {progress: true});
    }
    pct = Math.clamp(pct, 0, 100) / 100;
    bar.update({message: label, pct});
  }

  static #loadingBar;
}
