import RegionBehaviorType from "./base.mjs";
import {REGION_EVENTS} from "../../../common/constants.mjs";
import * as fields from "../../../common/data/fields.mjs";
import MouseInteractionManager from "@client/canvas/interaction/mouse-handler.mjs";

/**
 * @import {RegionBehaviorViewedEvent, RegionBehaviorUnviewedEvent, RegionRegionBoundaryEvent,
 *   RegionBehaviorActivatedEvent, RegionBehaviorDeactivatedEvent} from "@client/documents/_types.mjs";
 */

/**
 * The data model for a behavior that defines surface(s) that can restrict light, movement, sight, and sound.
 *
 * @property {"bottom"|"top"|"both"} surface  Is the surface at the bottom or top of the elevation range?
 *                                            Or are there surfaces at both the bottom and the top?
 * @property {boolean} light                  Does the surface restrict light?
 * @property {boolean} move                   Does the surface restrict movement?
 * @property {boolean} sight                  Does the surface restrict sight?
 * @property {boolean} sound                  Does the surface restrict sound?
 * @property {boolean} occlusion              Does the surface cause occlusion?
 * @property {boolean} exposure               Does the surface cause exposure?
 * @property {boolean} culling                Does the surface cause culling?
 */
export default class DefineSurfaceRegionBehaviorType extends RegionBehaviorType {

  /** @override */
  static LOCALIZATION_PREFIXES = ["BEHAVIOR.TYPES.defineSurface", "BEHAVIOR.TYPES.base"];

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

  /** @override */
  static defineSchema() {
    return {
      placement: new fields.StringField({required: true, blank: false, initial: "bottom", choices: {
        bottom: "BEHAVIOR.TYPES.defineSurface.PLACEMENTS.bottom.label",
        top: "BEHAVIOR.TYPES.defineSurface.PLACEMENTS.top.label",
        both: "BEHAVIOR.TYPES.defineSurface.PLACEMENTS.both.label"
      }}),
      light: new fields.BooleanField({initial: true}),
      move: new fields.BooleanField({initial: true}),
      sight: new fields.BooleanField({initial: true}),
      sound: new fields.BooleanField({initial: true}),
      occlusion: new fields.BooleanField({initial: true}),
      exposure: new fields.BooleanField(),
      culling: new fields.BooleanField()
    };
  }

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

  /**
   * Called when the behavior is activated.
   * @param {RegionBehaviorActivatedEvent} event
   * @this {DefineSurfaceRegionBehaviorType}
   */
  static async #onBehaviorActivated(event) {
    this.scene._invalidateSurfaces();
  }

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

  /**
   * Called when the behavior is deactivated.
   * @param {RegionBehaviorDeactivatedEvent} event
   * @this {DefineSurfaceRegionBehaviorType}
   */
  static async #onBehaviorDeactivated(event) {
    this.scene._invalidateSurfaces();
  }

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

  /**
   * Called when the behavior is viewed.
   * @param {RegionBehaviorViewedEvent} event
   * @this {DefineSurfaceRegionBehaviorType}
   */
  static async #onBehaviorViewed(event) {
    this.#refreshAll();
  }

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

  /**
   * Called when the behavior is unviewed.
   * @param {RegionBehaviorUnviewedEvent} event
   * @this {DefineSurfaceRegionBehaviorType}
   */
  static async #onBehaviorUnviewed(event) {
    this.#refreshAll();
  }

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

  /**
   * Called when the boundary of an event has changed.
   * @param {RegionRegionBoundryEvent} event
   * @this {DefineSurfaceRegionBehaviorType}
   */
  static async #onRegionBoundary(event) {
    this.scene._invalidateSurfaces();
    if ( this.behavior.viewed ) this.#refreshAll();
  }

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

  /** @override */
  static events = {
    [REGION_EVENTS.BEHAVIOR_ACTIVATED]: this.#onBehaviorActivated,
    [REGION_EVENTS.BEHAVIOR_DEACTIVATED]: this.#onBehaviorDeactivated,
    [REGION_EVENTS.BEHAVIOR_VIEWED]: this.#onBehaviorViewed,
    [REGION_EVENTS.BEHAVIOR_UNVIEWED]: this.#onBehaviorUnviewed,
    [REGION_EVENTS.REGION_BOUNDARY]: this.#onRegionBoundary
  };

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

  /**
   * Refresh perception, planned movement paths, and culling as necessary.
   */
  #refreshAll() {
    if ( !canvas.ready ) return;
    canvas.perception.update({
      initializeLighting: this.light && this.exposure,
      initializeVision: this.sight && this.exposure,
      refreshLighting: this.light,
      refreshVision: this.light || this.sight || this.sound || this.move,
      refreshSounds: this.sound,
      refreshOccludedSurfaces: this.occlusion
    });
    if ( this.occlusion ) MouseInteractionManager.emulateMoveEvent();
    if ( this.move ) canvas.tokens.recalculatePlannedMovementPaths();
    if ( this.culling ) this.#refreshCulling();
  }

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

  /**
   * Refresh culling of tokens.
   */
  #refreshCulling() {
    if ( this.scene.tokenVision && (!game.user.isGM
      || canvas.effects.visionSources.some(s => s.active)) ) return;
    for ( const token of canvas.tokens.placeables ) {
      if ( !token.controlled ) token.renderFlags.set({refreshVisibility: true});
    }
  }

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

  /** @inheritDoc */
  _onUpdate(changed, options, userId) {
    super._onUpdate(changed, options, userId);
    if ( !this.behavior.active || !("system" in changed) ) return;
    this.scene._invalidateSurfaces();
    if ( !this.behavior.viewed ) return;
    const placementChanged = "placement" in changed.system;
    const lightChanged = (placementChanged && this.light) || ("light" in changed.system);
    const moveChanged = (placementChanged && this.move) || ("move" in changed.system);
    const sightChanged = (placementChanged && this.sight) || ("sight" in changed.system);
    const soundChanged = (placementChanged && this.sound) || ("sound" in changed.system);
    const exposureChanged = (placementChanged && this.exposure) || ("exposure" in changed.system);
    const occlusionChanged = (placementChanged && this.occlusion) || ("occlusion" in changed.system);
    const cullingChanged = (placementChanged && this.culling) || ("culling" in changed.system);
    canvas.perception.update({
      initializeLighting: (lightChanged && this.exposure) || (this.light && exposureChanged)
        || (lightChanged && exposureChanged),
      initializeVision: (sightChanged && this.exposure) || (this.sight && exposureChanged)
        || (sightChanged && exposureChanged),
      refreshLighting: lightChanged,
      refreshVision: lightChanged || sightChanged || soundChanged || moveChanged,
      refreshSounds: soundChanged,
      refreshOccludedSurfaces: occlusionChanged
    });
    if ( occlusionChanged ) MouseInteractionManager.emulateMoveEvent();
    if ( moveChanged ) canvas.tokens.recalculatePlannedMovementPaths();
    if ( cullingChanged ) this.#refreshCulling();
  }

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

  /**
   * Restricts darkness? Darkness is restricted if and only if light is restricted.
   * @type {boolean}
   */
  get darkness() {
    return this.light;
  }
}
