import PlaceableObject from "./placeable-object.mjs";
import ShapeObjectMixin from "./mixins/shapes.mjs";
import AmbientLightShapeControls from "./lights/shape-controls.mjs";
import PreciseText from "../containers/elements/precise-text.mjs";

/**
 * @import {LightSourceData} from "../sources/base-light-source.mjs";
 */

/**
 * An AmbientLight is an implementation of PlaceableObject which represents a dynamic light source within the Scene.
 * @category Canvas
 * @see {@link foundry.documents.AmbientLightDocument}
 * @see {@link foundry.canvas.layers.LightingLayer}
 */
export default class AmbientLight extends ShapeObjectMixin(PlaceableObject) {
  /**
   * The area that is affected by this light.
   * @type {PIXI.Graphics}
   */
  field;

  /**
   * A reference to the PointSource object which defines this light or darkness area of effect.
   * This is undefined if the AmbientLight does not provide an active source of light.
   * @type {PointDarknessSource|PointLightSource}
   */
  lightSource;

  /**
   * The shape controls.
   * @type {AmbientLightShapeControls}
   */
  controls;

  /**
   * The tooltip text of this AmbientLight, which contains its elevation.
   * @type {PreciseText}
   */
  tooltip;

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

  /** @inheritdoc */
  static embeddedName = "AmbientLight";

  /** @override */
  static RENDER_FLAGS = {
    redraw: {propagate: ["refresh"]},
    refresh: {propagate: ["refreshState", "refreshTransform", "refreshField", "refreshTooltip"], alias: true},
    refreshState: {propagate: ["refreshVisibility"]},
    refreshVisibility: {},
    refreshTransform: {propagate: ["refreshPosition", "refreshRotation", "refreshSize"], alias: true},
    refreshPosition: {propagate: ["refreshMeasurements"]},
    refreshRotation: {propagate: ["refreshMeasurements"]},
    refreshSize: {propagate: ["refreshMeasurements"]},
    refreshField: {},
    refreshTooltip: {},
    refreshMeasurements: {},
    /** @deprecated since v14 */
    refreshElevation: {propagate: ["refreshTooltip"], deprecated: {since: 14, until: 16}, alias: true}
  };

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

  /** @override */
  get sourceId() {
    let id = `${this.document.documentName}.${this.document.id}`;
    if ( this.isPreview ) id += ".preview";
    return id;
  }

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

  /**
   * A convenience accessor to the LightData configuration object
   * @returns {LightData}
   */
  get config() {
    return this.document.config;
  }

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

  /**
   * Test whether a specific AmbientLight source provides global illumination
   * @type {boolean}
   */
  get global() {
    return this.document.isGlobal;
  }

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

  /**
   * The maximum radius in pixels of the light field
   * @type {number}
   */
  get radius() {
    return this.document.shape.radius;
  }

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

  /**
   * Get the pixel radius of dim light emitted by this light source
   * @type {number}
   */
  get dimRadius() {
    return this.config.dim * canvas.dimensions.distancePixels;
  }

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

  /**
   * Get the pixel radius of bright light emitted by this light source
   * @type {number}
   */
  get brightRadius() {
    return this.config.bright * canvas.dimensions.distancePixels;
  }

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

  /**
   * Check if the point source is a LightSource instance
   * @type {boolean}
   */
  get isLightSource() {
    return this.lightSource instanceof CONFIG.Canvas.lightSourceClass;
  }

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

  /**
   * Check if the point source is a DarknessSource instance
   * @type {boolean}
   */
  get isDarknessSource() {
    return this.lightSource instanceof CONFIG.Canvas.darknessSourceClass;
  }

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

  /**
   * Is the source of this Ambient Light disabled?
   * @type {boolean}
   * @protected
   */
  _isLightSourceDisabled() {
    const {hidden, config} = this.document;

    // Hidden lights and lights with zero radius or angle are disabled
    if ( hidden || !this.radius || !config.angle ) return true;

    if ( this.isFilteredOut ) return true;

    // If the darkness level is outside of the darkness activation range, the light is disabled
    const darkness = canvas.darknessLevel;
    return !darkness.between(config.darkness.min, config.darkness.max);
  }

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

  /**
   * Does this Ambient Light actively emit darkness light given
   * its properties and the current darkness level of the Scene?
   * @type {boolean}
   */
  get emitsDarkness() {
    return this.document.config.negative && !this._isLightSourceDisabled();
  }

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

  /**
   * Does this Ambient Light actively emit positive light given
   * its properties and the current darkness level of the Scene?
   * @type {boolean}
   */
  get emitsLight() {
    return !this.document.config.negative && !this._isLightSourceDisabled();
  }

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

  /** @override */
  get isInteractable() {
    if ( this.isPreview ) return false;
    if ( !this.layer.active ) return false;
    return !!ui.controls.tool?.interaction;
  }

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

  /** @inheritDoc */
  _destroy(options) {
    this.initializeLightSource({deleted: true});
    this._original?.initializeLightSource();
    super._destroy(options);
  }

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

  /** @inheritDoc */
  async _draw(options) {
    await super._draw(options);
    this.field = this.addChild(new PIXI.Graphics());
    this.field.eventMode = "none";
    this.addChild(this._measurementLines);
    this.controls = this.addChild(new AmbientLightShapeControls(this.document.shape.clone()));
    await this.controls.draw();
    this.addChild(this._measurementLabels);
    this.tooltip = this.addChild(this.#drawTooltip());
    this.cursor = "pointer";
    this._original?.initializeLightSource({deleted: true});
    this.initializeLightSource();
  }

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

  /**
   * Draw a text tooltip for the AmbientLight which can be used to display Elevation or something else.
   * @returns {PreciseText}     The text object used to render the tooltip
   */
  #drawTooltip() {
    const s = canvas.dimensions.uiScale;
    const tooltip = new PreciseText(this._getTooltipText(), this._getTextStyle());
    tooltip.anchor.set(0.5, 1);
    tooltip.pivot.set(0, CONFIG.Canvas.objectBorderThickness * 3 * s);
    tooltip.scale.set(s, s);
    return tooltip;
  }

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

  /** @override */
  _overlapsSelection(rectangle) {
    const {x, y} = this.controls.shape.origin;
    return rectangle.contains(x, y);
  }

  /* -------------------------------------------- */
  /*  Incremental Refresh                         */
  /* -------------------------------------------- */

  /** @inheritDoc */
  _applyRenderFlags(flags) {
    super._applyRenderFlags(flags);
    if ( flags.refreshPosition ) this._refreshPosition();
    if ( flags.refreshRotation ) this._refreshRotation();
    if ( flags.refreshSize ) this._refreshSize();
    if ( flags.refreshField ) this._refreshField();
    if ( flags.refreshTooltip ) this._refreshTooltip();
    if ( flags.refreshMeasurements ) this._refreshMeasurements();
  }

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

  /**
   * Refresh the position of the AmbientLight.
   * @protected
   */
  _refreshPosition() {
    const {x, y} = this.document.shape;
    this.controls.shape.updateSource({x, y});
    this.controls.refresh();
    this.tooltip.position.set(x, y);
  }

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

  /**
   * Refresh the rotation of the AmbientLight.
   * @protected
   */
  _refreshRotation() {
    if ( this.controls.shape.type !== "cone" ) return;
    this.controls.shape.updateSource({angle: this.document.config.angle, rotation: this.document.rotation + 90});
    this.controls.refresh();
  }

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

  /**
   * Refresh the size of the AmbientLight.
   * @protected
   */
  _refreshSize() {
    this.controls.shape.updateSource({radius: this.document.shape.radius});
    this.controls.refresh();
  }

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

  /**
   * Refresh the shape of the light field-of-effect. This is refreshed when the AmbientLight fov polygon changes.
   * @protected
   */
  _refreshField() {
    this.field.clear();
    if ( !this.lightSource?.shape ) return;
    const s = canvas.dimensions.uiScale;
    this.field.lineStyle(2 * s, 0xEEEEEE, 0.4).drawShape(this.lightSource.shape);
  }

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

  /**
   * Refresh the tooltip.
   * @protected
   */
  _refreshTooltip() {
    this.tooltip.text = this._getTooltipText();
    this.tooltip.style = this._getTextStyle();
  }

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

  /**
   * Return the text which should be displayed in the tooltip.
   * @returns {string}
   * @protected
   */
  _getTooltipText() {
    const elevation = (this.document.elevation- canvas.level.elevation.base).toNearest(0.01);
    if ( !Number.isFinite(elevation) || (elevation === 0) ) return "";
    return `${elevation.signedString()} ${canvas.grid.units}`.trim();
  }

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

  /**
   * Get the text style that should be used for the tooltip.
   * @returns {PIXI.TextStyle}
   * @protected
   */
  _getTextStyle() {
    const style = CONFIG.canvasTextStyle.clone();
    style.fontSize = 24;
    return style;
  }

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

  /** @override */
  _getMeasuredShapes() {
    return this.isPreview ? [this.controls.shape] : [];
  }

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

  /** @inheritDoc */
  _refreshState() {
    super._refreshState();
    const {hidden, locked} = this.document;
    const colors = CONFIG.Canvas.dispositionColors;
    this.controls.tint = this.controlled || this.isPreview ? colors.CONTROLLED
      : (locked ? colors.HOSTILE : colors.INACTIVE);
    this.controls.border.visible = this.controlled || this.hover || this.layer.highlightObjects || this.isPreview;
    const showNonTranslateHandles = this.controlled && (this.layer.controlled.length === 1) && !locked;
    for ( const handle of this.controls.handles.children ) {
      if ( handle.name !== "translate" ) handle.visible = showNonTranslateHandles;
    }
    this.controls.editable = this.layer.active && !!ui.controls.tool?.interaction
      && !!ui.controls.tool?.control && !locked;
    this.controls.dashed = hidden;
  }

  /* -------------------------------------------- */
  /*  Light Source Management                     */
  /* -------------------------------------------- */

  /**
   * Update the LightSource associated with this AmbientLight object.
   * Darkness sources always generate edges. Light sources only do so if their priority is strictly greater than 0.
   * If any aspect changes (deletion, switching between darkness/light, or priority change), the source may be destroyed
   * and recreated as needed, and relevant perception flags are set.
   * @param {object}  [options={}]                Options which modify how the source is updated.
   * @param {boolean} [options.deleted=false]     Indicate that this light source has been deleted.
   */
  initializeLightSource({deleted=false}={}) {
    if ( this.destroyed || (this.hasPreview && !deleted) ) return;
    const perceptionFlags = {
      initializeVision: false,
      initializeLighting: false,
      refreshLighting: true,
      refreshVision: true
    };
    const affectedLevels = new Set();

    // Handle updates based on the prior source having edges
    if ( this.lightSource?.requiresEdges ) {
      perceptionFlags.initializeLighting = true;
      perceptionFlags.initializeVision ||= this.lightSource.level.isView;
      affectedLevels.add(this.lightSource.level);
    }

    // Handle deletion
    if ( deleted ) this.#destroyLightSource();
    else {

      // Create if missing or recreate if type changed
      const sourceType = this.config.negative ? "darkness" : "light";
      if ( this.lightSource && (this.lightSource.constructor.sourceType !== sourceType) ) this.#destroyLightSource();
      this.lightSource ??= this.#createLightSource();

      // Re-initialize source data and add it to the active collection
      this.lightSource.initialize(this._getLightSourceData());
      this.lightSource.add();

      // Handle updates based on the current source having edges
      if ( this.lightSource.requiresEdges ) {
        perceptionFlags.initializeLighting = true;
        perceptionFlags.initializeVision ||= this.lightSource.level.isView;
        affectedLevels.add(this.lightSource.level);
      }
    }

    // Update perception and rendering
    canvas.perception.update(perceptionFlags);
    if ( this.layer.active ) this.renderFlags.set({refreshField: true});
    for ( const level of affectedLevels ) level.updateRegionShapeConstraints(["light", "darkness", "sight"]);
  }

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

  /**
   * Get the light source data.
   * @returns {LightSourceData}
   * @protected
   */
  _getLightSourceData() {
    const {x, y, elevation, rotation, walls, vision, config, levels} = this.document;
    const level = canvas.inferLevelFromElevation(elevation, {levels});
    return foundry.utils.mergeObject(config.toObject(false), {
      x, y, level: level.id, elevation, rotation, walls, vision,
      dim: this.dimRadius,
      bright: this.brightRadius,
      seed: this.document.getFlag("core", "animationSeed"),
      disabled: this._isLightSourceDisabled(),
      preview: this.isPreview
    });
  }

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

  /**
   * Returns a new point source: DarknessSource or LightSource, depending on the config data.
   * @returns {PointLightSource|PointDarknessSource} The created source
   */
  #createLightSource() {
    const sourceClass = this.config.negative ? CONFIG.Canvas.darknessSourceClass : CONFIG.Canvas.lightSourceClass;
    const sourceId = this.sourceId;
    return new sourceClass({sourceId, object: this});
  }

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

  /**
   * Destroy the existing BaseEffectSource instance for this AmbientLight.
   */
  #destroyLightSource() {
    this.lightSource?.destroy();
    this.lightSource = undefined;
  }

  /* -------------------------------------------- */
  /*  Document Event Handlers                     */
  /* -------------------------------------------- */

  /** @inheritDoc */
  _onCreate(data, options, userId) {
    super._onCreate(data, options, userId);
    this.initializeLightSource();
  }

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

  /** @override */
  _onUpdate(changed, options, userId) {
    super._onUpdate(changed, options, userId);
    this.initializeLightSource();
    this.renderFlags.set({
      redraw: this.controls.shape.type !== this.document.shape.type,
      refreshState: ("hidden" in changed) || ("locked" in changed),
      refreshPosition: ("x" in changed) || ("y" in changed),
      refreshRotation: ("rotation" in changed) || (("config" in changed) && ("angle" in changed.config)),
      refreshSize: ("config" in changed) && (("dim" in changed.config) || ("bright" in changed.config)),
      refreshTooltip: "elevation" in changed
    });
  }

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

  /** @inheritDoc */
  _onDelete(options, userId) {
    this.initializeLightSource({deleted: true});
    super._onDelete(options, userId);
  }

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

  /** @override */
  _hasShapeChanged(changed) {
    if ( ["x", "y", "rotation"].some(k => k in changed) ) return true;
    if ( !changed.config ) return false;
    return ["dim", "bright", "angle"].some(k => k in changed.config);
  }

  /* -------------------------------------------- */
  /*  Interactivity                               */
  /* -------------------------------------------- */

  /** @inheritdoc */
  _canHUD(user, event) {
    return this.layer.active && user.isGM; // Allow GMs to single right-click
  }

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

  /** @inheritdoc */
  _canConfigure(user, event) {
    return false; // Double-right does nothing
  }

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

  /** @override */
  _onControl(options) {
    super._onControl(options);
    for ( const object of this.layer.controlled ) {
      if ( object !== this ) object.renderFlags.set({refreshState: true});
    }
  }

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

  /** @override */
  _onRelease(options) {
    super._onRelease(options);
    for ( const object of this.layer.controlled ) object.renderFlags.set({refreshState: true});
  }

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

  /** @override */
  _onClickRight(event) {
    if ( !this.document.locked ) this.document.update({hidden: !this.document.hidden});
    if ( !this._propagateRightClick(event) ) event.stopPropagation();
  }

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

  /** @override */
  _updateDragPreviews(event) {
    const {clones, origin, shape, offset} = event.interactionData;
    const dx = shape.origin.x - (origin.x - offset.x);
    const dy = shape.origin.y - (origin.y - offset.y);
    for ( const clone of clones ) {
      const {x, y} = clone._original.document;
      clone.document.updateSource({x: x + dx, y: y + dy});
      clone.renderFlags.set({refreshPosition: true, refreshField: true});
      clone.initializeLightSource();
    }
  }
}
