import PlaceableConfig from "./placeable-config.mjs";
import FormDataExtended from "../ux/form-data-extended.mjs";
import RegionBehavior from "@client/documents/region-behavior.mjs";
import DragDrop from "../ux/drag-drop.mjs";
import ShapeConfig from "../apps/shape-config.mjs";
import {DOCUMENT_OWNERSHIP_LEVELS} from "@common/constants.mjs";

/**
 * @import {ApplicationClickAction, FormFooterButton} from "../_types.mjs";
 */

/**
 * The Application responsible for configuring a single Region document within a parent Scene.
 */
export default class RegionConfig extends PlaceableConfig {

  /** @inheritDoc */
  static DEFAULT_OPTIONS = {
    classes: ["region-config"],
    viewPermission: DOCUMENT_OWNERSHIP_LEVELS.OBSERVER,
    window: {
      contentClasses: ["standard-form"],
      icon: "fa-regular fa-game-board"
    },
    position: {width: 500},
    form: {
      closeOnSubmit: true
    },
    actions: {
      shapeCreateFromWalls: RegionConfig.#onShapeCreateFromWalls,
      shapeToggleHole: RegionConfig.#onShapeToggleHole,
      shapeMoveUp: RegionConfig.#onShapeMoveUp,
      shapeMoveDown: RegionConfig.#onShapeMoveDown,
      shapeEdit: RegionConfig.#onShapeEdit,
      shapeRemove: RegionConfig.#onShapeRemove,
      shapeRemoveAll: RegionConfig.#onShapeRemoveAll,
      behaviorCreate: RegionConfig.#onBehaviorAdd,
      behaviorDelete: RegionConfig.#onBehaviorDelete,
      behaviorEdit: RegionConfig.#onBehaviorEdit,
      behaviorToggle: RegionConfig.#onBehaviorToggle
    }
  };

  /** @override */
  static PARTS = {
    tabs: {
      template: "templates/generic/tab-navigation.hbs"
    },
    appearance: {
      template: "templates/scene/parts/region-appearance.hbs"
    },
    shapes: {
      template: "templates/scene/parts/region-shapes.hbs",
      templates: [
        "templates/apps/shape-config/parts/origin.hbs",
        "templates/apps/shape-config/circle.hbs",
        "templates/apps/shape-config/cone.hbs",
        "templates/apps/shape-config/ellipse.hbs",
        "templates/apps/shape-config/line.hbs",
        "templates/apps/shape-config/polygon.hbs",
        "templates/apps/shape-config/rectangle.hbs",
        "templates/apps/shape-config/emanation.hbs",
        "templates/apps/shape-config/ring.hbs",
        "templates/apps/shape-config/token.hbs",
        "templates/apps/shape-config/grid.hbs"
      ],
      scrollable: [".scrollable"]
    },
    placement: {
      template: "templates/scene/parts/region-placement.hbs",
      scrollable: [".scrollable"]
    },
    behaviors: {
      template: "templates/scene/parts/region-behaviors.hbs",
      scrollable: [".scrollable"]
    },
    footer: {
      template: "templates/generic/form-footer.hbs"
    }
  };

  /** @override */
  static TABS = {
    sheet: {
      tabs: [
        {id: "appearance", icon: "fa-solid fa-paint-roller"},
        {id: "shapes", icon: "fa-solid fa-shapes"},
        {id: "placement", icon: "fa-solid fa-location-dot"},
        {id: "behaviors", icon: "fa-solid fa-child-reaching"}
      ],
      initial: "appearance",
      labelPrefix: "REGION.TABS"
    }
  };

  /* -------------------------------------------- */
  /*  Context Preparation                         */
  /* -------------------------------------------- */

  /** @inheritDoc */
  _configureRenderOptions(options) {
    super._configureRenderOptions(options);
    switch ( options.renderContext ) {
      case "createbehaviors": case "deletebehaviors": options.parts = ["behaviors"]; break;
      case "updatebehaviors":
        if ( options.renderData?.some(d => ["name", "disabled"].some(k => k in d)) ) options.parts = ["behaviors"];
        else options.parts = [];
        break;
    }
  }

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

  /** @override */
  async _preparePartContext(partId, context) {
    const document = context.document;
    switch ( partId ) {
      case "tabs":
        context.tabClasses = "top-tabs";
        break;
      case "footer":
        context.buttons = this.#getFooterButtons();
        break;
      case "behaviors":
        context.behaviors = this.document.behaviors.filter(b => b.visible).map(b => ({
          id: b.id,
          name: b.name,
          typeLabel: _loc(CONFIG.RegionBehavior.typeLabels[b.type]),
          typeIcon: CONFIG.RegionBehavior.typeIcons[b.type] || "fa-regular fa-notdef",
          disabled: b.disabled,
          canUpdate: b.canUserModify(game.user, "update"),
          canDelete: b.canUserModify(game.user, "delete")
        })).sort((a, b) => (a.disabled - b.disabled) || a.name.localeCompare(b.name, game.i18n.lang));
        context.canCreateBehavior = RegionBehavior.canUserCreate(game.user);
        break;
      case "appearance":
        context.visibilities = Object.entries(CONST.REGION_VISIBILITY).reduce((arr, [key, value]) => {
          arr.push({value, label: _loc(`REGION.VISIBILITY.${key}.label`)});
          return arr;
        }, []);
        break;
      case "shapes":
        if ( document.isSingleShape ) {
          const shape = document.shapes[0];
          const fields = {...shape.schema.fields};
          delete fields.hole;
          context.shapeContext = {rootId: this.id};
          ShapeConfig._prepareShapeContext(context.shapeContext, shape, fields);
        }
        break;
      case "placement":
        context.attachableTokens = (document.parent?.tokens ?? []).reduce((arr, t) => {
          if ( t.isOwner ) arr.push({ value: t.id, label: t.name });
          return arr;
        }, []).sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang));
        context.restrictionTypes = CONST.EDGE_RESTRICTION_TYPES.reduce((arr, value) => {
          arr.push({value, label: _loc(`REGION.RESTRICTION_TYPES.${value}.label`)});
          return arr;
        }, []).sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang));
        break;
    }
    if ( partId in context.tabs ) context.tab = context.tabs[partId];
    return context;
  }

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

  /** @inheritDoc */
  async _onRender(context, options) {
    new DragDrop.implementation({
      dragSelector: ".draggable",
      permissions: {
        dragstart: this._canDragStart.bind(this),
        drop: this._canDragDrop.bind(this)
      },
      callbacks: {
        dragstart: this._onDragStart.bind(this),
        dragover: this._onDragOver.bind(this),
        drop: this._onDrop.bind(this)
      }
    }).bind(this.element);
    if ( this.document.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER) ) {
      this.element.querySelectorAll(".region-shape").forEach(e => {
        e.addEventListener("mouseover", this.#onShapeHoverIn.bind(this));
        e.addEventListener("mouseout", this.#onShapeHoverOut.bind(this));
      });
    }
    this._updateLevelsSelectElement();
    this._updateRestrictionEnabledElement();
    this.document.object?.renderFlags.set({refreshState: true});
    return super._onRender(context, options);
  }

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

  /** @inheritDoc */
  _onChangeForm(formConfig, event) {
    ShapeConfig._onChangeDimension(event.target, this.document.parent.grid);
    super._onChangeForm(formConfig, event);
    if ( event.target.name === "levels" ) this._updateRestrictionEnabledElement();
    else if ( ["attachment.token", "restriction.enabled"].includes(event.target.name) ) this._updateLevelsSelectElement(event);
  }

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

  /**
   * Update the Levels select element.
   * @param {Event} [event]    An input change event within the form
   * @protected
   */
  _updateLevelsSelectElement(event) {
    const attachedToken = this.document.parent.tokens.get(this.element.querySelector('[name="attachment.token"]')?.value);
    const unattachedToken = (event?.target.name === "attachment.token") && !event.target.value;
    const restrictionEnabled = this.element.querySelector('[name="restriction.enabled"]').checked;
    const oldLevelsInput = this.element.querySelector('[name="levels"]');
    if ( !oldLevelsInput ) return;
    const newLevelsInput = this.document.schema.fields.levels.toInput({
      value: attachedToken ? attachedToken._source.level : (unattachedToken && !restrictionEnabled
        ? this.document._source.levels : oldLevelsInput.value),
      rootId: this.id,
      input: restrictionEnabled || attachedToken
        ? (field, config) => foundry.applications.fields.createSelectInput(config)
        : (field, config) => foundry.applications.fields.createMultiSelectInput(config),
      options: (this.document.parent?.levels ?? []).reduce((arr, l) => {
        if ( l.visible ) arr.push({ value: l.id, label: l.name });
        return arr;
      }, []),
      disabled: !!attachedToken,
      dataset: restrictionEnabled || attachedToken ? {tooltip: ""} : {},
      aria: attachedToken ? {label: _loc("REGION.TOOLTIPS.CannotChangeLevelsIfAttached")}
        : (restrictionEnabled ? {label: _loc("REGION.TOOLTIPS.CannotSelectMultipleLevelsIfRestricted")} : {})
    });
    oldLevelsInput.replaceWith(newLevelsInput);
    if ( !foundry.utils.equals(oldLevelsInput.value, newLevelsInput.value) ) this._updateRestrictionEnabledElement();
  }

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

  /**
   * Update the Is Restricted select element.
   * @param {Event} [event]    An input change event within the form
   * @protected
   */
  _updateRestrictionEnabledElement(event) {
    let levels = this.element.querySelector('[name="levels"]');
    if ( !levels ) return;
    levels = levels.value;
    if ( !Array.isArray(levels) ) levels = [levels];
    const restrictionEnabledInput = this.element.querySelector('[name="restriction.enabled"]');
    const canChangeRestricted = restrictionEnabledInput.checked || (levels.length === 1)
      || ((levels.length === 0) && (this.document.parent.levels.size === 1));
    if ( canChangeRestricted ) {
      restrictionEnabledInput.disabled = false;
      delete restrictionEnabledInput.dataset.tooltip;
      restrictionEnabledInput.removeAttribute("aria-label");
    } else {
      restrictionEnabledInput.disabled = true;
      restrictionEnabledInput.dataset.tooltip = "";
      restrictionEnabledInput.ariaLabel = _loc("REGION.TOOLTIPS.CannotEnableRestriction");
    }
  }

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

  /** @inheritDoc */
  _processFormData(event, form, formData) {
    const data = super._processFormData(event, form, formData);

    // Single shape
    if ( "shapes" in data ) {

      // The fieldPath doesn't work as the name and therefore must be converted
      const shapeData = Object.values(data.shapes.element)[0];
      data.shapes = [ShapeConfig._processShapeData(this.document.shapes[0], shapeData)];
    }

    // FormData doesn't include disabled fields
    if ( form.elements.levels ) data.levels = form.elements.levels.value;
    const restrictionEnabledInput = form.elements["restriction.enabled"];
    if ( restrictionEnabledInput ) foundry.utils.setProperty(data, "restriction.enabled", restrictionEnabledInput.checked);

    return data;
  }

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

  /** @inheritDoc */
  _previewChanges(changes) {
    const priorShapeTypes = this._preview.shapes.map(s => `${s.type}.${s.base?.type ?? ""}`);
    super._previewChanges(changes);
    if ( !this._preview.rendered ) return;
    canvas.regions._highlightShape(null);
    const redrawShapeControls = !foundry.utils.equals(this._preview.shapes.map(
      s => `${s.type}.${s.base?.type ?? ""}`), priorShapeTypes);
    if ( redrawShapeControls ) this._preview.object._redrawShapeControls();
  }

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

  /**
   * Prepare an array of form footer buttons.
   * @returns {Partial<FormFooterButton>[]}
   */
  #getFooterButtons() {
    return [
      {type: "submit", icon: "fa-solid fa-floppy-disk", label: "REGION.ACTIONS.update"}
    ];
  }

  /* -------------------------------------------- */
  /*  Drag and Drop                               */
  /* -------------------------------------------- */

  /**
   * Define whether a user is able to begin a dragstart workflow for a given drag selector.
   * @param {string} selector       The candidate HTML selector for dragging
   * @returns {boolean}             Can the current user drag this selector?
   * @protected
   */
  _canDragStart(selector) {
    return true;
  }

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

  /**
   * Define whether a user is able to conclude a drag-and-drop workflow for a given drop selector.
   * @param {string} selector       The candidate HTML selector for the drop target
   * @returns {boolean}             Can the current user drop on this selector?
   * @protected
   */
  _canDragDrop(selector) {
    return false;
  }

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

  /**
   * An event that occurs when a drag workflow begins.
   * @param {DragEvent} event      The initiating drag start event
   * @returns {Promise<void>}
   * @protected
   */
  async _onDragStart(event) {
    const target = event.currentTarget;
    if ( "link" in event.target.dataset ) return;
    let dragData;

    // Region Behavior
    if ( target.dataset.behaviorId ) {
      const effect = this.document.behaviors.get(target.dataset.behaviorId);
      dragData = effect.toDragData();
    }

    // Set data transfer
    if ( !dragData ) return;
    event.dataTransfer.setData("text/plain", JSON.stringify(dragData));
  }

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

  /**
   * An event that occurs when a drag workflow moves over a drop target.
   * @param {DragEvent} event
   * @protected
   */
  _onDragOver(event) {}

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

  /**
   * An event that occurs when data is dropped into a drop target.
   * @param {DragEvent} event
   * @returns {Promise<void>}
   * @protected
   */
  async _onDrop(event) {}

  /* -------------------------------------------- */
  /*  Event Listeners and Handlers                */
  /* -------------------------------------------- */

  /**
   * Handle mouse-hover events on a shape.
   * @param {MouseEvent} event
   */
  #onShapeHoverIn(event) {
    event.preventDefault();
    if ( !this._preview?.rendered ) return;
    const index = this.#getControlShapeIndex(event);
    canvas.regions._highlightShape(this._preview.shapes[index]);
  }

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

  /**
   * Handle mouse-unhover events for shape.
   * @param {MouseEvent} event
   */
  #onShapeHoverOut(event) {
    event.preventDefault();
    if ( !this._preview ) return;
    canvas.regions._highlightShape(null);
  }

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

  /**
   * Handle button clicks to move the shape up.
   * @param {PointerEvent} event
   * @this {RegionConfig}
   */
  static async #onShapeMoveUp(event) {
    if ( this.document.shapes.length <= 1 ) return;
    const index = this.#getControlShapeIndex(event);
    if ( index === 0 ) return;
    const shapes = [...this.document.shapes];
    [shapes[index - 1], shapes[index]] = [shapes[index], shapes[index - 1]];
    await this.document.update({shapes});
  }

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

  /**
   * Handle button clicks to move the shape down.
   * @param {PointerEvent} event
   * @this {RegionConfig}
   */
  static async #onShapeMoveDown(event) {
    if ( this.document.shapes.length <= 1 ) return;
    const index = this.#getControlShapeIndex(event);
    if ( index === this.document.shapes.length - 1 ) return;
    const shapes = [...this.document.shapes];
    [shapes[index], shapes[index + 1]] = [shapes[index + 1], shapes[index]];
    await this.document.update({shapes});
  }

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

  /**
   * Handle button clicks to create shapes from the controlled walls.
   * @param {PointerEvent} event
   * @this {RegionConfig}
   */
  static async #onShapeCreateFromWalls(event) {
    event.preventDefault(); // Don't open context menu
    event.stopPropagation(); // Don't trigger other events
    if ( !canvas.ready || (event.detail > 1) ) return; // Ignore repeated clicks

    // If no walls are controlled, inform the user they need to control walls
    if ( !canvas.walls.controlled.length ) {
      if ( canvas.walls.active ) {
        ui.notifications.error("SHAPE.NOTIFICATIONS.NoControlledWalls", {localize: true});
      }
      else {
        canvas.walls.activate({tool: "select"});
        ui.notifications.info("SHAPE.NOTIFICATIONS.ControlWalls", {localize: true});
      }
      return;
    }

    // Create the shape
    const polygons = canvas.walls.identifyInteriorArea(canvas.walls.controlled);
    if ( polygons.length === 0 ) {
      ui.notifications.error("SHAPE.NOTIFICATIONS.EmptyEnclosedArea", {localize: true});
      return;
    }
    const shapes = polygons.map(p => new foundry.data.PolygonShapeData({points: p.points}));

    // Merge the new shape with form submission data
    const form = this.element;
    const formData = new FormDataExtended(form);
    const submitData = this._prepareSubmitData(event, form, formData, {
      shapes: [...this.document._source.shapes, ...shapes]
    });

    // Update the region
    await this.document.update(submitData);
  }

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

  /**
   * Handle button clicks to toggle the hold field of a shape.
   * @param {PointerEvent} event
   * @this {RegionConfig}
   */
  static async #onShapeToggleHole(event) {
    const index = this.#getControlShapeIndex(event);
    const shapes = this.document.shapes.map(s => s.toObject());
    shapes[index].hole = !shapes[index].hole;
    await this.document.update({shapes});
  }

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

  /**
   * Handle button clicks to edit a shape.
   * @param {PointerEvent} event
   * @this {RegionConfig}
   */
  static #onShapeEdit(event) {
    const shapeIndex = this.#getControlShapeIndex(event);
    const shape = this.document.shapes[shapeIndex];
    const config = new ShapeConfig({shape});
    const existingConfig = foundry.applications.instances.get(config.id);
    if ( existingConfig?.rendered ) existingConfig.bringToFront();
    else (config ?? existingConfig).render({force: true});
  }

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

  /**
   * Handle button clicks to remove a shape.
   * @param {PointerEvent} event
   * @this {RegionConfig}
   */
  static async #onShapeRemove(event) {
    const index = this.#getControlShapeIndex(event);
    return this.document.removeShapeDialog(index);
  }

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

  /**
   * Handle button clicks to remove all shapes.
   * @param {PointerEvent} event
   * @this {RegionConfig}
   */
  static async #onShapeRemoveAll(event) {
    const confirmed = await foundry.applications.api.DialogV2.confirm({
      window: {title: "SHAPE.ACTIONS.removeAll"},
      content: `<p>${_loc("COMMON.AreYouSure")}</p>`
    });
    if ( confirmed ) await this.document.update({shapes: []});
  }

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

  /**
   * Get the shape index from a control button click.
   * @param {PointerEvent} event    The button-click event
   * @returns {number}              The shape index
   */
  #getControlShapeIndex(event) {
    const button = event.target;
    const li = button.closest(".region-shape");
    return Number(li.dataset.shapeIndex);
  }

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

  /**
   * Create a new region behavior.
   * @this {RegionConfig}
   * @type {ApplicationClickAction}
   */
  static async #onBehaviorAdd() {
    await RegionBehavior.implementation.createDialog({}, {parent: this.document});
  }

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

  /**
   * Handle button clicks to delete a behavior.
   * @param {PointerEvent} event
   * @this {RegionConfig}
   */
  static async #onBehaviorDelete(event) {
    const behavior = this.#getControlBehavior(event);
    await behavior.deleteDialog();
  }

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

  /**
   * Handle button clicks to edit a behavior.
   * @param {PointerEvent} event
   * @this {RegionConfig}
   */
  static async #onBehaviorEdit(event) {
    const target = event.target;
    if ( target.closest(".region-element-name") && (event.detail !== 2) ) return; // Double-click on name
    const behavior = this.#getControlBehavior(event);
    await behavior.sheet.render(true);
  }

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

  /**
   * Handle button clicks to toggle a behavior.
   * @param {PointerEvent} event
   * @this {RegionConfig}
   */
  static async #onBehaviorToggle(event) {
    const behavior = this.#getControlBehavior(event);
    await behavior.update({disabled: !behavior.disabled});
  }

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

  /**
   * Get the RegionBehavior document from a control button click.
   * @param {PointerEvent} event    The button-click event
   * @returns {RegionBehavior}      The region behavior document
   */
  #getControlBehavior(event) {
    const button = event.target;
    const li = button.closest(".region-behavior");
    return this.document.behaviors.get(li.dataset.behaviorId);
  }
}
