import PlaceableObject from "./placeable-object.mjs";
import {loadTexture} from "../loader.mjs";
import PrimaryCanvasGroup from "../groups/primary.mjs";
import {getDocumentClass, getPlaceableObjectClass} from "@client/utils/helpers.mjs";
import TileShapeControls from "./tiles/shape-controls.mjs";
import ShapeObjectMixin from "./mixins/shapes.mjs";

/**
 * A Tile is an implementation of PlaceableObject which represents a static piece of artwork or prop within the Scene.
 * @category Canvas
 * @see {@link foundry.documents.TileDocument}
 * @see {@link foundry.canvas.layers.TilesLayer}
 */
export default class Tile extends ShapeObjectMixin(PlaceableObject) {

  /* -------------------------------------------- */
  /*  Attributes                                  */
  /* -------------------------------------------- */

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

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

  /**
   * The Tile border frame
   * @type {PIXI.Container}
   */
  frame;

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

  /**
   * The primary tile image texture
   * @type {PIXI.Texture|PIXI.Spritesheet|null}
   */
  texture = null;

  /**
   * A Tile background which is displayed if no valid image texture is present
   * @type {PIXI.Sprite|null}
   */
  bg = null;

  /**
   * A reference to the SpriteMesh which displays this Tile in the PrimaryCanvasGroup.
   * @type {PrimarySpriteMesh|null}
   */
  mesh = null;

  /**
   * A flag to capture whether this Tile has an unlinked video texture
   * @type {boolean}
   */
  #unlinkedVideo = false;

  /**
   * Video options passed by the HUD
   * @type {object}
   */
  #hudVideoOptions = {
    playVideo: undefined,
    offset: undefined
  };

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

  /**
   * Get the native aspect ratio of the base texture for the Tile sprite
   * @type {number}
   */
  get aspectRatio() {
    if ( !this.texture ) return 1;
    const tex = this.texture.baseTexture;
    return (tex.width / tex.height);
  }

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

  /**
   * The HTML source element for the primary Tile texture
   * @type {PIXI.ImageSource|null}
   */
  get sourceElement() {
    return this.texture?.baseTexture.resource.source ?? null;
  }

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

  /**
   * Does this Tile depict an animated video texture?
   * @type {boolean}
   */
  get isVideo() {
    const source = this.sourceElement;
    return source?.tagName === "VIDEO";
  }

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

  /**
   * Is this tile occluded?
   * @returns {boolean}
   */
  get occluded() {
    return this.mesh?.occluded ?? false;
  }

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

  /**
   * Is the tile video playing?
   * @type {boolean}
   */
  get playing() {
    return this.isVideo && !this.sourceElement.paused;
  }

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

  /**
   * The effective volume at which this Tile should be playing, including the global ambient volume modifier
   * @type {number}
   */
  get volume() {
    return this.document.video.volume * game.settings.get("core", "globalAmbientVolume");
  }

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

  /** @inheritDoc */
  async _draw(options) {
    await super._draw(options);

    // Load Tile texture
    let texture;
    if ( this._original && (this._original.document.texture.src === this.document.texture.src) ) {
      texture = this._original.texture?.clone();
    } else if ( this.document.texture.src ) {
      texture = await loadTexture(this.document.texture.src, {fallback: "icons/svg/hazard.svg"});
    }

    // Manage video playback and clone texture for unlinked video
    let video = game.video.getVideoSource(texture);
    this.#unlinkedVideo = !!video && !this._original;
    if ( this.#unlinkedVideo ) {
      texture = await game.video.cloneTexture(video);
      video = game.video.getVideoSource(texture);
      if ( (this.document.getFlag("core", "randomizeVideo") !== false) && Number.isFinite(video.duration) ) {
        video.currentTime = Math.random() * video.duration;
      }
    }
    if ( !video ) this.#hudVideoOptions.playVideo = undefined;
    this.#hudVideoOptions.offset = undefined;
    this.texture = texture;

    // Draw the Token mesh
    if ( this.texture ) {
      this.mesh = canvas.primary.addTile(this);
      this.bg = null;
    }

    // Draw a placeholder background
    else {
      canvas.primary.removeTile(this);
      this.texture = this.mesh = null;
      this.bg = this.addChild(new PIXI.Sprite(PIXI.Texture.WHITE));
      this.bg.alpha = 0.5;
      this.bg.eventMode = "none";
    }

    // Hitbox
    this.frame = this.addChild(this.#drawFrame());

    // Shape controls
    this.controls = this.addChild(new TileShapeControls(this.document.shape.clone()));
    await this.controls.draw();

    // Interactivity
    this.cursor = "pointer";
  }

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

  /**
   * Create elements for the Tile hitbox
   * @returns {PIXI.Container}
   */
  #drawFrame() {
    const point = new PIXI.Point();
    const frame = new PIXI.Container();
    frame.eventMode = "auto";
    frame.hitArea = {
      contains: (x, y) => this.document.shape.testPoint(point.set(x, y))
    };
    return frame;
  }

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

  /** @inheritDoc */
  _clear() {
    if ( this.mesh ) {
      this.mesh.texture = PIXI.Texture.EMPTY;
      this.mesh.visible = false;
    }
    if ( this.#unlinkedVideo ) {
      this.texture?.baseTexture?.destroy(); // Base texture destroyed for non preview video
      this.texture = null;
      this.#unlinkedVideo = false;
    }
    super._clear();
  }

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

  /** @inheritdoc */
  _destroy(options) {
    canvas.primary.removeTile(this);
    if ( this.texture ) {
      if ( this.#unlinkedVideo ) this.texture?.baseTexture?.destroy(); // Base texture destroyed for non preview video
      this.texture = null;
      this.#unlinkedVideo = false;
    }
  }

  /* -------------------------------------------- */
  /*  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.refreshMesh ) this._refreshMesh();
    if ( flags.refreshElevation ) this._refreshElevation();
    if ( flags.refreshPerception ) this.#refreshPerception();
    if ( flags.refreshVideo ) this._refreshVideo();
  }

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

  /**
   * Refresh the position.
   * @protected
   */
  _refreshPosition() {
    const {x, y} = this.document.shape;
    this.controls.shape.updateSource({x, y});
    this.controls.refresh();
    if ( this.mesh ) this.mesh.position.set(x, y);
    else this.bg.position.set(x, y);
    if ( this.hasActiveHUD ) this.layer.hud.setPosition();
  }

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

  /**
   * Refresh the rotation.
   * @protected
   */
  _refreshRotation() {
    const {rotation} = this.document.shape;
    this.controls.shape.updateSource({rotation});
    this.controls.refresh();
    if ( this.mesh ) this.mesh.angle = rotation;
    else this.bg.angle = rotation;
    if ( this.hasActiveHUD ) this.layer.hud.setPosition();
  }

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

  /**
   * Refresh the size.
   * @protected
   */
  _refreshSize() {
    const {width, height, anchorX, anchorY} = this.document.shape;
    this.controls.shape.updateSource({width, height, anchorX, anchorY});
    this.controls.refresh();
    if ( this.mesh ) {
      const {texture: {fit, scaleX, scaleY}} = this.document;
      this.mesh.resize(width, height, {fit, scaleX, scaleY});
      this.mesh.anchor.set(anchorX, anchorY);
    } else {
      this.bg.width = width;
      this.bg.height = height;
      this.bg.anchor.set(anchorX, anchorY);
    }
    if ( this.hasActiveHUD ) this.layer.hud.setPosition();
  }

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

  /** @inheritDoc */
  _refreshVisibility() {
    super._refreshVisibility();
    if ( this.mesh ) this.mesh.visible = this.visible;
  }

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

  /** @inheritDoc */
  _refreshState() {
    super._refreshState();
    const {hidden, locked, sort} = this.document;
    if ( this.bg ) this.bg.visible = this.layer.active;
    const colors = CONFIG.Canvas.dispositionColors;
    this.controls.tint = this.controlled || this.isPreview ? colors.CONTROLLED
      : (locked ? colors.HOSTILE : colors.INACTIVE);
    this.controls.visible = this.controlled || this.hover || this.layer.highlightObjects || ["dragging", "wheel", "controls"].includes(this.previewType);
    this.controls.handles.visible = this.controlled && !locked;
    this.controls.editable = this.layer.active && !!ui.controls.tool?.interaction
      && !!ui.controls.tool?.control && !locked;
    this.controls.dashed = hidden;
    if ( !this.mesh ) return;
    this.mesh.sort = sort;
    this.mesh.sortLayer = PrimaryCanvasGroup.SORT_LAYERS.TILES;
    this.mesh.zIndex = this.zIndex;
    const isDragging = ["dragging", "creation"].includes(this.previewType);
    this.mesh.alpha = this.alpha * (isDragging ? 0.8 : 1) * (hidden ? 0.5 : 1);
    this.mesh.hidden = hidden;
    this.mesh.restrictsLight = this.document.restrictions.light;
    this.mesh.restrictsWeather = this.document.restrictions.weather;
  }

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

  /**
   * Refresh the appearance of the tile.
   * @protected
   */
  _refreshMesh() {
    if ( !this.mesh ) return;
    const {alpha, occlusion, texture} = this.document;
    const {tint, alphaThreshold} = texture;
    const M = CONST.OCCLUSION_MODES;
    this.mesh.unoccludedAlpha = alpha;
    this.mesh.occludedAlpha = occlusion.alpha;
    this.mesh.occlusionMode = occlusion.modes.reduce((modes, mode) => modes |= mode, M.NONE);
    this.mesh.hoverFade = (this.mesh.occlusionMode !== M.NONE) && !(this.mesh.occlusionMode & M.SURFACE);
    this.mesh.tint = tint;
    this.mesh.textureAlphaThreshold = alphaThreshold;
  }

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

  /**
   * Refresh the elevation.
   * @protected
   */
  _refreshElevation() {
    if ( !this.mesh ) return;
    this.mesh.elevation = this.document.elevation;
  }

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

  /**
   * Refresh the tiles.
   */
  #refreshPerception() {
    if ( !this.mesh ) return;
    canvas.perception.update({refreshOcclusionStates: true});
  }

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

  /**
   * Refresh changes to the video playback state.
   * @protected
   */
  _refreshVideo() {
    if ( !this.texture || !this.#unlinkedVideo ) return;
    const video = game.video.getVideoSource(this.texture);
    if ( !video ) return;
    const playOptions = {...this.document.video, volume: this.volume};
    playOptions.playing = (this.#hudVideoOptions.playVideo ?? playOptions.autoplay);
    playOptions.offset = this.#hudVideoOptions.offset;
    this.#hudVideoOptions.offset = undefined;
    game.video.play(video, playOptions);

    // Refresh HUD if necessary
    if ( this.hasActiveHUD ) this.layer.hud.render();
  }

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

  /** @inheritDoc */
  _onUpdate(changed, options, userId) {
    super._onUpdate(changed, options, userId);
    const restrictionsChanged = ("restrictions" in changed) && !foundry.utils.isEmpty(changed.restrictions);

    // Refresh the Drawing
    this.renderFlags.set({
      redraw: ("texture" in changed) && ("src" in changed.texture),
      refreshState: ("sort" in changed) || ("hidden" in changed) || ("locked" in changed) || restrictionsChanged,
      refreshPosition: ("x" in changed) || ("y" in changed),
      refreshSize: ("width" in changed) || ("height" in changed) || (("texture" in changed)
        && ["scaleX", "scaleY", "anchorX", "anchorY"].some(k => k in changed.texture)),
      refreshRotation: ("rotation" in changed),
      refreshMesh: ("alpha" in changed) || ("occlusion" in changed) || (("texture" in changed)
        && ["tint", "alphaThreshold"].some(k => k in changed.texture)),
      refreshElevation: "elevation" in changed,
      refreshPerception: ("occlusion" in changed) && ("mode" in changed.occlusion),
      refreshVideo: ("video" in changed) || ("playVideo" in options) || ("offset" in options)
    });

    // Set the video options
    if ( "playVideo" in options ) this.#hudVideoOptions.playVideo = options.playVideo;
    if ( "offset" in options ) this.#hudVideoOptions.offset = options.offset;
  }

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

  /** @override */
  _hasShapeChanged(changed) {
    if ( ["x", "y", "rotation", "width", "height"].some(k => k in changed) ) return true;
    if ( !changed.texture ) return false;
    return ["anchorX", "anchorY"].some(k => k in changed.texture);
  }

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

  /**
   * @deprecated since v14
   * @ignore
   */
  static createPreview(data) {
    foundry.utils.logCompatibilityWarning("Tile.createPreview has been deprecated without replacement.",
      {since: 14, until: 16, once: true});
    data.width = data.height = 1;
    data.elevation ??= canvas.level.elevation.base;
    data.sort = Math.max(canvas.tiles.getMaxSort() + 1, 0);

    // Create a pending TileDocument
    const documentClass = getDocumentClass("Tile");
    const document = new documentClass(data, {parent: canvas.scene});

    // Render the preview Tile object
    const tileClass = getPlaceableObjectClass("Tile");
    const tile = new tileClass(document);
    tile._previewType = "creation";
    return tile;
  }
}
