/**
 * The sidebar directory which organizes and displays world-level Combat documents.
 */
class CombatTracker extends SidebarTab {
  constructor(options) {
    super(options);
    if ( !this.popOut ) game.combats.apps.push(this);

    /**
     * Record a reference to the currently highlighted Token
     * @type {Token|null}
     * @private
     */
    this._highlighted = null;

    /**
     * Record the currently tracked Combat encounter
     * @type {Combat|null}
     */
    this.viewed = null;

    // Initialize the starting encounter
    this.initialize({render: false});
  }

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

  /** @inheritdoc */
  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      id: "combat",
      template: "templates/sidebar/combat-tracker.html",
      title: "COMBAT.SidebarTitle",
      scrollY: [".directory-list"]
    });
  }

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

  /**
   * Return an array of Combat encounters which occur within the current Scene.
   * @type {Combat[]}
   */
  get combats() {
    return game.combats.combats;
  }

  /* -------------------------------------------- */
  /*  Methods                                     */
  /* -------------------------------------------- */

  /** @inheritdoc */
  createPopout() {
    const pop = super.createPopout();
    pop.initialize({combat: this.viewed, render: true});
    return pop;
  }

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

  /**
   * Initialize the combat tracker to display a specific combat encounter.
   * If no encounter is provided, the tracker will be initialized with the first encounter in the viewed scene.
   * @param {object} [options]                   Additional options to configure behavior.
   * @param {Combat|null} [options.combat=null]  The combat encounter to initialize
   * @param {boolean} [options.render=true]      Whether to re-render the sidebar after initialization
   */
  initialize({combat=null, render=true}={}) {

    // Retrieve a default encounter if none was provided
    if ( combat === null ) {
      const combats = this.combats;
      combat = combats.length ? combats.find(c => c.active) || combats[0] : null;
      combat?.updateCombatantActors();
    }

    // Prepare turn order
    if ( combat && !combat.turns ) combat.turns = combat.setupTurns();

    // Set flags
    this.viewed = combat;
    this._highlighted = null;

    // Also initialize the popout
    if ( this._popout ) {
      this._popout.viewed = combat;
      this._popout._highlighted = null;
    }

    // Render the tracker
    if ( render ) this.render();
  }

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

  /**
   * Scroll the combat log container to ensure the current Combatant turn is centered vertically
   */
  scrollToTurn() {
    const combat = this.viewed;
    if ( !combat || (combat.turn === null) ) return;
    let active = this.element.find(".active")[0];
    if ( !active ) return;
    let container = active.parentElement;
    const nViewable = Math.floor(container.offsetHeight / active.offsetHeight);
    container.scrollTop = (combat.turn * active.offsetHeight) - ((nViewable/2) * active.offsetHeight);
  }

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

  /** @inheritdoc */
  async getData(options={}) {
    let context = await super.getData(options);

    // Get the combat encounters possible for the viewed Scene
    const combat = this.viewed;
    const hasCombat = combat !== null;
    const combats = this.combats;
    const currentIdx = combats.findIndex(c => c === combat);
    const previousId = currentIdx > 0 ? combats[currentIdx-1].id : null;
    const nextId = currentIdx < combats.length - 1 ? combats[currentIdx+1].id : null;
    const settings = game.settings.get("core", Combat.CONFIG_SETTING);

    // Prepare rendering data
    context = foundry.utils.mergeObject(context, {
      combats: combats,
      currentIndex: currentIdx + 1,
      combatCount: combats.length,
      hasCombat: hasCombat,
      combat,
      turns: [],
      previousId,
      nextId,
      started: this.started,
      control: false,
      settings,
      linked: combat?.scene !== null,
      labels: {}
    });
    context.labels.scope = game.i18n.localize(`COMBAT.${context.linked ? "Linked" : "Unlinked"}`);
    if ( !hasCombat ) return context;

    // Format information about each combatant in the encounter
    let hasDecimals = false;
    const turns = [];
    for ( let [i, combatant] of combat.turns.entries() ) {
      if ( !combatant.visible ) continue;

      // Prepare turn data
      const resource = combatant.permission >= CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER ? combatant.resource : null;
      const turn = {
        id: combatant.id,
        name: combatant.name,
        img: await this._getCombatantThumbnail(combatant),
        active: i === combat.turn,
        owner: combatant.isOwner,
        defeated: combatant.isDefeated,
        hidden: combatant.hidden,
        initiative: combatant.initiative,
        hasRolled: combatant.initiative !== null,
        hasResource: resource !== null,
        resource: resource,
        canPing: (combatant.sceneId === canvas.scene?.id) && game.user.hasPermission("PING_CANVAS")
      };
      if ( (turn.initiative !== null) && !Number.isInteger(turn.initiative) ) hasDecimals = true;
      turn.css = [
        turn.active ? "active" : "",
        turn.hidden ? "hidden" : "",
        turn.defeated ? "defeated" : ""
      ].join(" ").trim();

      // Actor and Token status effects
      turn.effects = new Set();
      for ( const effect of (combatant.actor?.temporaryEffects || []) ) {
        if ( effect.statuses.has(CONFIG.specialStatusEffects.DEFEATED) ) turn.defeated = true;
        else if ( effect.img ) turn.effects.add(effect.img);
      }
      turns.push(turn);
    }

    // Format initiative numeric precision
    const precision = CONFIG.Combat.initiative.decimals;
    turns.forEach(t => {
      if ( t.initiative !== null ) t.initiative = t.initiative.toFixed(hasDecimals ? precision : 0);
    });

    // Confirm user permission to advance
    const isPlayerTurn = combat.combatant?.players?.includes(game.user);
    const canControl = combat.turn && combat.turn.between(1, combat.turns.length - 2)
      ? combat.canUserModify(game.user, "update", {turn: 0})
      : combat.canUserModify(game.user, "update", {round: 0});

    // Merge update data for rendering
    return foundry.utils.mergeObject(context, {
      round: combat.round,
      turn: combat.turn,
      turns: turns,
      control: isPlayerTurn && canControl
    });
  }

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

  /**
   * Retrieve a source image for a combatant.
   * @param {Combatant} combatant         The combatant queried for image.
   * @returns {Promise<string>}           The source image attributed for this combatant.
   * @protected
   */
  async _getCombatantThumbnail(combatant) {
    if ( combatant._videoSrc && !combatant.img ) {
      if ( combatant._thumb ) return combatant._thumb;
      return combatant._thumb = await game.video.createThumbnail(combatant._videoSrc, {width: 100, height: 100});
    }
    return combatant.img ?? CONST.DEFAULT_TOKEN;
  }

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

  /** @inheritdoc */
  activateListeners(html) {
    super.activateListeners(html);
    const tracker = html.find("#combat-tracker");
    const combatants = tracker.find(".combatant");

    // Create new Combat encounter
    html.find(".combat-create").click(ev => this._onCombatCreate(ev));

    // Display Combat settings
    html.find(".combat-settings").click(ev => {
      ev.preventDefault();
      new CombatTrackerConfig().render(true);
    });

    // Cycle the current Combat encounter
    html.find(".combat-cycle").click(ev => this._onCombatCycle(ev));

    // Combat control
    html.find(".combat-control").click(ev => this._onCombatControl(ev));

    // Combatant control
    html.find(".combatant-control").click(ev => this._onCombatantControl(ev));

    // Hover on Combatant
    combatants.hover(this._onCombatantHoverIn.bind(this), this._onCombatantHoverOut.bind(this));

    // Click on Combatant
    combatants.click(this._onCombatantMouseDown.bind(this));

    // Context on right-click
    if ( game.user.isGM ) this._contextMenu(html);

    // Intersection Observer for Combatant avatars
    const observer = new IntersectionObserver(this._onLazyLoadImage.bind(this), {root: tracker[0]});
    combatants.each((i, li) => observer.observe(li));
  }

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

  /**
   * Handle new Combat creation request
   * @param {Event} event
   * @private
   */
  async _onCombatCreate(event) {
    event.preventDefault();
    let scene = game.scenes.current;
    const cls = getDocumentClass("Combat");
    await cls.create({scene: scene?.id, active: true});
  }

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

  /**
   * Handle a Combat cycle request
   * @param {Event} event
   * @private
   */
  async _onCombatCycle(event) {
    event.preventDefault();
    const btn = event.currentTarget;
    const combat = game.combats.get(btn.dataset.documentId);
    if ( !combat ) return;
    await combat.activate({render: false});
  }

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

  /**
   * Handle click events on Combat control buttons
   * @private
   * @param {Event} event   The originating mousedown event
   */
  async _onCombatControl(event) {
    event.preventDefault();
    const combat = this.viewed;
    const ctrl = event.currentTarget;
    if ( ctrl.getAttribute("disabled") ) return;
    else ctrl.setAttribute("disabled", true);
    try {
      const fn = combat[ctrl.dataset.control];
      if ( fn ) await fn.bind(combat)();
    } finally {
      ctrl.removeAttribute("disabled");
    }
  }

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

  /**
   * Handle a Combatant control toggle
   * @private
   * @param {Event} event   The originating mousedown event
   */
  async _onCombatantControl(event) {
    event.preventDefault();
    event.stopPropagation();
    const btn = event.currentTarget;
    const li = btn.closest(".combatant");
    const combat = this.viewed;
    const c = combat.combatants.get(li.dataset.combatantId);

    // Switch control action
    switch ( btn.dataset.control ) {

      // Toggle combatant visibility
      case "toggleHidden":
        return c.update({hidden: !c.hidden});

      // Toggle combatant defeated flag
      case "toggleDefeated":
        return this._onToggleDefeatedStatus(c);

      // Roll combatant initiative
      case "rollInitiative":
        return combat.rollInitiative([c.id]);

      // Actively ping the Combatant
      case "pingCombatant":
        return this._onPingCombatant(c);

      case "panToCombatant":
        return this._onPanToCombatant(c);
    }
  }

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

  /**
   * Handle toggling the defeated status effect on a combatant Token
   * @param {Combatant} combatant     The combatant data being modified
   * @returns {Promise}                A Promise that resolves after all operations are complete
   * @private
   */
  async _onToggleDefeatedStatus(combatant) {
    const isDefeated = !combatant.isDefeated;
    await combatant.update({defeated: isDefeated});
    const defeatedId = CONFIG.specialStatusEffects.DEFEATED;
    await combatant.actor?.toggleStatusEffect(defeatedId, {overlay: true, active: isDefeated});
  }

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

  /**
   * Handle pinging a combatant Token
   * @param {Combatant} combatant     The combatant data
   * @returns {Promise}
   * @protected
   */
  async _onPingCombatant(combatant) {
    if ( !canvas.ready || (combatant.sceneId !== canvas.scene.id) ) return;
    if ( !combatant.token.object.visible ) return ui.notifications.warn(game.i18n.localize("COMBAT.WarnNonVisibleToken"));
    await canvas.ping(combatant.token.object.center);
  }

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

  /**
   * Handle panning to a combatant Token
   * @param {Combatant} combatant     The combatant data
   * @returns {Promise}
   * @protected
   */
  async _onPanToCombatant(combatant) {
    if ( !canvas.ready || (combatant.sceneId !== canvas.scene.id) ) return;
    if ( !combatant.token.object.visible ) return ui.notifications.warn(game.i18n.localize("COMBAT.WarnNonVisibleToken"));
    const {x, y} = combatant.token.object.center;
    await canvas.animatePan({x, y, scale: Math.max(canvas.stage.scale.x, 0.5)});
  }

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

  /**
   * Handle mouse-down event on a combatant name in the tracker
   * @param {Event} event   The originating mousedown event
   * @returns {Promise}     A Promise that resolves once the pan is complete
   * @private
   */
  async _onCombatantMouseDown(event) {
    event.preventDefault();

    const li = event.currentTarget;
    const combatant = this.viewed.combatants.get(li.dataset.combatantId);
    const token = combatant.token;
    if ( !combatant.actor?.testUserPermission(game.user, "OBSERVER") ) return;
    const now = Date.now();

    // Handle double-left click to open sheet
    const dt = now - this._clickTime;
    this._clickTime = now;
    if ( dt <= 250 ) return combatant.actor?.sheet.render(true);

    // Control and pan to Token object
    if ( token?.object ) {
      token.object?.control({releaseOthers: true});
      return canvas.animatePan(token.object.center);
    }
  }

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

  /**
   * Handle mouse-hover events on a combatant in the tracker
   * @private
   */
  _onCombatantHoverIn(event) {
    event.preventDefault();
    if ( !canvas.ready ) return;
    const li = event.currentTarget;
    const combatant = this.viewed.combatants.get(li.dataset.combatantId);
    const token = combatant.token?.object;
    if ( token?.isVisible ) {
      if ( !token.controlled ) token._onHoverIn(event, {hoverOutOthers: true});
      this._highlighted = token;
    }
  }

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

  /**
   * Handle mouse-unhover events for a combatant in the tracker
   * @private
   */
  _onCombatantHoverOut(event) {
    event.preventDefault();
    if ( this._highlighted ) this._highlighted._onHoverOut(event);
    this._highlighted = null;
  }

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

  /**
   * Highlight a hovered combatant in the tracker.
   * @param {Combatant} combatant The Combatant
   * @param {boolean} hover       Whether they are being hovered in or out.
   */
  hoverCombatant(combatant, hover) {
    const trackers = [this.element[0]];
    if ( this._popout ) trackers.push(this._popout.element[0]);
    for ( const tracker of trackers ) {
      const li = tracker.querySelector(`.combatant[data-combatant-id="${combatant.id}"]`);
      if ( !li ) continue;
      if ( hover ) li.classList.add("hover");
      else li.classList.remove("hover");
    }
  }

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

  /** @inheritdoc */
  _contextMenu(html) {
    ContextMenu.create(this, html, ".directory-item", this._getEntryContextOptions());
  }

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

  /**
   * Get the Combatant entry context options
   * @returns {object[]}   The Combatant entry context options
   * @private
   */
  _getEntryContextOptions() {
    return [
      {
        name: "COMBAT.CombatantUpdate",
        icon: '<i class="fas fa-edit"></i>',
        callback: this._onConfigureCombatant.bind(this)
      },
      {
        name: "COMBAT.CombatantClear",
        icon: '<i class="fas fa-undo"></i>',
        condition: li => {
          const combatant = this.viewed.combatants.get(li.data("combatant-id"));
          return Number.isNumeric(combatant?.initiative);
        },
        callback: li => {
          const combatant = this.viewed.combatants.get(li.data("combatant-id"));
          if ( combatant ) return combatant.update({initiative: null});
        }
      },
      {
        name: "COMBAT.CombatantReroll",
        icon: '<i class="fas fa-dice-d20"></i>',
        callback: li => {
          const combatant = this.viewed.combatants.get(li.data("combatant-id"));
          if ( combatant ) return this.viewed.rollInitiative([combatant.id]);
        }
      },
      {
        name: "COMBAT.CombatantRemove",
        icon: '<i class="fas fa-trash"></i>',
        callback: li => {
          const combatant = this.viewed.combatants.get(li.data("combatant-id"));
          if ( combatant ) return combatant.delete();
        }
      }
    ];
  }

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

  /**
   * Display a dialog which prompts the user to enter a new initiative value for a Combatant
   * @param {jQuery} li
   * @private
   */
  _onConfigureCombatant(li) {
    const combatant = this.viewed.combatants.get(li.data("combatant-id"));
    new CombatantConfig(combatant, {
      top: Math.min(li[0].offsetTop, window.innerHeight - 350),
      left: window.innerWidth - 720,
      width: 400
    }).render(true);
  }
}
