import ClientDocumentMixin from "./client-document.mjs";
import PlaceableConfig from "@client/applications/sheets/placeable-config.mjs";

/**
 * @import Document from "@common/abstract/document.mjs";
 * @import PlaceableObject from "@client/canvas/placeables/placeable-object.mjs";
 * @import PlaceablesLayer from "@client/canvas/layers/base/placeables-layer.mjs";
 * @import Level from "../level.mjs";
 */

/**
 * A specialized subclass of the ClientDocumentMixin which is used for document types that are intended to be
 * represented upon the game Canvas.
 * @category Mixins
 * @param {typeof Document} Base        The base document class mixed with client and canvas features
 */
export default function CanvasDocumentMixin(Base) {
  /**
   * A ClientDocument class with additional facilities for utilizing the {@link foundry.canvas.Canvas} API
   */
  class CanvasDocument extends ClientDocumentMixin(Base) {

    /* -------------------------------------------- */
    /*  Properties                                  */
    /* -------------------------------------------- */

    /**
     * A lazily constructed PlaceableObject instance which can represent this Document on the game canvas.
     * @type {PlaceableObject|null}
     */
    get object() {
      if ( this._object || this._destroyed ) return this._object;
      if ( !this.viewed || !this.layer ) return null;
      return this._object = this.layer.createObject(this);
    }

    /**
     * @type {PlaceableObject|null}
     * @internal
     */
    _object = this._object ?? null;

    /**
     * Has this object been deliberately destroyed as part of the deletion workflow?
     * @type {boolean}
     * @internal
     */
    _destroyed = false;

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

    /**
     * A reference to the CanvasLayer which contains Document objects of this type.
     * @type {PlaceablesLayer}
     */
    get layer() {
      return canvas.getLayerByEmbeddedName(this.documentName);
    }

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

    /**
     * An indicator for whether this document is currently rendered on the game canvas.
     * @type {boolean}
     */
    get rendered() {
      return this._object?.destroyed === false;
    }

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

    /** @override */
    get visible() {
      if ( game.user.isGM ) return true;
      if ( this.schema.has("author") && this.author?.isSelf ) return true;
      if ( this.hidden ) return false;
      return this.testUserPermission(game.user, "OBSERVER");
    }

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

    /**
     * Is this CanvasDocument viewed, i.e. included in the viewed Level?
     * @type {boolean}
     */
    get viewed() {
      return !!this.parent?._view && this.includedInLevel(this.parent._view);
    }

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

    /**
     * Is this CanvasDocument included in a given Level?
     * @param {string|Level} level    The Level or ID
     * @returns {boolean}
     */
    includedInLevel(level) {
      if ( !this.schema.has("levels") || !this.levels.size ) return true;
      let id;
      if ( typeof level === "string" ) id = level;
      else if ( this.parent && (level.parent === this.parent) ) id = level.id;
      else return false;
      return this.levels.has(id);
    }

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

    /**
     * Is this CanvasDocument located in a given Level?
     * @param {string|Level} level    The Level or ID
     * @returns {boolean}
     */
    locatedInLevel(level) {
      return this.includedInLevel(level);
    }

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

    /** @inheritDoc */
    async _preCreate(data, options, user) {
      const allowed = await super._preCreate(data, options, user);
      if ( allowed === false ) return false;
      if ( !this.schema.has("sort") || ("sort" in data) ) return;
      let sort = 0;
      for ( const document of this.collection ) sort = Math.max(sort, document.sort + 1);
      this.updateSource({sort});
    }

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

    /** @inheritDoc */
    _onCreate(data, options, userId) {

      // Create placeable object before the sheet is rendered in super._onCreate so that the preview of
      // the config is created because this.rendered is already true
      const object = this.object;
      super._onCreate(data, options, userId);
      if ( !object ) return;
      this.layer.objects?.addChild(object);
      object.draw().then(() => {
        object._onCreate(data, options, userId);
      });
    }

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

    /** @inheritDoc */
    static async _onCreateOperation(documents, operation, user) {

      // Control the object of each created document
      if ( operation.controlObject && (user.id === game.user.id) ) {
        let controlledObject = false;
        for ( const document of documents ) {
          if ( !document.rendered ) continue;
          document.object.control({isNew: true, releaseOthers: !controlledObject, renderSidebar: false});
          controlledObject = true;
        }
        if ( controlledObject ) {
          ui.placeables.render();
          if ( game.activeTool === "select" ) ui.placeablesPalette?.render();
        }
      }

      return super._onCreateOperation(documents, operation, user);
    }

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

    /** @inheritDoc */
    _onUpdate(changed, options, userId) {
      for ( const app of Object.values(this.apps) ) {
        if ( !(app instanceof PlaceableConfig) || !app._preview ) continue;
        app._previewChanges(changed);
        options.animate = false;
      }
      super._onUpdate(changed, options, userId);
      this._object?._onUpdate(changed, options, userId);
      if ( "levels" in changed ) this._refreshViewedState();
    }

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

    /** @inheritDoc */
    static async _onUpdateOperation(documents, operation, user) {

      // Release control if the object becomes hidden/locked and isn't visible/interactable while hidden/locked
      let releasedObject = false;
      let renderPalette = false;
      for ( let i = 0; i < documents.length; i++ ) {
        const document = documents[i];
        const changed = operation.updates[i];
        if ( !document.rendered ) continue;
        const object = document.object;
        if ( !object.controlled ) continue;
        const lockedChanged = "locked" in changed;
        if ( lockedChanged ) renderPalette = true;
        if ( (("hidden" in changed) && document.hidden && !object.isVisible)
          || (("locked" in changed) && document.locked && !object.isInteractable) ) {
          object.release({renderSidebar: false});
          releasedObject = true;
        }
      }
      if ( releasedObject ) {
        ui.placeables.render();
        renderPalette = true;
      }
      if ( renderPalette && (game.activeTool === "select") ) ui.placeablesPalette?.render();

      return super._onUpdateOperation(documents, operation, user);
    }
    /* -------------------------------------------- */

    /**
     * Handle the Document becoming (un)viewed.
     * @returns {Promise<void>}
     * @internal
     */
    async _refreshViewedState() {
      const isViewed = this.viewed;
      const wasViewed = this.rendered;
      if ( isViewed === wasViewed ) return;
      if ( isViewed ) {
        this._destroyed = false;
        const object = this.object;
        this.layer.objects?.addChild(object);
        await object.draw();
      }
      else this._object.destroy({children: true});
    }

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

    /** @inheritDoc */
    _onDelete(options, userId) {
      super._onDelete(options, userId);
      this._object?._onDelete(options, userId);
    }

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

    /** @inheritDoc */
    static async _onDeleteOperation(documents, operation, user) {

      // Release the object of each deleted document
      let releasedObject = false;
      for ( const document of documents ) {
        if ( !document.rendered ) continue;
        if ( document.object.controlled ) {
          document.object.release({renderSidebar: false});
          releasedObject = true;
        }
        document.object.destroy({children: true});
      }
      if ( releasedObject ) {
        ui.placeables.render();
        if ( game.activeTool === "select" ) ui.placeablesPalette?.render();
      }

      // Remove deleted documents from clipboard
      if ( documents.length ) {
        const layer = documents[0].layer;
        layer.clipboard.objects = layer.clipboard.objects.filter(o => !o.document._destroyed);
      }

      return super._onDeleteOperation(documents, operation, user);
    }
  }
  return CanvasDocument;
}
