/**
 * @typedef {ApplicationOptions} FormApplicationOptions
 * @property {boolean} [closeOnSubmit=true]     Whether to automatically close the application when it's contained
 *                                              form is submitted.
 * @property {boolean} [submitOnChange=false]   Whether to automatically submit the contained HTML form when an input
 *                                              or select element is changed.
 * @property {boolean} [submitOnClose=false]    Whether to automatically submit the contained HTML form when the
 *                                              application window is manually closed.
 * @property {boolean} [editable=true]          Whether the application form is editable - if true, it's fields will
 *                                              be unlocked and the form can be submitted. If false, all form fields
 *                                              will be disabled and the form cannot be submitted.
 * @property {boolean} [sheetConfig=false]      Support configuration of the sheet type used for this application.
 */

/**
 * An abstract pattern for defining an Application responsible for updating some object using an HTML form
 *
 * A few critical assumptions:
 * 1) This application is used to only edit one object at a time
 * 2) The template used contains one (and only one) HTML form as it's outer-most element
 * 3) This abstract layer has no knowledge of what is being updated, so the implementation must define _updateObject
 *
 * @extends {Application}
 * @abstract
 * @interface
 *
 * @param {object} object                     Some object which is the target data structure to be updated by the form.
 * @param {FormApplicationOptions} [options]  Additional options which modify the rendering of the sheet.
 */
class FormApplication extends Application {
  constructor(object={}, options={}) {
    super(options);

    /**
     * The object target which we are using this form to modify
     * @type {*}
     */
    this.object = object;

    /**
     * A convenience reference to the form HTMLElement
     * @type {HTMLElement}
     */
    this.form = null;

    /**
     * Keep track of any mce editors which may be active as part of this form
     * The values of this object are inner-objects with references to the MCE editor and other metadata
     * @type {Record<string, object>}
     */
    this.editors = {};
  }

  /**
   * An array of custom element tag names that should be listened to for changes.
   * @type {string[]}
   * @protected
   */
  static _customElements = Object.values(foundry.applications.elements).reduce((arr, el) => {
    if ( el.tagName ) arr.push(el.tagName);
    return arr;
  }, []);

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

  /**
   * Assign the default options which are supported by the document edit sheet.
   * In addition to the default options object supported by the parent Application class, the Form Application
   * supports the following additional keys and values:
   *
   * @returns {FormApplicationOptions}    The default options for this FormApplication class
   */
  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ["form"],
      closeOnSubmit: true,
      editable: true,
      sheetConfig: false,
      submitOnChange: false,
      submitOnClose: false
    });
  }

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

  /**
   * Is the Form Application currently editable?
   * @type {boolean}
   */
  get isEditable() {
    return this.options.editable;
  }

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

  /**
   * @inheritdoc
   * @returns {object|Promise<object>}
   */
  getData(options={}) {
    return {
      object: this.object,
      options: this.options,
      title: this.title
    };
  }

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

  /** @inheritdoc */
  async _render(force, options) {

    // Identify the focused element
    let focus = this.element.find(":focus");
    focus = focus.length ? focus[0] : null;

    // Render the application and restore focus
    await super._render(force, options);
    if ( focus && focus.name ) {
      const input = this.form?.[focus.name];
      if ( input && (input.focus instanceof Function) ) input.focus();
    }
  }

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

  /** @inheritdoc */
  async _renderInner(...args) {
    const html = await super._renderInner(...args);
    this.form = html.filter((i, el) => el instanceof HTMLFormElement)[0];
    if ( !this.form ) this.form = html.find("form")[0];
    return html;
  }

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

  /** @inheritdoc */
  _activateCoreListeners(html) {
    super._activateCoreListeners(html);
    if ( !this.form ) return;
    if ( !this.isEditable ) {
      return this._disableFields(this.form);
    }
    this.form.onsubmit = this._onSubmit.bind(this);
  }

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

  /** @inheritdoc */
  activateListeners(html) {
    super.activateListeners(html);
    if ( !this.isEditable ) return;
    const changeElements = ["input", "select", "textarea"].concat(this.constructor._customElements);
    html.on("change", changeElements.join(","), this._onChangeInput.bind(this));
    html.find(".editor-content[data-edit]").each((i, div) => this._activateEditor(div));
    html.find("button.file-picker").click(this._activateFilePicker.bind(this));
    if ( this._priorState <= this.constructor.RENDER_STATES.NONE ) html.find("[autofocus]")[0]?.focus();
  }

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

  /**
   * If the form is not editable, disable its input fields
   * @param {HTMLElement} form    The form HTML
   * @protected
   */
  _disableFields(form) {
    const inputs = ["INPUT", "SELECT", "TEXTAREA", "BUTTON"];
    for ( let i of inputs ) {
      for ( let el of form.getElementsByTagName(i) ) {
        if ( i === "TEXTAREA" ) el.readOnly = true;
        else el.disabled = true;
      }
    }
  }

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

  /**
   * Handle standard form submission steps
   * @param {Event} event               The submit event which triggered this handler
   * @param {object | null} [updateData]  Additional specific data keys/values which override or extend the contents of
   *                                    the parsed form. This can be used to update other flags or data fields at the
   *                                    same time as processing a form submission to avoid multiple database operations.
   * @param {boolean} [preventClose]    Override the standard behavior of whether to close the form on submit
   * @param {boolean} [preventRender]   Prevent the application from re-rendering as a result of form submission
   * @returns {Promise}                 A promise which resolves to the validated update data
   * @protected
   */
  async _onSubmit(event, {updateData=null, preventClose=false, preventRender=false}={}) {
    event.preventDefault();

    // Prevent double submission
    const states = this.constructor.RENDER_STATES;
    if ( (this._state === states.NONE) || !this.isEditable || this._submitting ) return false;
    this._submitting = true;

    // Process the form data
    const formData = this._getSubmitData(updateData);

    // Handle the form state prior to submission
    let closeForm = this.options.closeOnSubmit && !preventClose;
    const priorState = this._state;
    if ( preventRender ) this._state = states.RENDERING;
    if ( closeForm ) this._state = states.CLOSING;

    // Trigger the object update
    try {
      await this._updateObject(event, formData);
    }
    catch(err) {
      console.error(err);
      closeForm = false;
      this._state = priorState;
    }

    // Restore flags and optionally close the form
    this._submitting = false;
    if ( preventRender ) this._state = priorState;
    if ( closeForm ) await this.close({submit: false, force: true});
    return formData;
  }

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

  /**
   * Get an object of update data used to update the form's target object
   * @param {object} updateData     Additional data that should be merged with the form data
   * @returns {object}               The prepared update data
   * @protected
   */
  _getSubmitData(updateData={}) {
    if ( !this.form ) throw new Error("The FormApplication subclass has no registered form element");
    const fd = new FormDataExtended(this.form, {editors: this.editors});
    let data = fd.object;
    if ( updateData ) data = foundry.utils.flattenObject(foundry.utils.mergeObject(data, updateData));
    return data;
  }

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

  /**
   * Handle changes to an input element, submitting the form if options.submitOnChange is true.
   * Do not preventDefault in this handler as other interactions on the form may also be occurring.
   * @param {Event} event  The initial change event
   * @protected
   */
  async _onChangeInput(event) {

    // Saving a <prose-mirror> element
    if ( event.currentTarget.matches("prose-mirror") ) return this._onSubmit(event);

    // Ignore inputs inside an editor environment
    if ( event.currentTarget.closest(".editor") ) return;

    // Handle changes to specific input types
    const el = event.target;
    if ( (el.type === "color") && el.dataset.edit ) this._onChangeColorPicker(event);
    else if ( el.type === "range" ) this._onChangeRange(event);

    // Maybe submit the form
    if ( this.options.submitOnChange ) {
      return this._onSubmit(event);
    }
  }

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

  /**
   * Handle the change of a color picker input which enters it's chosen value into a related input field
   * @param {Event} event   The color picker change event
   * @protected
   */
  _onChangeColorPicker(event) {
    const input = event.target;
    input.form[input.dataset.edit].value = input.value;
  }

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

  /**
   * Handle changes to a range type input by propagating those changes to the sibling range-value element
   * @param {Event} event  The initial change event
   * @protected
   */
  _onChangeRange(event) {
    const field = event.target.parentElement.querySelector(".range-value");
    if ( field ) {
      if ( field.tagName === "INPUT" ) field.value = event.target.value;
      else field.innerHTML = event.target.value;
    }
  }

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

  /**
   * This method is called upon form submission after form data is validated
   * @param {Event} event       The initial triggering submission event
   * @param {object} formData   The object of validated form data with which to update the object
   * @returns {Promise}         A Promise which resolves once the update operation has completed
   * @abstract
   */
  async _updateObject(event, formData) {
    throw new Error("A subclass of the FormApplication must implement the _updateObject method.");
  }

  /* -------------------------------------------- */
  /*  TinyMCE Editor                              */
  /* -------------------------------------------- */

  /**
   * Activate a named TinyMCE text editor
   * @param {string} name             The named data field which the editor modifies.
   * @param {object} options          Editor initialization options passed to {@link TextEditor.create}.
   * @param {string} initialContent   Initial text content for the editor area.
   * @returns {Promise<TinyMCE.Editor|ProseMirror.EditorView>}
   */
  async activateEditor(name, options={}, initialContent="") {
    const editor = this.editors[name];
    if ( !editor ) throw new Error(`${name} is not a registered editor name!`);
    options = foundry.utils.mergeObject(editor.options, options);
    if ( !options.fitToSize ) options.height = options.target.offsetHeight;
    if ( editor.hasButton ) editor.button.style.display = "none";
    const instance = editor.instance = editor.mce = await TextEditor.create(options, initialContent || editor.initial);
    options.target.closest(".editor")?.classList.add(options.engine ?? "tinymce");
    editor.changed = false;
    editor.active = true;

    // Legacy behavior to support TinyMCE.
    // We could remove this in the future if we drop official support for TinyMCE.
    if ( options.engine !== "prosemirror" ) {
      instance.focus();
      instance.on("change", () => editor.changed = true);
    }
    return instance;
  }

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

  /**
   * Handle saving the content of a specific editor by name
   * @param {string} name                      The named editor to save
   * @param {object} [options]
   * @param {boolean} [options.remove]         Remove the editor after saving its content
   * @param {boolean} [options.preventRender]  Prevent normal re-rendering of the sheet after saving.
   * @returns {Promise<void>}
   */
  async saveEditor(name, {remove=true, preventRender}={}) {
    const editor = this.editors[name];
    if ( !editor || !editor.instance ) throw new Error(`${name} is not an active editor name!`);
    editor.active = false;
    const instance = editor.instance;
    await this._onSubmit(new Event("submit"), { preventRender });

    // Remove the editor
    if ( remove ) {
      instance.destroy();
      editor.instance = editor.mce = null;
      if ( editor.hasButton ) editor.button.style.display = "block";
      this.render();
    }
    editor.changed = false;
  }

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

  /**
   * Activate an editor instance present within the form
   * @param {HTMLElement} div  The element which contains the editor
   * @protected
   */
  _activateEditor(div) {

    // Get the editor content div
    const name = div.dataset.edit;
    const engine = div.dataset.engine || "tinymce";
    const collaborate = div.dataset.collaborate === "true";
    const button = div.previousElementSibling;
    const hasButton = button && button.classList.contains("editor-edit");
    const wrap = div.parentElement.parentElement;
    const wc = div.closest(".window-content");

    // Determine the preferred editor height
    const heights = [wrap.offsetHeight, wc ? wc.offsetHeight : null];
    if ( div.offsetHeight > 0 ) heights.push(div.offsetHeight);
    const height = Math.min(...heights.filter(h => Number.isFinite(h)));

    // Get initial content
    const options = {
      target: div,
      fieldName: name,
      save_onsavecallback: () => this.saveEditor(name),
      height, engine, collaborate
    };
    if ( engine === "prosemirror" ) options.plugins = this._configureProseMirrorPlugins(name, {remove: hasButton});

    // Define the editor configuration
    const initial = foundry.utils.getProperty(this.object, name);
    const editor = this.editors[name] = {
      options,
      target: name,
      button: button,
      hasButton: hasButton,
      mce: null,
      instance: null,
      active: !hasButton,
      changed: false,
      initial
    };

    // Activate the editor immediately, or upon button click
    const activate = () => {
      editor.initial = foundry.utils.getProperty(this.object, name);
      this.activateEditor(name, {}, editor.initial);
    };
    if ( hasButton ) button.onclick = activate;
    else activate();
  }

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

  /**
   * Configure ProseMirror plugins for this sheet.
   * @param {string} name                    The name of the editor.
   * @param {object} [options]               Additional options to configure the plugins.
   * @param {boolean} [options.remove=true]  Whether the editor should destroy itself on save.
   * @returns {object}
   * @protected
   */
  _configureProseMirrorPlugins(name, {remove=true}={}) {
    return {
      menu: ProseMirror.ProseMirrorMenu.build(ProseMirror.defaultSchema, {
        destroyOnSave: remove,
        onSave: () => this.saveEditor(name, {remove})
      }),
      keyMaps: ProseMirror.ProseMirrorKeyMaps.build(ProseMirror.defaultSchema, {
        onSave: () => this.saveEditor(name, {remove})
      })
    };
  }

  /* -------------------------------------------- */
  /*  Methods                                     */
  /* -------------------------------------------- */

  /** @inheritdoc */
  async close(options={}) {
    const states = Application.RENDER_STATES;
    if ( !options.force && ![states.RENDERED, states.ERROR].includes(this._state) ) return;

    // Trigger saving of the form
    const submit = options.submit ?? this.options.submitOnClose;
    if ( submit ) await this.submit({preventClose: true, preventRender: true});

    // Close any open FilePicker instances
    for ( let fp of (this.#filepickers) ) fp.close();
    this.#filepickers.length = 0;
    for ( const fp of this.element[0].querySelectorAll("file-picker") ) fp.picker?.close();

    // Close any open MCE editors
    for ( let ed of Object.values(this.editors) ) {
      if ( ed.mce ) ed.mce.destroy();
    }
    this.editors = {};

    // Close the application itself
    return super.close(options);
  }

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

  /**
   * Submit the contents of a Form Application, processing its content as defined by the Application
   * @param {object} [options]            Options passed to the _onSubmit event handler
   * @returns {Promise<FormApplication>}  Return a self-reference for convenient method chaining
   */
  async submit(options={}) {
    if ( this._submitting ) return this;
    const submitEvent = new Event("submit");
    await this._onSubmit(submitEvent, options);
    return this;
  }

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

  /**
   * @deprecated since v12
   * @ignore
   */
  get filepickers() {
    foundry.utils.logCompatibilityWarning("FormApplication#filepickers is deprecated and replaced by the <file-picker>"
      + "HTML element", {since: 12, until: 14, once: true});
    return this.#filepickers;
  }

  #filepickers = [];

  /**
   * @deprecated since v12
   * @ignore
   */
  _activateFilePicker(event) {
    foundry.utils.logCompatibilityWarning("FormApplication#_activateFilePicker is deprecated without replacement",
      {since: 12, until: 14, once: true});
    event.preventDefault();
    const options = this._getFilePickerOptions(event);
    const fp = new FilePicker(options);
    this.#filepickers.push(fp);
    return fp.browse();
  }

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

  /**
   * @deprecated since v12
   * @ignore
   */
  _getFilePickerOptions(event) {
    foundry.utils.logCompatibilityWarning("FormApplication#_getFilePickerOptions is deprecated without replacement",
      {since: 12, until: 14, once: true});
    const button = event.currentTarget;
    const target = button.dataset.target;
    const field = button.form[target] || null;
    return {
      field: field,
      type: button.dataset.type,
      current: field?.value ?? "",
      button: button,
      callback: this._onSelectFile.bind(this)
    };
  }

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

  /**
   * @deprecated since v12
   * @ignore
   */
  _onSelectFile(selection, filePicker) {}
}


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

/**
 * @typedef {FormApplicationOptions} DocumentSheetOptions
 * @property {number} viewPermission                The default permissions required to view this Document sheet.
 * @property {HTMLSecretConfiguration[]} [secrets]  An array of {@link HTMLSecret} configuration objects.
 */

/**
 * Extend the FormApplication pattern to incorporate specific logic for viewing or editing Document instances.
 * See the FormApplication documentation for more complete description of this interface.
 *
 * @extends {FormApplication}
 * @abstract
 * @interface
 */
class DocumentSheet extends FormApplication {
  /**
   * @param {Document} object                    A Document instance which should be managed by this form.
   * @param {DocumentSheetOptions} [options={}]  Optional configuration parameters for how the form behaves.
   */
  constructor(object, options={}) {
    super(object, options);
    this._secrets = this._createSecretHandlers();
  }

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

  /**
   * The list of handlers for secret block functionality.
   * @type {HTMLSecret[]}
   * @protected
   */
  _secrets = [];

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

  /**
   * @override
   * @returns {DocumentSheetOptions}
   */
  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ["sheet"],
      template: `templates/sheets/${this.name.toLowerCase()}.html`,
      viewPermission: CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED,
      sheetConfig: true,
      secrets: []
    });
  }

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

  /**
   * A semantic convenience reference to the Document instance which is the target object for this form.
   * @type {ClientDocument}
   */
  get document() {
    return this.object;
  }

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

  /** @inheritdoc */
  get id() {
    return `${this.constructor.name}-${this.document.uuid.replace(/\./g, "-")}`;
  }

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

  /** @inheritdoc */
  get isEditable() {
    let editable = this.options.editable && this.document.isOwner;
    if ( this.document.pack ) {
      const pack = game.packs.get(this.document.pack);
      if ( pack.locked ) editable = false;
    }
    return editable;
  }

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

  /** @inheritdoc */
  get title() {
    const reference = this.document.name ? `: ${this.document.name}` : "";
    return `${game.i18n.localize(this.document.constructor.metadata.label)}${reference}`;
  }

  /* -------------------------------------------- */
  /*  Methods                                     */
  /* -------------------------------------------- */

  /** @inheritdoc */
  async close(options={}) {
    await super.close(options);
    delete this.object.apps?.[this.appId];
  }

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

  /** @inheritdoc */
  getData(options={}) {
    const data = this.document.toObject(false);
    const isEditable = this.isEditable;
    return {
      cssClass: isEditable ? "editable" : "locked",
      editable: isEditable,
      document: this.document,
      data: data,
      limited: this.document.limited,
      options: this.options,
      owner: this.document.isOwner,
      title: this.title
    };
  }

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

  /** @inheritdoc */
  _activateCoreListeners(html) {
    super._activateCoreListeners(html);
    if ( this.isEditable ) html.find("img[data-edit]").on("click", this._onEditImage.bind(this));
    if ( !this.document.isOwner ) return;
    this._secrets.forEach(secret => secret.bind(html[0]));
  }

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

  /** @inheritdoc */
  async activateEditor(name, options={}, initialContent="") {
    const editor = this.editors[name];
    options.document = this.document;
    if ( editor?.options.engine === "prosemirror" ) {
      options.plugins = foundry.utils.mergeObject({
        highlightDocumentMatches: ProseMirror.ProseMirrorHighlightMatchesPlugin.build(ProseMirror.defaultSchema)
      }, options.plugins);
    }
    return super.activateEditor(name, options, initialContent);
  }

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

  /** @inheritDoc */
  async _render(force, options={}) {

    // Verify user permission to view and edit
    if ( !this._canUserView(game.user) ) {
      if ( !force ) return;
      const err = game.i18n.format("SHEETS.DocumentSheetPrivate", {
        type: game.i18n.localize(this.object.constructor.metadata.label)
      });
      ui.notifications.warn(err);
      return;
    }
    options.editable = options.editable ?? this.object.isOwner;

    // Parent class rendering workflow
    await super._render(force, options);

    // Register the active Application with the referenced Documents
    this.object.apps[this.appId] = this;
  }

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

  /** @inheritDoc */
  async _renderOuter() {
    const html = await super._renderOuter();
    this._createDocumentIdLink(html);
    return html;
  }

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

  /**
   * Create an ID link button in the document sheet header which displays the document ID and copies to clipboard
   * @param {jQuery} html
   * @protected
   */
  _createDocumentIdLink(html) {
    if ( !(this.object instanceof foundry.abstract.Document) || !this.object.id ) return;
    const title = html.find(".window-title");
    const label = game.i18n.localize(this.object.constructor.metadata.label);
    const idLink = document.createElement("a");
    idLink.classList.add("document-id-link");
    idLink.ariaLabel = game.i18n.localize("SHEETS.CopyUuid");
    idLink.dataset.tooltip = `SHEETS.CopyUuid`;
    idLink.dataset.tooltipDirection = "UP";
    idLink.innerHTML = '<i class="fa-solid fa-passport"></i>';
    idLink.addEventListener("click", event => {
      event.preventDefault();
      game.clipboard.copyPlainText(this.object.uuid);
      ui.notifications.info(game.i18n.format("DOCUMENT.IdCopiedClipboard", {label, type: "uuid", id: this.object.uuid}));
    });
    idLink.addEventListener("contextmenu", event => {
      event.preventDefault();
      game.clipboard.copyPlainText(this.object.id);
      ui.notifications.info(game.i18n.format("DOCUMENT.IdCopiedClipboard", {label, type: "id", id: this.object.id}));
    });
    title.append(idLink);
  }

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

  /**
   * Test whether a certain User has permission to view this Document Sheet.
   * @param {User} user     The user requesting to render the sheet
   * @returns {boolean}     Does the User have permission to view this sheet?
   * @protected
   */
  _canUserView(user) {
    return this.object.testUserPermission(user, this.options.viewPermission);
  }

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

  /**
   * Create objects for managing the functionality of secret blocks within this Document's content.
   * @returns {HTMLSecret[]}
   * @protected
   */
  _createSecretHandlers() {
    if ( !this.document.isOwner || this.document.compendium?.locked ) return [];
    return this.options.secrets.map(config => {
      config.callbacks = {
        content: this._getSecretContent.bind(this),
        update: this._updateSecret.bind(this)
      };
      return new HTMLSecret(config);
    });
  }

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

  /** @inheritdoc */
  _getHeaderButtons() {
    let buttons = super._getHeaderButtons();

    // Compendium Import
    if ( (this.document.constructor.name !== "Folder") && !this.document.isEmbedded &&
          this.document.compendium && this.document.constructor.canUserCreate(game.user) ) {
      buttons.unshift({
        label: "Import",
        class: "import",
        icon: "fas fa-download",
        onclick: async () => {
          await this.close();
          return this.document.collection.importFromCompendium(this.document.compendium, this.document.id);
        }
      });
    }

    // Sheet Configuration
    if ( this.options.sheetConfig && this.isEditable && (this.document.getFlag("core", "sheetLock") !== true) ) {
      buttons.unshift({
        label: "Sheet",
        class: "configure-sheet",
        icon: "fas fa-cog",
        onclick: ev => this._onConfigureSheet(ev)
      });
    }
    return buttons;
  }

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

  /**
   * Get the HTML content that a given secret block is embedded in.
   * @param {HTMLElement} secret  The secret block.
   * @returns {string}
   * @protected
   */
  _getSecretContent(secret) {
    const edit = secret.closest("[data-edit]")?.dataset.edit;
    if ( edit ) return foundry.utils.getProperty(this.document, edit);
  }

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

  /**
   * Update the HTML content that a given secret block is embedded in.
   * @param {HTMLElement} secret         The secret block.
   * @param {string} content             The new content.
   * @returns {Promise<ClientDocument>}  The updated Document.
   * @protected
   */
  _updateSecret(secret, content) {
    const edit = secret.closest("[data-edit]")?.dataset.edit;
    if ( edit ) return this.document.update({[edit]: content});
  }

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

  /**
   * Handle requests to configure the default sheet used by this Document
   * @param event
   * @private
   */
  _onConfigureSheet(event) {
    event.preventDefault();
    new DocumentSheetConfig(this.document, {
      top: this.position.top + 40,
      left: this.position.left + ((this.position.width - DocumentSheet.defaultOptions.width) / 2)
    }).render(true);
  }

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

  /**
   * Handle changing a Document's image.
   * @param {MouseEvent} event  The click event.
   * @returns {Promise}
   * @protected
   */
  _onEditImage(event) {
    const attr = event.currentTarget.dataset.edit;
    const current = foundry.utils.getProperty(this.object, attr);
    const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {};
    const fp = new FilePicker({
      current,
      type: "image",
      redirectToRoot: img ? [img] : [],
      callback: path => {
        event.currentTarget.src = path;
        if ( this.options.submitOnChange ) return this._onSubmit(event);
      },
      top: this.position.top + 40,
      left: this.position.left + 10
    });
    return fp.browse();
  }

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

  /** @inheritdoc */
  async _updateObject(event, formData) {
    if ( !this.object.id ) return;
    return this.object.update(formData);
  }
}
