/**
 * This module contains data field classes which are used to define a data schema.
 * A data field is responsible for cleaning, validation, and initialization of the value assigned to it.
 * Each data field extends the {@link foundry.data.fields.DataField} class to implement logic specific to its
 * contained data type.
 * @module fields
 */

import {
  ALL_DOCUMENT_TYPES,
  BASE_DOCUMENT_TYPE,
  DOCUMENT_OWNERSHIP_LEVELS,
  FILE_CATEGORIES
} from "../constants.mjs";
import {
  isColorString,
  isValidId,
  isJSON,
  hasFileExtension,
  isBase64Data
} from "./validators.mjs";
import {
  applyDataOperators,
  deepClone,
  diffObject,
  isEmpty,
  isPlainObject,
  isSubclass,
  iterateEntries,
  iterateKeys,
  objectValues,
  mergeObject,
  parseUuid,
  expandObjectInPlace,
  randomID
} from "../utils/helpers.mjs";
import SingletonEmbeddedCollection from "../abstract/singleton-collection.mjs";
import EmbeddedCollection from "../abstract/embedded-collection.mjs";
import EmbeddedCollectionDelta from "../abstract/embedded-collection-delta.mjs";
import {AsyncFunction, Color, buildRelativeUuid, equals} from "../utils/_module.mjs";
import {
  DataFieldOperator,
  ForcedReplacement,
  ForcedDeletion,
  OPERATOR_IDENTIFIER,
  reconstructOperator
} from "./operators.mjs";
import {DataModelValidationError, DataModelValidationFailure} from "./validation-failure.mjs";

/**
 * @import {EffectChangeData} from "../documents/_types.mjs";
 * @import {
 *   ArrayFieldOptions,
 *   ChoiceInputConfig,
 *   CodeMirrorInputConfig,
 *   DataFieldContext,
 *   DataFieldOptions,
 *   DataFieldValidationOptions,
 *   DataModelCleaningOptions,
 *   DataModelSanitizationOptions,
 *   DataModelUpdateState,
 *   DocumentStats,
 *   DocumentUUIDFieldOptions,
 *   EmbeddedCollectionUpdateContext,
 *   FilePathFieldOptions,
 *   FormGroupConfig,
 *   FormInputConfig,
 *   GridOffsetFieldOptions,
 *   GridOffsetsFieldOptions,
 *   JavaScriptFieldOptions,
 *   NumberFieldOptions,
 *   StringFieldInputConfig,
 *   StringFieldOptions,
 *   TypedObjectFieldOptions
 * } from "./_types.mjs";
 * @import {Document} from "../abstract/_module.mjs";
 * @import {DataSchema, DataModelUpdateOptions} from "../abstract/_types.mjs";
 * @import {FormSelectOption} from "../../client/applications/forms/fields.mjs"
 * @import {Roll, ReplaceFormulaDataOptions} from "../../client/dice/_module.mjs";
 */

/* ---------------------------------------- */
/*  Abstract Data Field                     */
/* ---------------------------------------- */

/**
 * An abstract class that defines the base pattern for a data field within a data schema.
 * @property {string} name                The name of this data field within the schema that contains it.
 * @mixes DataFieldOptions
 */
class DataField {
  /**
   * @param {DataFieldOptions} [options]    Options which configure the behavior of the field
   * @param {DataFieldContext} [context]    Additional context which describes the field
   */
  constructor(options={}, {name, parent}={}) {
    this.name = name;
    this.parent = parent;
    this.options = options;
    for ( const k in this.constructor._defaults ) {
      this[k] = k in this.options ? this.options[k] : this.constructor._defaults[k];
    }
    if ( typeof this.migrateSource === "function" ) {
      foundry.utils.logCompatibilityWarning(`${this.constructor.name}#migrateSource is deprecated in favor of
        ${this.constructor.name}#_migrate which has an expanded signature passing options and cleaning state.`,
        {since: 14, until: 16, once: true});
      this.#legacyMigrateSource = true;
    }
  }

  /**
   * The field name of this DataField instance.
   * This is assigned by SchemaField#initialize.
   * @internal
   */
  name;

  /**
   * A reference to the parent schema to which this DataField belongs.
   * This is assigned by SchemaField#initialize.
   * @internal
   */
  parent;

  /**
   * The initially provided options which configure the data field
   * @type {DataFieldOptions}
   */
  options;

  /** @deprecated since v14 */
  #legacyMigrateSource = false;

  /**
   * Is a value of this field written to source data? A Non-persisted value is initialized (with its initial value), and
   * ActiveEffects can use the field for change application.
   * @type {boolean}
   */
  persisted = true;

  /**
   * Whether this field defines part of a Document/Embedded Document hierarchy.
   * @type {boolean}
   */
  static hierarchical = false;

  /**
   * Does this field type contain other fields in a recursive structure?
   * Examples of recursive fields are SchemaField, ArrayField, or TypeDataField
   * Examples of non-recursive fields are StringField, NumberField, or ObjectField
   * @type {boolean}
   */
  static recursive = false;

  /**
   * Default parameters for this field type
   * @returns {DataFieldOptions}
   * @protected
   */
  static get _defaults() {
    return {
      required: false,
      nullable: false,
      initial: undefined,
      readonly: false,
      persisted: true,
      gmOnly: false,
      label: "",
      hint: "",
      placeholder: "",
      validationError: "is not a valid value"
    };
  }

  /**
   * A dot-separated string representation of the field path within the parent schema.
   * @type {string}
   */
  get fieldPath() {
    return [this.parent?.fieldPath, this.name].filterJoin(".");
  }

  /**
   * Apply a function to this DataField which propagates through recursively to any contained data schema.
   * @param {string|Function} fn          The function to apply
   * @param {*} value                     The current value of this field
   * @param {object} [options={}]         Additional options passed to the applied function
   * @returns {object}                    The results object
   */
  apply(fn, value, options={}) {
    if ( typeof fn === "string" ) fn = this[fn];
    return fn.call(this, value, options);
  }

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

  /**
   * Recursively traverse a schema and retrieve a field specification by a given path/key
   * @param {string[]} parts             The field path or property key if `source` is passed as an array of strings
   *                                     (in reverse order)
   * @param {object} [options]           Additional options
   * @param {object} [options.source]    The source data of the field
   * @param {object} [options.type]      The Document type of the parent field
   * @returns {DataField|undefined}      The corresponding DataField definition for that field, or undefined
   * @protected
   */
  _getField(parts, {source, type}={}) {
    return parts.length ? undefined : this;
  }

  /* -------------------------------------------- */
  /*  Field Cleaning                              */
  /* -------------------------------------------- */

  static #cleanDefaultTrue = ["copy", "expand", "migrate", "model", "prune", "persisted"];

  /**
   * Preprocess and clean source data to ensure that it conforms to the correct data type for this field and is ready
   * to be used by data model construction or update operations.
   * Data cleaning is synchronous and automatically applied as the first step of construction and update operations.
   *
   * @param {any} value                           An initial requested value
   * @param {Readonly<DataModelCleaningOptions>} [options]  Additional options for how the field is cleaned
   * @param {DataModelUpdateState} [_state]       Internal state variables which are used during recursion
   * @returns {any}                               The cleaned value
   */
  clean(value, options={}, _state={}) {
    if ( !_state.cleanOptions && !Object.isFrozen(options) ) {
      for ( const key of DataField.#cleanDefaultTrue ) options[key] ??= true;
      _state.cleanOptions = true;
    }

    // Migrate a prior stored value to an appropriate value for this field
    if ( options.migrate ) value = this._migrate(value, options, _state);

    // Handle special values and initial defaults
    if ( value === undefined ) return this.getInitialValue(_state.modelSource ?? {});
    try {
      const isValid = this._validateSpecial(value);
      if ( isValid === true ) return value;
    } catch(err) {
      return this.getInitialValue(_state.modelSource ?? {});
    }

    // Cast a provided value to the correct type
    value = this._cast(value);

    // Cleaning logic specific to the DataField.
    value = this._cleanType(value, options, _state);

    // Sanitization of user input
    if ( options.sanitize ) value = this._sanitize(value, options.sanitize, _state);
    return value;
  }

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

  /**
   * Attempt to retrieve a valid initial value for the DataField.
   * @param {object} source   Model source data object within which an initial value is required for this field
   * @returns {*}             A proposed initial value
   */
  getInitialValue(source) {
    if ( typeof this.initial === "function" ) return this.initial(source);  // Explicit function
    else if ( this.initial !== undefined ) return this.initial;           // Explicit value
    if ( !this.required ) return undefined;                               // Prefer undefined if non-required
    if ( this.nullable ) return null;                                     // Prefer explicit null
    return undefined;                                                     // Otherwise undefined
  }

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

  /**
   * Migrate this field's candidate source data.
   * This workflow occurs as a component step of DataField#clean.
   *
   *
   * @param {any} value                           Candidate source value for the field
   * @param {Readonly<DataModelCleaningOptions>} [options]  Options for how the field is cleaned
   * @param {DataModelUpdateState} [_state]       Internal state variables transacted during cleaning recursion.
   * @returns {any}                               A migrated value suitable for cleaning
   * @protected
   */
  _migrate(value, options, _state) {
    /** @deprecated since v14 */
    if ( this.#legacyMigrateSource ) this.migrateSource(_state.modelSource, value); // Did not return any value
    return value;
  }

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

  /**
   * Cast a non-default value to ensure it is the correct type for the field
   * @param {*} value       The provided non-default value
   * @returns {*}           The standardized value
   * @protected
   */
  _cast(value) {
    return value;
  }

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

  /**
   * Apply any cleaning logic specific to this DataField type.
   * @param {any} value         A candidate value that has been cast to the appropriate type
   * @param {Readonly<DataModelCleaningOptions>} [options]  Options for how the field is cleaned
   * @param {DataModelUpdateState} [_state] Internal state variables which are used during recursion
   * @returns {any}             The cleaned value
   * @protected
   */
  _cleanType(value, options, _state) {
    return value;
  }

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

  /**
   * Apply special sanitization workflows to this value.
   * This method is reserved for implementation in server-side workflows.
   * @param {any} value         A candidate value that has been cast and cleaned
   * @param {Readonly<DataModelSanitizationOptions>} [options]  Options for how the field is sanitized
   * @param {DataModelUpdateState} [state] Internal state variables which are used during recursion
   * @returns {any}             The sanitized value
   * @internal
   * @ignore
   */
  _sanitize(value, options, state) {

    // GM-only fields
    if ( this.gmOnly && (options.user?.isGM === false) ) {
      if ( state.creation && !this.required ) return undefined;
      throw new Error(`${this.fieldPath} may only be modified by a GM or Assistant GM user.`);
    }

    // Falseish values don't require sanitization
    if ( !value || (value instanceof ForcedDeletion) ) return value;

    // Custom sanitization method
    if ( typeof this._sanitizeType === "function" ) {
      let result = value;
      if ( value instanceof ForcedReplacement ) result = applyDataOperators(value);
      const sanitized = this._sanitizeType(result, options, state);
      if ( value instanceof ForcedReplacement ) ForcedReplacement.set(value, sanitized);
      else value = sanitized;
    }
    return value;
  }

  /* -------------------------------------------- */
  /*  Field Validation                            */
  /* -------------------------------------------- */

  /**
   * Iterate over validation functions applied to this field.
   * Validation happens in a depth-first way, for example consider an outerSchema with an innerField:
   *
   * @example Validator Invocation Order
   * ```
   * outerSchema
   *   _validateSpecial
   *   _validateType
   *   _validateRecursive
   *   outerSchema.options.validate
   *     innerField
   *       _validateSpecial
   *       _validateType
   *       innerField.options.validate
   *       _validateModel
   *   _validateModel
   * ```
   * @param {Pick<DataFieldValidationOptions,"phase"|"recursive">} options
   * @yields {(any) => boolean}
   */
  * validators({phase, recursive=true}={}) {
    if ( !phase || (phase === "pre") ) {
      yield this._validateSpecial;      // Special values that are always valid or invalid for the field
      yield this._validateType;         // Validate the expected type for the field
    }
    if ( this.constructor.recursive && recursive && (!phase || (phase === "recursive")) ) {
      yield this._validateRecursive;    // Recursive validation of child fields
    }
    if ( !phase || (phase === "post") ) {
      if ( typeof this.options.validate === "function" ) {
        yield this.options.validate;    // Custom instance-level validation rules
      }
      yield this._validateModel;        // Joint validations on this field and its children
    }
  }

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

  /**
   * Validate candidate input for this field, ensuring it meets the field requirements.
   *
   * This public method validates recursively, testing both the immediate value of this field as well as the validity
   * of any child values for fields with hierarchical data types.
   *
   * @param {any} value                               The candidate value to validate
   * @param {DataFieldValidationOptions} [options={}] Options which affect validation behavior
   * @returns {DataModelValidationFailure|void}       A failure that occurred if validation is non-strict.
   *                                                  The failure may have been resolved by fallback or dropping
   *                                                  invalid elements.
   * @throws {DataModelValidationFailure}             A failure error type including details of the failure if
   *                                                  validation is performed strictly.
   */
  validate(value, options={}) {
    // TODO: Remove the next line. value shouldn't be a data operator or contain data operators.
    // The latter case isn't even handled.
    value = DataFieldOperator.get(value);
    let failure;
    try {
      this.#validate(value, options);
    } catch(err) {
      failure = err;
    }
    if ( !failure ) return;

    // Wrap errors in the DataModelValidationFailure type
    if ( !(failure instanceof DataModelValidationFailure) ) {
      failure = new DataModelValidationFailure(failure.message, {
        invalidValue: value,
        fieldPath: this.fieldPath,
        unresolved: true,
        cause: failure
      });
    }

    // Return or throw
    if ( options.strict && failure.unresolved ) throw failure;
    return failure;
  }

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

  /**
   * A private function that guarantees that all validators are called using the same handling logic.
   * @param {any} value                         The candidate value that is desired as an update
   * @param {DataModelUpdateOptions} options    Options which modify how the model update workflow is performed
   * @throws {Error}                            An error if the proposed value is not valid
   */
  #validate(value, options) {
    for ( const validator of this.validators(options) ) {
      let result;
      try {
        result = validator.call(this, value, options);
      }
      finally {
        if ( result === true ) return;  // Guaranteed to be valid
        if ( result === false ) {       // Invalid but without specific Error
          throw new Error(this.validationError || `Invalid value "${JSON.stringify(value)}" for ${this.fieldPath}`);
        }
      }
    }
  }

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

  /**
   * Special validation rules which supersede regular field validation.
   * This validator screens for certain values which are otherwise incompatible with this field like null or undefined.
   * @param {*} value                                  The candidate value
   * @returns {boolean|void}                           A boolean to indicate with certainty whether the value is valid
   * @throws {Error}                                   An error with a specific reason the value is invalid
   * @protected
   */
  _validateSpecial(value) {

    // Allow null values for explicitly nullable fields
    if ( value === null ) {
      if ( this.nullable ) return true;
      else throw new Error("may not be null");
    }

    // Allow undefined if the field is not required
    if ( value === undefined ) {
      if ( this.required ) throw new Error("may not be undefined");
      else return true;
    }
  }

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

  /**
   * A default type-specific validator that can be overridden by child classes
   * This method should validate only the value at the current hierarchy level, rather than validating recursively.
   * This method should throw if any validation error occurred, even if resolved by fallback or dropping.
   *
   * @param {*} value                                  The candidate value
   * @param {DataFieldValidationOptions} [options={}]  Options which affect validation behavior
   * @returns {boolean|void}                           A boolean to indicate with certainty whether the value is valid
   * @throws {Error}                                   An error with a specific reason the value is invalid
   * @protected
   */
  _validateType(value, options={}) {}

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

  /**
   * For fields which have hierarchical data structures, define how their inner fields should be validated.
   * This method is called after _validateSpecial and _validateType. It can be assumed that those tests passed.
   * This method should throw if any validation error occurred, even if resolved by fallback or dropping.
   *
   * @param {*} value                                  The candidate value
   * @param {DataFieldValidationOptions} [options={}]  Options which affect validation behavior
   * @returns {boolean|void}                           A boolean to indicate with certainty whether the value is valid
   * @throws {Error}                                   An error with a specific reason the value is invalid
   * @protected
   */
  _validateRecursive(value, options) {}

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

  /**
   * Certain fields may declare joint data validation criteria.
   * This method will only be called if the field is designated as recursive.
   * This method should throw if any validation error occurred, even if resolved by fallback or dropping.
   *
   * @param {object} data       Candidate data for joint model validation
   * @param {object} options    Options which modify joint model validation
   * @throws {Error}            An error if joint model validation fails
   * @internal
   */
  _validateModel(data, options={}) {}

  /* -------------------------------------------- */
  /*  Initialization and Updates                  */
  /* -------------------------------------------- */

  /**
   * Initialize the original source data into a mutable copy for the DataModel instance.
   * @param {unknown} value             The source value of the field
   * @param {object} model              The DataModel instance that this field belongs to
   * @param {object} [options]          Initialization options
   * @returns {unknown}                 An initialized copy of the source data
   */
  initialize(value, model, options={}) {
    return value;
  }

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

  /**
   * Update the source data for a DataModel which includes this DataField.
   * Updates are performed depth-first; validated and recorded from leaf-nodes upwards.
   *
   * This method is responsible for preparing and validating the requested update.
   * If the update is valid, it is responsible for updating the pending source copy and recording the diff.
   * If the update is invalid it must avoid recording a diff or staging an update to the source copy.
   * If validation fails, this workflow may optionally permit `fallback` to a valid initial value.
   *
   *                                      a partial node of source data, relative to which this field belongs.
   * @param {string} key                  The name of this field within the context of the source data.
   * @param {any} value                   The candidate value that should be applied as an update.
   * @param {DataModelUpdateOptions} options Options which modify how this update workflow is performed.
   * @param {DataModelUpdateState} state  Stateful data that is transacted throughout the model hierarchy during an
   *                                      update operation.
   * @throws {Error}                      An error if the requested update cannot be performed.
   * @internal
   */
  _updateDiff(key, value, options, state) {
    const current = state.source[key];
    if ( value === current ) return;
    let result = value;

    // DataFieldOperator instances are applied directly to the new source but retained in the difference
    if ( value instanceof DataFieldOperator ) {
      result = applyDataOperators(value);
      if ( foundry.utils.equals(result, current) ) return;
    }

    // Validate requested value non-recursively
    const validationOptions = {model: options.model, recursive: false, fallback: options.fallback, strict: false};
    const failure = this.validate(result, validationOptions);
    if ( failure ) {
      state.failure.fields[key] = failure;
      SchemaField._handleValidationFailure(this, state.source, key, state.failure, failure, validationOptions);
      if ( failure.unresolved ) return; // Prevent assignment
    }

    // Valid values are applied to both difference and source
    state.diff[key] = value;
    state.source[key] = result;
  }

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

  /**
   * Commit a prepared update to DataModel#_source.
   * @param {object} source               The parent source object within which the `key` field exists
   * @param {string} key                  The named field in source to commit
   * @param {object} value                The new value of the field which should be committed to source
   * @param {object} diff                 The reported change to the field
   * @param {DataModelUpdateOptions} options Options which modify how this update workflow is performed.
   * @internal
   */
  _updateCommit(source, key, value, diff, options) {
    source[key] = value;
  }

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

  /**
   * Export the current value of the field into a serializable object.
   * @param {*} value                   The initialized value of the field
   * @returns {*}                       An exported representation of the field
   */
  toObject(value) {
    return value;
  }

  /* -------------------------------------------- */
  /*  Form Field Integration                      */
  /* -------------------------------------------- */

  /**
   * Does this form field class have defined form support?
   * @type {boolean}
   */
  static get hasFormSupport() {
    return this.prototype._toInput !== DataField.prototype._toInput;
  }

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

  /**
   * Render this DataField as an HTML element.
   * @param {FormInputConfig} config        Form element configuration parameters
   * @throws {Error}                        An Error if this DataField subclass does not support input rendering
   * @returns {HTMLElement|HTMLElement[]|HTMLCollection}  A rendered HTMLElement for the field
   */
  toInput(config={}) {
    const inputConfig = {name: this.fieldPath, ...config};
    inputConfig.placeholder ??= this.placeholder;
    if ( typeof inputConfig.input === "function" ) return config.input(this, inputConfig);
    return this._toInput(inputConfig);
  }

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

  // eslint-disable-next-line jsdoc/require-returns-check
  /**
   * Render this DataField as an HTML element.
   * Subclasses should implement this method rather than the public toInput method which wraps it.
   * @param {FormInputConfig} config        Form element configuration parameters
   * @throws {Error}                        An Error if this DataField subclass does not support input rendering
   * @returns {HTMLElement|HTMLElement[]|HTMLCollection} A rendered HTMLElement for the field
   * @protected
   */
  _toInput(config) {
    throw new Error(`The ${this.constructor.name} class does not implement the _toInput method`);
  }

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

  /**
   * Render this DataField as a standardized form-group element.
   * @param {FormGroupConfig} groupConfig   Configuration options passed to the wrapping form-group
   * @param {FormInputConfig} inputConfig   Input element configuration options passed to DataField#toInput
   * @returns {HTMLDivElement}              The rendered form group element
   */
  toFormGroup(groupConfig={}, inputConfig={}) {
    if ( typeof groupConfig.widget === "function" ) return groupConfig.widget(this, groupConfig, inputConfig);
    groupConfig.label ??= this.label ?? this.fieldPath;
    groupConfig.hint ??= this.hint;
    groupConfig.input ??= this.toInput(inputConfig);
    return foundry.applications.fields.createFormGroup(groupConfig);
  }

  /* -------------------------------------------- */
  /*  Active Effect Integration                   */
  /* -------------------------------------------- */

  /**
   * Apply an ActiveEffectChange to this field.
   * @template T
   * @param {T} value                       The field's current value.
   * @param {DataModel} model               The model instance.
   * @param {ActiveEffectChangeData} change The change to apply.
   * @param {object} options                Additional options to configure the change application.
   * @param {Record<string, unknown>} [options.replacementData] Data used to resolve "@" expressions.
   * @returns {T}                           The updated value.
   */
  applyChange(value, model, change, {replacementData={}}={}) {
    let delta;
    try {
      delta = this._castChangeDelta(change.value, replacementData);
    }
    catch(error) {
      const warningHeader = change.effect ? `Active Effect (${change.effect.uuid}) | ` : "";
      console.warn(`${warningHeader} "${change.type}" change to ${change.key} failed to resolve: ${error.message}`);
      return value;
    }
    let updated;
    switch ( change.type ) {
      case "add":
        updated = this._applyChangeAdd(value, delta, model, change);
        break;
      case "subtract":
        updated = this._applyChangeSubtract(value, delta, model, change);
        break;
      case "multiply":
        updated = this._applyChangeMultiply(value, delta, model, change);
        break;
      case "override":
        updated = this._applyChangeOverride(value, delta, model, change);
        break;
      case "upgrade":
        updated = this._applyChangeUpgrade(value, delta, model, change);
        break;
      case "downgrade":
        updated = this._applyChangeDowngrade(value, delta, model, change);
        break;
      default:
        updated = this._applyChangeCustom(value, delta, model, change);
    }

    // Clean and revalidate the updated value, returning the original value if there is a failure
    if ( !equals(updated, value) ) {
      updated = this.clean(updated, {persisted: false});
      const failure = this.validate(updated, {fallback: false, strict: false});
      if ( failure ) {
        const warningHeader = change.effect ? `Active Effect (${change.effect.uuid}) | ` : "";
        const {type, key} = change;
        const v = JSON.stringify(value);
        console.warn(`${warningHeader} "${type}" change to "${key}" with ${v} failed to validate: ${failure.message}`);
        updated = value;
      }
    }

    return this.initialize(updated, model);
  }

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

  /**
   * Cast a change delta into an appropriate type to be applied to this field.
   * @param {*} delta  The change delta.
   * @param {object} replacementData Data used to resolve "@" expressions.
   * @returns {*}
   * @protected
   */
  _castChangeDelta(delta, replacementData={}) {
    if ( typeof delta === "string" ) delta = this._replaceDataRefs(delta, replacementData);
    return this._cast(delta);
  }

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

  /**
   * Recursively replace data references in a string change value.
   * @param {string} raw
   * @param {Record<string, unknown>} data  An object providing replacements
   * @param {object} [options]              Additional options to configure the data replacement
   * @param {boolean} [options.strict=true] Throw an Error if data replacement fails
   * @returns {string} The expression with data references resolved
   * @protected
   */
  _replaceDataRefs(raw, data, {strict=true}={}) {
    const resolved = foundry.dice.Roll.defaultImplementation.replaceFormulaData(raw, data, {recursive: true});
    if ( strict && resolved.replace(/@\w+\[[^#\]]+]/g, "").includes("@") ) {
      throw new Error(`Unable to replace all data references in ${JSON.stringify(raw)}`);
    }
    return resolved;
  }

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

  /**
   * Apply an "add" change to this field.
   * @param {*} value                  The field's current value.
   * @param {*} delta                  The change delta.
   * @param {DataModel} model          The model instance.
   * @param {EffectChangeData} change  The original change data.
   * @returns {*}                      The updated value.
   * @protected
   */
  _applyChangeAdd(value, delta, model, change) {
    return value + delta;
  }

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

  /**
   * Apply a "subtract" change to this field.
   * @param {*} value                  The field's current value.
   * @param {*} delta                  The change delta.
   * @param {DataModel} model          The model instance.
   * @param {EffectChangeData} change  The original change data.
   * @returns {*}                      The updated value.
   * @protected
   */
  _applyChangeSubtract(value, delta, model, change) {
    return value;
  }

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

  /**
   * Apply a "multiply" change to this field.
   * @param {*} value                  The field's current value.
   * @param {*} delta                  The change delta.
   * @param {DataModel} model          The model instance.
   * @param {EffectChangeData} change  The original change data.
   * @returns {*}                      The updated value.
   * @protected
   */
  _applyChangeMultiply(value, delta, model, change) {
    return value;
  }

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

  /**
   * Apply an "override" change to this field.
   * @param {*} value                  The field's current value.
   * @param {*} delta                  The change delta.
   * @param {DataModel} model          The model instance.
   * @param {EffectChangeData} change  The original change data.
   * @returns {*}                      The updated value.
   * @protected
   */
  _applyChangeOverride(value, delta, model, change) {
    return delta;
  }

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

  /**
   * Apply an "upgrade" change to this field.
   * @param {*} value                  The field's current value.
   * @param {*} delta                  The change delta.
   * @param {DataModel} model          The model instance.
   * @param {EffectChangeData} change  The original change data.
   * @returns {*}                      The updated value.
   * @protected
   */
  _applyChangeUpgrade(value, delta, model, change) {
    return value;
  }

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

  /**
   * Apply a "downgrade" change to this field.
   * @param {*} value                  The field's current value.
   * @param {*} delta                  The change delta.
   * @param {DataModel} model          The model instance.
   * @param {EffectChangeData} change  The original change data.
   * @returns {*}                      The updated value.
   * @protected
   */
  _applyChangeDowngrade(value, delta, model, change) {
    return value;
  }

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

  /**
   * Apply a custom change to this field.
   * @param {*} value                  The field's current value.
   * @param {*} delta                  The change delta.
   * @param {DataModel} model          The model instance.
   * @param {EffectChangeData} change  The original change data.
   * @returns {*}                      The updated value.
   * @protected
   */
  _applyChangeCustom(value, delta, model, change) {
    const preHook = foundry.utils.getProperty(model, change.key);
    Hooks.call("applyActiveEffect", model, change, value, delta, {});
    const postHook = foundry.utils.getProperty(model, change.key);
    if ( postHook !== preHook ) return postHook;
  }
}

/* -------------------------------------------- */
/*  Data Schema Field                           */
/* -------------------------------------------- */

/**
 * A special class of {@link foundry.data.fields.DataField} which defines a data schema.
 */
class SchemaField extends DataField {
  /**
   * @param {DataSchema} fields                 The contained field definitions
   * @param {DataFieldOptions} [options]        Options which configure the behavior of the field
   * @param {DataFieldContext} [context]        Additional context which describes the field
   */
  constructor(fields, options, context={}) {
    super(options, context);
    this.fields = this._initialize(fields);
  }

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

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {required: true, nullable: false});
  }

  /** @override */
  static recursive = true;

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

  /**
   * The contained field definitions.
   * @type {DataSchema}
   */
  fields;

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

  /**
   * Initialize and validate the structure of the provided field definitions.
   * @param {DataSchema} fields     The provided field definitions
   * @returns {DataSchema}          The validated schema
   * @protected
   */
  _initialize(fields) {
    if ( !isPlainObject(fields) ) {
      throw new Error("A DataSchema must be an object with string keys and DataField values.");
    }
    fields = {...fields};
    for ( const [name, field] of Object.entries(fields) ) {
      if ( name === "_source" ) throw new Error('"_source" is not a valid name for a field of a SchemaField.');
      if ( !(field instanceof DataField) ) {
        throw new Error(`The "${name}" field is not an instance of the DataField class.`);
      }
      if ( field.parent !== undefined ) {
        throw new Error(`The "${field.fieldPath}" field already belongs to some other parent and may not be reused.`);
      }
      field.name = name;
      field.parent = this;
    }
    return fields;
  }

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

  /**
   * Extend this schema definition with additional fields.
   * @param {DataSchema} fields  The additional fields.
   */
  extendFields(fields) {
    Object.assign(this.fields, this._initialize(fields));
    this.#entries = null;
  }

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

  /**
   * Remove fields from this schema definition.
   * @param {string[]} fields  The fields to remove.
   */
  removeFields(fields) {
    for ( const fieldName of fields ) {
      delete this.fields[fieldName];
    }
    this.#entries = null;
  }

  /* -------------------------------------------- */
  /*  Schema Iteration                            */
  /* -------------------------------------------- */

  /**
   * Iterate over a SchemaField by iterating over its fields.
   * @yields {DataField}
   */
  *[Symbol.iterator]() {
    yield* objectValues(this.fields);
  }

  /**
   * An array of field names which are present in the schema.
   * @returns {string[]}
   */
  keys() {
    return this.entries().map(e => e[0]);
  }

  /**
   * An array of DataField instances which are present in the schema.
   * @returns {DataField[]}
   */
  values() {
    return this.entries().map(e => e[1]);
  }

  /**
   * An array of [name, DataField] tuples which define the schema.
   * @returns {Array<[string, DataField]>}
   */
  entries() {
    return this.#entries ??= Object.entries(this.fields);
  }

  /**
   * @type {Array<[string, DataField]>|null}
   */
  #entries = null;

  /**
   * Test whether a certain field name belongs to this schema definition.
   * @param {string} fieldName    The field name
   * @returns {boolean}           Does the named field exist in this schema?
   */
  has(fieldName) {
    return Object.hasOwn(this.fields, fieldName);
  }

  /**
   * Get a DataField instance from the schema by name.
   * @param {string} fieldName    The field name
   * @returns {DataField|void}    The DataField instance or undefined
   */
  get(fieldName) {
    if ( !this.has(fieldName) ) return;
    return this.fields[fieldName];
  }

  /**
   * Traverse the schema, obtaining the DataField definition for a particular field.
   * @param {string[]|string} pathOrKey  A field path or property key if `options.source` is given.
   *                                     Examples: `["abilities", "strength"]` or `"abilities.strength"`.
   * @param {object} [options]           Additional options
   * @param {object} [options.source]    The (partial) source data of the field
   * @returns {DataField|undefined}      The corresponding DataField definition for that field, or undefined
   */
  getField(pathOrKey, {source}={}) {
    let parts;
    if ( typeof pathOrKey === "string" ) parts = pathOrKey.split(".");
    else if ( Array.isArray(pathOrKey) ) parts = pathOrKey.slice();
    else throw new Error("A field path / property path must be an array of strings or a dot-delimited string");
    return this._getField(parts.reverse(), {source});
  }

  /** @override */
  _getField(parts, {source}={}) {
    if ( !parts.length ) return this;
    const name = parts.pop();
    const field = this.get(name);
    if ( !field ) return undefined;
    if ( (source !== undefined) && (source !== null) ) {
      if ( typeof source !== "object" ) return undefined;
      source = source[name] ?? null;
    }
    return field._getField(parts, {source});
  }

  /* -------------------------------------------- */
  /*  Data Cleaning                               */
  /* -------------------------------------------- */

  /** @override */
  getInitialValue(source) {
    const initial = super.getInitialValue(source);
    if ( this.required && (initial === undefined) ) return this._cleanType({}, {persisted: true}, {});
    return initial;
  }

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

  /** @override */
  _cast(value) {
    return isPlainObject(value) ? value : {};
  }

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

  /** @inheritdoc */
  _cleanType(data, options, _state) {

    // Expand objects if dot-notation keys were used
    SchemaField.expandObject(data, options, _state);

    // Restrict to schema and process DataFieldOperator instances
    this.#cleanKeys(data, options.prune, options.persisted);

    // Clean each field of the schema
    for ( const [name, field] of this.entries() ) {
      if ( !field.persisted && options.persisted ) continue;
      const cleanField = (name in data) || !options.partial || !_state.source; // Clean all fields if going from empty -> *
      if ( !cleanField ) continue;
      const v = data[name];
      const _innerState = {..._state, source: _state.source?.[name]};
      if ( v instanceof DataFieldOperator ) {
        if ( v instanceof ForcedReplacement ) {
          const cv = DataFieldOperator.get(v);
          DataFieldOperator.set(v, field.clean(cv, Object.freeze({...options, partial: false}), _innerState));
        }
      }
      else data[name] = field.clean(v, options, _innerState);
    }
    return data;
  }

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

  /**
   * Restrict candidate input data to the fields defined by this schema.
   * Handle the presence of DataFieldOperator instances in serialized or legacy key format.
   * @param {object} data  Candidate data for the schema field
   * @param {boolean} prune Prune keys not specified in the schema
   * @param {boolean} persisted Prune keys corresponding with non-persisted fields
   */
  #cleanKeys(data, prune, persisted) {
    for ( let k in data ) {
      const v = data[k];

      // Backwards compatibility for legacy "special keys" into DataFieldOperator instances
      /** @deprecated since v14 until v16 */
      if ( k[1] === "=" ) {
        if ( k[0] === "-" ) {
          const k2 = k.slice(2);
          foundry.utils.logCompatibilityWarning(`The {${k}: null} syntax for forced deletion of a data field value` +
            ` is no longer supported in favor of {${k2}: new foundry.data.operators.ForcedDeletion()}`,
            {since: 14, until: 16});
          if ( v !== null ) throw new Error(`Removing the value of a data field using the ${k} deletion syntax
           requires the value of that deletion key to be null. For example {${k}: null}`);
          data[k2] = new ForcedDeletion();
          delete data[k];
          k = k2;
        }
        else if ( k[0] === "=" ) {
          const k2 = k.slice(2);
          foundry.utils.logCompatibilityWarning(`The {${k}: value} syntax for forced replacement of a data field ` +
            ` value is no longer supported in favor of {${k2}: new foundry.data.operators.ForcedReplacement(value)}`,
            {since: 14, until: 16});
          data[k2] = ForcedReplacement.create(v);
          delete data[k];
          k = k2;
        }
      }

      // Discard keys not present in the schema
      const field = this.fields[k];
      if ( (!field && prune) || (persisted && (field?.persisted === false)) ) {
        delete data[k];
        continue;
      }

      // Reconstruct serialized DataFieldOperator instances
      SchemaField.reconstructOperator(data, k, v);
    }
  }

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

  /**
   * Expand a transacted object.
   * @param {object} data                       The object.
   * @param {DataModelCleaningOptions} options  Cleaning operation options.
   * @param {DataModelUpdateState} _state       Cleaning operation state.
   */
  static expandObject(data, options, _state) {
    if ( options.expand === false ) return;
    expandObjectInPlace(data, {shallow: true});
  }

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

  /**
   * When iterating over the keys and values of an object, reconstruct serialized DataFieldOperator values.
   * @param {object} data
   * @param {string} k
   * @param {any} v
   */
  static reconstructOperator(data, k, v) {
    if ( v && (typeof v === "object") && (OPERATOR_IDENTIFIER in v) ) {
      try {
        data[k] = reconstructOperator(v);
      } catch(err) {
        console.warn(err.message);
        delete data[k];
      }
    }
  }

  /* -------------------------------------------- */
  /*  Data Initialization                         */
  /* -------------------------------------------- */

  /** @override */
  initialize(value, model, options={}) {
    if ( !value ) return value;
    const data = {};
    for ( const [name, field] of this.entries() ) {
      const fieldValue = field.persisted ? value[name] : field.getInitialValue(value);
      const initialized = field.initialize(fieldValue, model, options);

      // Readonly fields
      const isFunction = typeof initialized === "function";
      const isClass = isFunction && initialized.prototype
        && Function.prototype.toString.call(initialized).startsWith("class ");
      if ( isFunction && !isClass ) {
        Object.defineProperty(data, name, {get: initialized, configurable: true, enumerable: true});
      }
      else if ( field.readonly ) {
        Object.defineProperty(data, name, {value: initialized, writable: false, enumerable: true});
      }

      // Writable fields
      else data[name] = initialized;
    }
    return data;
  }

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

  /** @inheritDoc */
  _updateDiff(key, value, options, state) {
    const result = DataFieldOperator.get(value);

    // * -> null
    if ( !result ) return super._updateDiff(key, value, options, state);

    // Pre-validation of the requested value
    const preFailure = this.validate(result, {strict: false, phase: "pre", partial: true});
    if ( preFailure?.unresolved ) {
      state.failure.fields[key] = preFailure;
      state.failure.unresolved = true;
      return;
    }

    // Recursive updates and validation of child fields
    const wasNullish = !state.source[key];
    const source = (state.source[key] ||= {});
    const diff = state.diff[key] = {};
    let failure = new DataModelValidationFailure("SchemaField#_updateDiff", {fieldPath: this.fieldPath});
    const fieldState = {...state, model: state.model, source, diff, failure};
    for ( const [k, v] of iterateEntries(result) ) {
      const field = this.get(k);
      field?._updateDiff(k, v, options, fieldState);
    }

    // Post-validation of the resulting schema
    if ( !failure.unresolved ) {
      const validationOptions = {strict: false, partial: !wasNullish};
      if ( !wasNullish ) validationOptions.phase = "post";
      failure = this.validate(source, validationOptions) ?? failure;
    }

    // Record failure
    if ( failure.unresolved ) {
      state.failure.fields[key] = failure;
      state.failure.unresolved = true;
      delete diff[key];
      return;
    }

    // Record final changes
    if ( value instanceof ForcedReplacement ) state.diff[key] = value;
    else if ( isEmpty(diff) ) delete state.diff[key];
  }

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

  /** @override */
  _updateCommit(source, key, value, diff, options) {
    const s = source[key];

    // * -> undefined, * -> null, undefined -> *, null -> *
    if ( !s || !value ) {
      source[key] = value;
      return;
    }

    // * -> *
    for ( const [k, d] of iterateEntries(diff) ) {
      const field = this.get(k);
      field?._updateCommit(s, k, value[k], d, options);
    }
  }

  /* -------------------------------------------- */
  /*  Data Validation                             */
  /* -------------------------------------------- */

  /** @override */
  _validateType(data, options={}) {
    if ( !(data instanceof Object) ) throw new Error("must be an object");
  }

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

  /** @override */
  _validateRecursive(data, options) {
    const schemaFailure = new DataModelValidationFailure("SchemaField#_validateRecursive",
      {fieldPath: this.fieldPath, unresolved: false});
    const validationOptions = {...options, strict: false};
    for ( const [key, field] of this.entries() ) {
      if ( !(key in data) && (options.partial || !field.persisted) ) continue;
      const failure = field.validate(data[key], validationOptions);
      if ( failure ) {
        schemaFailure.fields[key] = failure;
        SchemaField._handleValidationFailure(field, data, key, schemaFailure, failure, options);
      }
    }
    if ( !schemaFailure.empty ) throw schemaFailure; // Throw even if resolved
  }

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

  /**
   * An internal helper function that attempts fallback to a valid initial value in the case of validation failure.
   * @param {DataField} field                             The child data field being handled
   * @param {object} data                                 Data attempted for validation at the parent level
   * @param {string} key                                  The key of this field being validated
   * @param {DataModelValidationFailure} parentFailure    The validation failure at the parent level
   * @param {DataModelValidationFailure} fieldFailure     The validation failure at this field level
   * @param {Partial<DataModelValidationOptions>} options Handling options
   * @internal
   */
  static _handleValidationFailure(field, data, key, parentFailure, fieldFailure, options) {
    if ( !fieldFailure.unresolved ) return; // Fallback already applied
    if ( options.fallback ) {               // Allow fallback
      const initial = field.getInitialValue(options.model?._source ?? {});
      try {
        field.validate(initial, {...options, fallback: false, strict: true});
        data[key] = fieldFailure.fallbackValue = initial;
        fieldFailure.unresolved = false;    // Resolved via fallback
      } catch(err) {
        fieldFailure.unresolved = parentFailure.unresolved = true;
      }
    }
    else parentFailure.unresolved = fieldFailure.unresolved = true;
  }

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

  /** @override */
  _validateModel(changes, options={}) {
    options.source = options.source || changes;
    if ( !changes ) return;
    for ( const [name, field] of this.entries() ) {
      const change = changes[name];  // May be nullish
      if ( !change || !field.constructor.recursive ) continue;
      try {
        field._validateModel(change, options);
      } catch(err) {
        if ( err instanceof DataModelValidationFailure ) throw err;
        const failure = new DataModelValidationFailure(err.message, {
          fieldPath: field.fieldPath, unresolved: true, cause: err
        });
        failure.joint = err.message;
        throw failure;
      }
    }
  }

  /* -------------------------------------------- */
  /*  Field Transformations                       */
  /* -------------------------------------------- */

  /** @override */
  toObject(value) {
    if ( (value === undefined) || (value === null) ) return value;
    const data = {};
    for ( const [name, field] of this.entries() ) {
      data[name] = field.toObject(value[name]);
    }
    return data;
  }

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

  /** @override */
  apply(fn, data={}, options={}) {

    // Apply to this SchemaField
    const thisFn = typeof fn === "string" ? this[fn] : fn;
    thisFn?.call(this, data, options);
    if ( !data || (typeof data !== "object") ) return data; // Do not recurse for non-object types or null

    // Recursively apply to inner fields
    const results = {};
    for ( const [key, field] of this.entries() ) {
      if ( options.partial && !(key in data) ) continue;
      const r = field.apply(fn, data[key], options);
      if ( !options.filter || !isEmpty(r) ) results[key] = r;
    }
    return results;
  }

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

  /** @override */
  _applyChangeAdd(value, delta, model, change) {
    return value;
  }
}

/* -------------------------------------------- */
/*  Basic Field Types                           */
/* -------------------------------------------- */

/**
 * A subclass of {@link foundry.data.fields.DataField} which deals with boolean-typed data.
 */
class BooleanField extends DataField {

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {required: true, nullable: false, initial: false});
  }

  /** @override */
  _cast(value) {
    if ( typeof value === "string" ) return value === "true";
    if ( typeof value === "object" ) return false;
    return Boolean(value);
  }

  /** @override */
  _validateType(value, _options) {
    if (typeof value !== "boolean") throw new Error("must be a boolean");
  }

  /** @override */
  _toInput(config) {
    config.value ??= this.initial;
    return foundry.applications.fields.createCheckboxInput(config);
  }

  /* -------------------------------------------- */
  /*  Active Effect Integration                   */
  /* -------------------------------------------- */

  /** @override */
  _applyChangeAdd(value, delta, model, change) {
    return value || delta;
  }

  /** @override */
  _applyChangeSubtract(value, delta, model, change) {
    return value !== delta;
  }

  /** @override */
  _applyChangeMultiply(value, delta, model, change) {
    return value && delta;
  }

  /** @override */
  _applyChangeUpgrade(value, delta, model, change) {
    return delta > value ? delta : value;
  }

  /** @override */
  _applyChangeDowngrade(value, delta, model, change) {
    return delta < value ? delta : value;
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.DataField} which deals with number-typed data.
 *
 * @property {number} min                 A minimum allowed value
 * @property {number} max                 A maximum allowed value
 * @property {number} step                A permitted step size
 * @property {boolean} integer=false      Must the number be an integer?
 * @property {boolean} positive=false     Must the number be positive?
 * @property {number[]|object|Function} [choices] An array of values or an object of values/labels which represent
 *                                        allowed choices for the field. A function may be provided which dynamically
 *                                        returns the array of choices.
 */
class NumberField extends DataField {
  /**
   * @param {NumberFieldOptions} options  Options which configure the behavior of the field
   * @param {DataFieldContext} [context]  Additional context which describes the field
   */
  constructor(options={}, context={}) {
    super(options, context);
    // If choices are provided, the field should not be null by default
    if ( this.choices ) {
      this.nullable = options.nullable ?? false;
    }
    if ( this.integer && this.positive ) this.min ||= 1;
    if ( Number.isFinite(this.min) && Number.isFinite(this.max) && (this.min > this.max) ) {
      throw new Error("NumberField minimum constraint cannot exceed its maximum constraint");
    }
  }

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {
      nullable: true,
      min: undefined,
      max: undefined,
      step: undefined,
      integer: false,
      positive: false,
      choices: undefined
    });
  }

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

  /** @override */
  _cast(value) {
    return this.nullable && (value === "") ? null : Number(value);
  }

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

  /** @override */
  _cleanType(value, _options, _state) {
    if ( typeof value !== "number" ) return value;
    if ( this.integer ) value = Math.round(value);
    if ( Number.isFinite(this.step) ) {
      let base = 0;
      if ( Number.isFinite(this.min) ) value = Math.max(value, base = this.min);
      value = value.toNearest(this.step, "round", base);
      if ( Number.isFinite(this.max) ) value = Math.min(value, this.max.toNearest(this.step, "floor", base));
    } else {
      if ( Number.isFinite(this.min) ) value = Math.max(value, this.min);
      if ( Number.isFinite(this.max) ) value = Math.min(value, this.max);
    }
    return value;
  }

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

  /** @override */
  _validateType(value, _options) {
    if ( (typeof value !== "number") || Number.isNaN(value) ) throw new Error("must be a number");
    if ( this.positive && (value <= 0) ) throw new Error("must be a positive number");
    if ( Number.isFinite(this.min) && (value < this.min) ) throw new Error(`must be at least ${this.min}`);
    if ( Number.isFinite(this.max) && (value > this.max) ) throw new Error(`must be at most ${this.max}`);
    if ( Number.isFinite(this.step) && (value.toNearest(this.step, "round", Number.isFinite(this.min) ? this.min : 0) !== value) ) {
      if ( Number.isFinite(this.min) && (this.min !== 0) ) throw new Error(`must be an increment of ${this.step} after subtracting ${this.min}`);
      else throw new Error(`must be an increment of ${this.step}`);
    }
    if ( this.choices && !this.#isValidChoice(value) ) throw new Error(`${value} is not a valid choice`);
    if ( this.integer ) {
      if ( !Number.isInteger(value) ) throw new Error("must be an integer");
    }
    else if ( !Number.isFinite(value) ) throw new Error("must be a finite number");
  }

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

  /**
   * Test whether a provided value is a valid choice from the allowed choice set
   * @param {number} value      The provided value
   * @returns {boolean}         Is the choice valid?
   */
  #isValidChoice(value) {
    let choices = this.choices;
    if ( typeof choices === "function" ) choices = choices();
    if ( Array.isArray(choices) ) return choices.includes(value);
    return String(value) in choices;
  }

  /* -------------------------------------------- */
  /*  Form Field Integration                      */
  /* -------------------------------------------- */

  /** @override */
  _toInput(config) {
    config.min ??= this.min;
    config.max ??= this.max;
    config.step ??= this.step;
    if ( config.value === undefined ) config.value = this.getInitialValue({});
    if ( this.integer ) {
      if ( Number.isNumeric(config.value) ) config.value = Math.round(config.value);
      config.step ??= 1;
    }
    if ( this.positive && Number.isFinite(config.step) ) config.min ??= config.step;

    // Number Select
    config.choices ??= this.choices;
    StringField._prepareChoiceConfig(config);
    if ( config.options ) {
      config.dataset ||= {};
      config.dataset.dtype = "Number";
      return foundry.applications.fields.createSelectInput(config);
    }

    // Range Slider
    if ( ["min", "max", "step"].every(k => config[k] !== undefined) && (config.type !== "number") ) {
      return foundry.applications.elements.HTMLRangePickerElement.create(config);
    }

    // Number Input
    return foundry.applications.fields.createNumberInput(config);
  }

  /* -------------------------------------------- */
  /*  Active Effect Integration                   */
  /* -------------------------------------------- */

  /** @inheritDoc */
  _castChangeDelta(raw, replacementData) {
    if ( typeof raw === "string" ) {
      raw = foundry.dice.Roll.create(this._replaceDataRefs(raw, replacementData)).evaluateSync().total;
    }
    return super._castChangeDelta(raw);
  }

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

  /** @override */
  _applyChangeSubtract(value, delta, model, change) {
    return value - delta;
  }

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

  /** @override */
  _applyChangeMultiply(value, delta, model, change) {
    return value * delta;
  }

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

  /** @override */
  _applyChangeUpgrade(value, delta, model, change) {
    return delta > value ? delta : value;
  }

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

  /** @override */
  _applyChangeDowngrade(value, delta, model, change) {
    return delta < value ? delta : value;
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.DataField} which deals with string-typed data.
 */
class StringField extends DataField {
  /**
   * @param {StringFieldOptions} [options]  Options which configure the behavior of the field
   * @param {DataFieldContext} [context]    Additional context which describes the field
   */
  constructor(options={}, context={}) {
    super(options, context);

    // If choices are provided, the field should not be null or blank by default
    if ( this.choices ) {
      this.nullable = options.nullable ?? false;
      this.blank = options.blank ?? false;
    }
  }

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {blank: true, trim: true, choices: undefined, textSearch: false});
  }

  /**
   * Is the string allowed to be blank (empty)?
   * @type {boolean}
   */
  blank = this.blank;

  /**
   * Should any provided string be trimmed as part of cleaning?
   * @type {boolean}
   */
  trim = this.trim;

  /**
   * An array of values or an object of values/labels which represent
   * allowed choices for the field. A function may be provided which dynamically
   * returns the array of choices.
   * @type {string[]|object|Function}
   */
  choices = this.choices;

  /**
   * Is this string field a target for text search?
   * @type {boolean}
   */
  textSearch = this.textSearch;

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

  /** @inheritdoc */
  clean(value, options, _state) {
    if ( (typeof value === "string") && this.trim ) value = value.trim(); // Trim input strings
    return super.clean(value, options, _state);
  }

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

  /** @override */
  getInitialValue(source) {
    const initial = super.getInitialValue(source);
    if ( this.blank && this.required && !initial ) return "";  // Prefer blank to null for required fields
    return initial;
  }

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

  /** @override */
  _cast(value) {
    return String(value);
  }

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

  /** @inheritdoc */
  _validateSpecial(value) {
    if ( value === "" ) {
      if ( this.blank ) return true;
      else throw new Error("may not be a blank string");
    }
    return super._validateSpecial(value);
  }

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

  /** @override */
  _validateType(value, _options) {
    if ( typeof value !== "string" ) throw new Error("must be a string");
    else if ( this.choices ) {
      if ( this._isValidChoice(value) ) return true;
      else throw new Error(`${value} is not a valid choice`);
    }
  }

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

  /**
   * Test whether a provided value is a valid choice from the allowed choice set
   * @param {string} value      The provided value
   * @returns {boolean}         Is the choice valid?
   * @protected
   */
  _isValidChoice(value) {
    let choices = this.choices;
    if ( typeof choices === "function" ) choices = choices();
    if ( Array.isArray(choices) ) return choices.includes(value);
    return String(value) in choices;
  }

  /* -------------------------------------------- */
  /*  Form Field Integration                      */
  /* -------------------------------------------- */

  /**
   * Prepare form input configuration to accept a limited choice set of options.
   * @param {FormInputConfig & Partial<ChoiceInputConfig>} [config]
   * @internal
   */
  static _prepareChoiceConfig(config) {
    if ( config.options || !("choices" in config) ) return;
    let choices;
    try {
      choices = typeof config.choices === "function" ? config.choices() : config.choices;
    } catch(error) {
      logger.error(error);
    }

    // Prepare options array - only accept arrays or records
    if ( (typeof choices === "object") && (choices !== null) ) {
      config.options = [];
      for ( const [value, entry] of Object.entries(choices) ) {
        const choice = {value, ...StringField.#getChoiceFromEntry(entry, config)};
        config.options.push(choice);
      }
    }

    // Remove consumed options
    delete config.choices;
    delete config.valueAttr;
    delete config.labelAttr;
  }

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

  /**
   * Convert a choice entry into a standardized FormSelectOption
   * @param {string|object} entry
   * @param {{labelAttr?: string; valueAttr?: string}} options
   * @returns {FormSelectOption}
   */
  static #getChoiceFromEntry(entry, {labelAttr="label", valueAttr}) {
    const choice = {};
    if ( isPlainObject(entry) ) {
      if ( valueAttr && (valueAttr in entry) ) choice.value = entry[valueAttr];
      if ( labelAttr && (labelAttr in entry) ) choice.label = entry[labelAttr];
      for ( const k of ["group", "disabled", "rule"] ) {
        if ( k in entry ) choice[k] = entry[k];
      }
    }
    else choice.label = String(entry);
    return choice;
  }

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

  /**
   * @param {FormInputConfig & StringFieldInputConfig} config
   * @override
   */
  _toInput(config) {
    if ( config.value === undefined ) config.value = this.getInitialValue({});
    config.choices ??= this.choices;

    // Choice Select
    StringField._prepareChoiceConfig(config);
    if ( config.options ) {
      if ( this.blank || this.nullable || !this.required ) config.blank ??= "";
      return foundry.applications.fields.createSelectInput(config);
    }

    // One of several options for element type
    switch ( config.elementType ?? "input" ) {
      case "input":
        return foundry.applications.fields.createTextInput(config);
      case "textarea":
        return foundry.applications.fields.createTextareaInput(config);
      case "file-picker":
        return foundry.applications.elements.HTMLFilePickerElement.create(config);
      case "prose-mirror":
        return foundry.applications.elements.HTMLProseMirrorElement.create(config);
      case "code-mirror":
        return foundry.applications.elements.HTMLCodeMirrorElement.create(config);
      default:
        throw new Error(`Unrecognized element type for StringField input: ${config.elementType}`);
    }
  }

  /* -------------------------------------------- */
  /*  Active Effect Integration                   */
  /* -------------------------------------------- */

  /** @inheritDoc */
  _replaceDataRefs(raw, data, {strict=true}={}) {
    const resolved = super._replaceDataRefs(raw, data, {strict: false});
    if ( strict && /@\{[^}\s]+\}/.test(resolved) ) {
      throw new Error(`Unable to replace all data references in ${JSON.stringify(raw)}.`);
    }
    return resolved;
  }

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

  /**
   * @param {string} value
   * @param {string} delta
   * @override
   */
  _applyChangeSubtract(value, delta, model, change) {
    return value.replace(delta, "");
  }
}

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

/**
 * A subclass of DataField which deals with object-typed data.
 */
class ObjectField extends DataField {
  /** @inheritDoc */
  static get _defaults() {
    return Object.assign(super._defaults, {required: true, nullable: false});
  }

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

  /** @override */
  getInitialValue(source) {
    const initial = super.getInitialValue(source);
    if ( this.required && (initial === undefined) ) return {};
    return initial;
  }

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

  /** @override */
  _cast(value) {
    if ( typeof value.toObject === "function" ) value = value.toObject();
    else if ( typeof value.toJSON === "function" ) value = value.toJSON();
    return isPlainObject(value) ? value : {};
  }

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

  /** @override */
  _cleanType(data, options, _state) {
    return this.#reconstructOperators(data, options, _state);
  }

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

  /**
   * Deeply reconstruct DataFieldOperators within an ObjectField data structure.
   * @param {object}                   data
   * @param {DataModelCleaningOptions} options
   * @param {DataModelUpdateState}     _state
   * @returns {object}
   */
  #reconstructOperators(data, options, _state) {
    SchemaField.expandObject(data, options, _state);
    for ( const k in data ) {
      SchemaField.reconstructOperator(data, k, data[k]);
      let v = data[k];
      if ( v && (typeof v === "object") ) {
        if ( !Array.isArray(v) && !(v instanceof DataFieldOperator) ) v = data[k] = this._cast(v);
        this.#reconstructOperators(v, options, _state);
      }
    }
    return data;
  }

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

  /** @override */
  initialize(value, model, options={}) {
    if ( !value ) return value;
    return deepClone(value);
  }

  /* ---------------------------------------- */
  /*  Validation and Updates                  */
  /* ---------------------------------------- */

  /** @override */
  _validateType(value, _options) {
    if ( !isPlainObject(value) ) throw new Error("must be an object");
  }

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

  /** @override */
  _updateDiff(key, value, options, state) {

    // * -> null or special data field operators
    if ( !value || (value instanceof DataFieldOperator) ) return super._updateDiff(key, value, options, state);

    // Pre-validation of the requested value
    let failure = this.validate(value, {strict: false, phase: "pre", partial: true});
    if ( failure?.unresolved ) {
      state.failure.fields[key] = failure;
      state.failure.unresolved = true;
      return; // Prevent assignment
    }

    // * -> {}
    if ( !isPlainObject(state.source[key]) ) state.source[key] = {};
    const diff = diffObject(state.source[key], value);
    if ( isEmpty(diff) ) return;
    const result = mergeObject(state.source[key], diff, {applyOperators: true, inplace: false});

    // Post-validation of the resulting value
    failure ||= this.validate(result, {strict: false, phase: "post", partial: true});
    if ( failure && !failure?.empty && failure.unresolved ) {
      state.failure.fields[key] = failure;
      state.failure.unresolved = true;
      return; // Prevent assignment
    }

    // Assign results to diff and source
    state.diff[key] = diff;
    state.source[key] = result;
  }

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

  /** @inheritDoc */
  _updateCommit(source, key, value, diff, options) {
    const s = source[key];

    // Special Cases: * -> undefined, * -> null, undefined -> *, null -> *
    if ( !s || !value || Object.isSealed(s) ) {
      source[key] = value;
      return;
    }

    // Replace object with new values
    for ( const k of iterateKeys(s) ) {
      if ( !(k in value) ) delete s[k];
    }
    Object.assign(s, value);
  }

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

  /** @override */
  toObject(value) {
    return deepClone(value);
  }

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

  /** @override */
  _applyChangeAdd(value, delta, model, change) {
    return value;
  }
}

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

/**
 * A subclass of ObjectField that represents a mapping of keys to the provided DataField type.
 */
class TypedObjectField extends ObjectField {
  /**
   * @param {DataField} element                  The value type of each entry in this object.
   * @param {TypedObjectFieldOptions} [options]  Options which configure the behavior of the field.
   * @param {DataFieldContext} [context]         Additional context which describes the field
   */
  constructor(element, options, context) {
    super(options, context);
    if ( !(element instanceof DataField) ) throw new Error("The element must be a DataField");
    if ( element.parent !== undefined ) throw new Error("The element DataField already has a parent");
    element.name ||= "element";
    element.parent = this;
    this.element = element;
  }

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

  /**
   * The value type of each entry in this object.
   * @type {DataField}
   */
  element;

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

  /** @override */
  static recursive = true;

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

  /** @inheritDoc */
  static get _defaults() {
    return mergeObject(super._defaults, {validateKey: undefined, expandKeys: true});
  }

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

  /** @override */
  initialize(value, model, options={}) {
    if ( !value ) return value;
    const object = {};
    for ( const key in value ) object[key] = this.element.initialize(value[key], model, options);
    return object;
  }

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

  /** @override */
  _getField(parts, {source}={}) {
    if ( !parts.length ) return this;
    const name = parts.pop();
    if ( source === undefined ) {
      if ( name !== this.element.name ) return undefined;
    } else {
      if ( this.validateKey?.(name) === false ) return undefined;
      if ( source !== null ) {
        if ( typeof source !== "object" ) return undefined;
        source = source[name] ?? null;
      }
    }
    return this.element._getField(parts, {source});
  }

  /* -------------------------------------------- */
  /*  Field Cleaning                              */
  /* -------------------------------------------- */

  /** @override */
  _cleanType(data, options, _state) {
    if ( this.expandKeys ) SchemaField.expandObject(data, options, _state);
    const clearObject = options.persisted && !this.element.persisted;
    for ( let key in data ) {
      if ( clearObject ) {
        delete data[key];
        continue;
      }

      // Backwards compatibility for converting legacy "special keys" into DataFieldOperator instances.
      /** @deprecated since v14 until v16 */
      if ( foundry.utils.isDeletionKey(key) ) {
        const k2 = key.slice(2);
        if ( key[0] === "-" ) {
          foundry.utils.logCompatibilityWarning(`The {${key}: null} syntax for forced deletion of a data field value is`
            + ` no longer supported in favor of {${k2}: new foundry.data.operators.ForcedDeletion()}`,
          {since: 14, until: 16});
          if ( data[key] !== null ) throw new Error(`Removing the value of a data field using the ${key} deletion`
            + ` syntax requires the value of that deletion key to be null. For example {${key}: null}`);
          data[k2] = new ForcedDeletion();
        } else {
          foundry.utils.logCompatibilityWarning(`The {${key}: value} syntax for forced replacement of a data field`
            + ` value is no longer supported in favor of {${k2}: new foundry.data.operators.ForcedReplacement(value)}`,
          {since: 14, until: 16});
          data[k2] = ForcedReplacement.create(data[key]);
        }
        delete data[key];
        key = k2;
      }

      const _innerState = {..._state, source: _state.source?.[key]};
      SchemaField.reconstructOperator(data, key, data[key]);
      const v = data[key];

      // Eliminate any keys which are invalid
      let valid;
      try {
        valid = this.validateKey?.(key);
      } catch {
        valid = false;
      }
      if ( valid === false ) {
        delete data[key];
        continue;
      }

      // Clean ForcedReplacement values
      if ( v instanceof DataFieldOperator ) {
        if ( v instanceof ForcedReplacement ) {
          const cv = DataFieldOperator.get(v);
          DataFieldOperator.set(v, this.element.clean(cv, Object.freeze({...options, partial: false}), _innerState));
        }
      }

      // Clean standard keys
      else data[key] = this.element.clean(v, options, _innerState);
    }
    return data;
  }

  /* -------------------------------------------- */
  /*  Field Validation                            */
  /* -------------------------------------------- */

  /** @override */
  _validateRecursive(data, options={}) {
    options.source = options.source || data;
    const objectFailure = new DataModelValidationFailure("TypedObjectField#_validateRecursive",
      {fieldPath: this.fieldPath, unresolved: false});
    const validationOptions = {...options, strict: false};
    for ( const [key, value] of iterateEntries(data) ) {
      if ( value instanceof ForcedDeletion ) continue; // Allow removing keys
      const failure = this.element.validate(value, validationOptions);
      if ( failure ) {
        objectFailure.fields[key] = failure;
        SchemaField._handleValidationFailure(this.element, data, key, objectFailure, failure, options);
      }
    }
    if ( !objectFailure.empty ) throw objectFailure; // Throw even if resolved
  }

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

  /** @override */
  _validateModel(changes, options={}) {
    options.source = options.source || changes;
    if ( !changes ) return;
    for ( const key in changes ) {
      const change = changes[key];  // May be nullish
      if ( change && this.element.constructor.recursive ) this.element._validateModel(change, options);
    }
  }

  /* -------------------------------------------- */
  /*  Field Updates                               */
  /* -------------------------------------------- */

  /** @override */
  _updateDiff(key, value, options, state) {

    // * -> null or DataFieldOperator
    if ( !value || (value instanceof DataFieldOperator) ) return super._updateDiff(key, value, options, state);

    // Pre-validation of the requested value
    let failure = this.validate(value, {strict: false, phase: "pre", partial: true});
    if ( failure?.unresolved ) {
      state.failure.fields[key] = failure;
      state.failure.unresolved = true;
      return; // Prevent assignment
    }

    // * -> {}
    const source = (state.source[key] ||= {});
    const diff = {};
    failure ||= new DataModelValidationFailure("TypedObjectField#_updateDiff", {fieldPath: this.fieldPath});
    const fieldState = {...state, model: state.model, source, diff, failure};
    for ( const [k, v] of iterateEntries(value) ) {
      if ( v instanceof ForcedDeletion ) { // Keep the deletion in the diff, but remove it from source
        diff[k] = v;
        delete source[k];
        continue;
      }
      this.element._updateDiff(k, v, options, fieldState);
    }
    if ( isEmpty(diff) ) return; // No changes

    // Post-validation of the resulting value
    failure ||= this.validate(source, {strict: false, phase: "post", partial: true});
    if ( failure && !failure?.empty && failure.unresolved ) {
      state.failure.fields[key] = failure;
      state.failure.unresolved = true;
      return; // Prevent assignment
    }

    // Assign results to diff and source
    state.diff[key] = diff;
    state.source[key] = source;
  }

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

  /** @override */
  _updateCommit(source, key, value, diff, options) {
    const s = source[key];

    // * -> undefined, * -> null, undefined -> *, null -> *
    if ( !s || !value || Object.isSealed(s) ) {
      source[key] = value;
      return;
    }

    // Remove keys which no longer exist in the new value
    for ( const k of iterateKeys(s) ) {
      if ( !(k in value) ) delete s[k];
    }

    // Update fields in source which changed in the diff
    for ( const [k, d] of iterateEntries(diff) ) {
      if ( d instanceof ForcedDeletion ) continue;
      this.element._updateCommit(s, k, value[k], d, options);
    }
  }

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

  /** @override */
  toObject(value) {
    if ( (value === undefined) || (value === null) ) return value;
    const object = {};
    for ( const key in value ) object[key] = this.element.toObject(value[key]);
    return object;
  }

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

  /** @override */
  apply(fn, data={}, options={}) {

    // Apply to this TypedObjectField
    const thisFn = typeof fn === "string" ? this[fn] : fn;
    thisFn?.call(this, data, options);

    // Recursively apply to inner fields
    const results = {};
    for ( const key in data ) {
      const r = this.element.apply(fn, data[key], options);
      if ( !options.filter || !isEmpty(r) ) results[key] = r;
    }
    return results;
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.DataField} which deals with array-typed data.
 * @template [ElementType=DataField]
 * @property {number} min     The minimum number of elements.
 * @property {number} max     The maximum number of elements.
 */
class ArrayField extends DataField {
  /**
   * @param {ElementType} element          The type of element contained in the Array
   * @param {ArrayFieldOptions} [options]  Options which configure the behavior of the field
   * @param {DataFieldContext} [context]   Additional context which describes the field
   */
  constructor(element, options={}, context={}) {
    super(options, context);
    this.element = this.constructor._validateElementType(element);
    if ( this.element instanceof DataField ) {
      this.element.name ||= "element";
      this.element.parent = this;
    }
    if ( this.min > this.max ) throw new Error("ArrayField minimum length cannot exceed maximum length");
  }

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {
      required: true,
      nullable: false,
      empty: true,
      exact: undefined,
      min: 0,
      max: Infinity
    });
  }

  /** @override */
  static recursive = true;

  /**
   * The data type of each element in this array
   * @type {ElementType}
   */
  element;

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

  /**
   * Validate the contained element type of the ArrayField
   * @param {*} element        The type of Array element
   * @returns {ElementType}    The validated element type
   * @throws                   An error if the element is not a valid type
   * @protected
   */
  static _validateElementType(element) {
    if ( !(element instanceof DataField) ) {
      throw new Error(`${this.name} must have a DataField as its contained element`);
    }
    if ( element.parent !== undefined ) throw new Error("The element DataField already has a parent");
    return element;
  }

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

  /** @override */
  getInitialValue(source) {
    const initial = super.getInitialValue(source);
    if ( this.required && (initial === undefined) ) return [];
    return initial;
  }

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

  /** @override */
  initialize(value, model, options={}) {
    if ( !value ) return value;
    return value.map(v => this.element.initialize(v, model, options));
  }

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

  /** @override */
  _getField(parts, {source}={}) {
    if ( !parts.length ) return this;
    const name = parts.pop();
    if ( source === undefined ) {
      if ( name !== this.element.name ) return undefined;
    } else if ( source !== null ) {
      if ( !Array.isArray(source) ) return undefined;
      const index = Number(name);
      if ( !Number.isInteger(index) || (index < 0) ) return undefined;
      source = source[index] ?? null;
    }
    return this.element._getField(parts, {source});
  }

  /* ---------------------------------------- */
  /*  Field Cleaning                          */
  /* ---------------------------------------- */

  /** @override */
  _cast(value) {
    if ( Array.isArray(value) ) return value;
    if ( isPlainObject(value) ) {
      const arr = [];
      for ( const [k, v] of Object.entries(value) ) {
        const i = Number(k);
        if ( Number.isInteger(i) && (i >= 0) ) arr[i] = v;
      }
      return arr;
    }
    if ( value instanceof Set ) return Array.from(value);
    return [value];
  }

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

  /**
   * When cleaning an ArrayField we must force {partial: false} since arrays are always fully replaced.
   * We take care to preserve the provided array reference, cleaning its elements rather than replacing the array.
   * @override
   */
  _cleanType(value, options, _state) {
    if ( options.persisted && !this.element.persisted ) {
      value.length = 0;
      return value;
    }

    // Constrain allowed array length
    if ( value.length > this.max ) value.length = this.max;

    // Clean every array element, propagating the inner model when elements are embedded DataModels.
    const isInnerModel = this.element instanceof EmbeddedDataField;
    for ( let i = 0; i < value.length; i++ ) {
      const _innerState = {..._state, source: _state.source?.[i]};
      if ( isInnerModel ) {
        _innerState.model = _state.model?._getInnerModel?.(this, { value: value[i], index: i }, options) ?? null;
      }
      value[i] = this._cleanElement(value[i], options, _innerState);
    }
    return value;
  }

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

  /**
   * Clean data for an individual element in the ArrayField.
   * @param {object} value                      Unclean data for the array entry
   * @param {DataModelCleaningOptions} options  Options which control how data is cleaned
   * @param {DataModelUpdateState} _state       State used during data cleaning
   * @returns {object}                          Cleaned data for the array entry
   * @protected
   */
  _cleanElement(value, options, _state) {
    return this.element.clean(value, Object.freeze({...options, partial: false}), _state);
  }

  /* ---------------------------------------- */
  /*  Field Validation                        */
  /* ---------------------------------------- */

  /** @override */
  _validateType(value, options) {
    if ( !Array.isArray(value) ) throw new Error("must be an Array");
    if ( value.length < this.min ) throw new Error(`cannot have fewer than ${this.min} elements`);
    if ( value.length > this.max ) throw new Error(`cannot have more than ${this.max} elements`);
  }

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

  /** @override */
  _validateRecursive(value, options) {
    const arrayFailure = new DataModelValidationFailure("ArrayField#_validateRecursive",
      {fieldPath: this.fieldPath, unresolved: false});
    const validationOptions = {...options, partial: false, strict: false}; // Arrays are always fully replaced
    validationOptions.dropInvalidElements ??= false; // Array fields cannot drop elements unless explicitly allowed
    for ( let i=value.length-1; i>=0; i-- ) { // Iterate backwards so we can splice as we go
      const v = value[i];
      const failure = this.element.validate(v, validationOptions);
      if ( failure ) {
        arrayFailure.elements.push({id: i, failure});
        ArrayField._handleValidationFailure(this.element, value, i, arrayFailure, failure, validationOptions);
      }
    }

    // Throw for any failure (even if resolved)
    arrayFailure.unresolved = arrayFailure.elements.some(e => e.failure.unresolved);
    if ( !arrayFailure.empty ) {
      arrayFailure.elements.reverse();
      throw arrayFailure;
    }
  }

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

  /** @override */
  _validateModel(changes, options) {
    if ( !this.element.constructor.recursive ) return;
    for ( const element of changes ) {
      this.element._validateModel(element, options);
    }
  }

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

  /**
   * @see {@link SchemaField._handleValidationFailure}
   * @internal
   */
  static _handleValidationFailure(field, value, index, parentFailure, fieldFailure, options) {
    SchemaField._handleValidationFailure(field, value, index, parentFailure, fieldFailure, options);
    // Optionally allow dropping invalid elements from Array-like structures
    if ( fieldFailure.unresolved && options.dropInvalidElements ) {
      value.splice(index, 1);
      fieldFailure.dropped = true;
      fieldFailure.unresolved = false;
    }
  }

  /* ---------------------------------------- */
  /*  Field Updates                           */
  /* ---------------------------------------- */

  /** @override */
  _updateDiff(key, value, options, state) {

    // * -> null
    const result = DataFieldOperator.get(value);
    if ( !result ) return super._updateDiff(key, value, options, state);

    // * -> []
    const current = state.source[key];
    let isDiff = result.length !== current?.length;
    const update = [];
    for ( const [i, v] of result.entries() ) {
      const v1 = applyDataOperators(v);
      const v0 = current?.[i];
      update.push(v1);
      isDiff ||= !foundry.utils.equals(v1, v0);
    }
    if ( !isDiff ) return;

    // Validate the field recursively because arrays are fully replaced leaf nodes
    const failure = this.validate(update, {recursive: true, fallback: options.fallback, strict: false});
    if ( failure ) {
      state.failure.fields[key] = failure;
      if ( failure?.unresolved ) return; // Prevent assignment
    }

    // Assign results of the update
    state.diff[key] = value;    // Retain array element operators in the difference
    state.source[key] = update; // Source copy includes applied operators
  }

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

  /**
   * Commit array field changes by replacing array contents while preserving the array reference itself.
   * @override
   */
  _updateCommit(source, key, value, _diff, _options) {
    const s = source[key];
    // * -> undefined or * -> null
    if ( !s || !value ) {
      source[key] = value;
      return;
    }
    // * -> []
    s.length = 0;
    s.push(...value);
  }

  /* -------------------------------------------- */
  /*  Field Transformations                       */
  /* -------------------------------------------- */

  /** @override */
  toObject(value) {
    if ( !value ) return value;
    return value.map(v => this.element.toObject(v));
  }

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

  /** @override */
  apply(fn, value=[], options={}) {

    // Apply to this ArrayField
    const thisFn = typeof fn === "string" ? this[fn] : fn;
    thisFn?.call(this, value, options);
    if ( !Array.isArray(value) ) return value; // Do not recurse for non-array types

    // Recursively apply to array elements
    const results = [];
    if ( !value.length && options.initializeArrays ) value = [undefined];
    for ( const v of value ) {
      const r = this.element.apply(fn, v, options);
      if ( !options.filter || !isEmpty(r) ) results.push(r);
    }
    return results;
  }

  /* -------------------------------------------- */
  /*  Active Effect Integration                   */
  /* -------------------------------------------- */

  /** @override */
  _castChangeDelta(raw, replacementData) {
    let delta;
    try {
      delta = JSON.parse(raw);
      delta = Array.isArray(delta) ? delta : [delta];
    } catch(_err) {
      delta = [raw];
    }
    return delta.map(value => this.element._castChangeDelta(value, replacementData));
  }

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

  /** @override */
  _applyChangeAdd(value, delta, model, change) {
    return value.concat(delta);
  }

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

  /**
   * @param {unknown[]} value
   * @param {unknown[]} delta
   * @override
   */
  _applyChangeSubtract(value, delta, model, change) {
    const changed = [...value];
    for ( const element of delta ) {
      changed.findSplice(e => equals(e, element));
    }
    return changed;
  }
}

/* -------------------------------------------- */
/*  Specialized Field Types                     */
/* -------------------------------------------- */

/**
 * A subclass of {@link foundry.data.fields.ArrayField} which supports a set of contained elements.
 * Elements in this set are treated as fungible and may be represented in any order or discarded if invalid.
 */
class SetField extends ArrayField {

  /** @override */
  initialize(value, model, options={}) {
    if ( !value ) return value;
    return new Set(super.initialize(value, model, options));
  }

  /* -------------------------------------------- */
  /*  Field Validation                            */
  /* -------------------------------------------- */

  /** @override */
  _validateRecursive(value, options={}) {
    const dropInvalidElements = options.dropInvalidElements ?? options.fallback;
    super._validateRecursive(value, {...options, dropInvalidElements});
  }

  /* -------------------------------------------- */
  /*  Form Field Integration                      */
  /* -------------------------------------------- */

  /** @override */
  _toInput(config) {
    const element = this.element;

    // Document UUIDs
    if ( element instanceof DocumentUUIDField ) {
      Object.assign(config, {type: element.type, single: false});
      if ( element.relative && config.model && config.value ) {
        config.value = config.value.map(uuid => parseUuid(uuid, {relative: config.model})?.uuid ?? uuid);
      }
      return foundry.applications.elements.HTMLDocumentTagsElement.create(config);
    }

    // Multi-Select Input
    if ( element.choices && !config.options ) {
      config.choices ??= element.choices;
      StringField._prepareChoiceConfig(config);
    }
    if ( config.options ) {
      if ( element instanceof NumberField ) mergeObject(config, {dataset: {dtype: "Number"}});
      return foundry.applications.fields.createMultiSelectInput(config);
    }

    // Arbitrary String Tags
    if ( element instanceof StringField ) return foundry.applications.elements.HTMLStringTagsElement.create(config);
    throw new Error(`SetField#toInput is not supported for a ${element.constructor.name} element type`);
  }

  /* -------------------------------------------- */
  /*  Field Transformations                       */
  /* -------------------------------------------- */

  /** @override */
  toObject(value) {
    if ( !value ) return value;
    return Array.from(value).map(v => this.element.toObject(v));
  }

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

  /** @inheritDoc */
  _castChangeDelta(raw, replacementData) {
    return new Set(super._castChangeDelta(raw, replacementData));
  }

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

  /** @override */
  _applyChangeAdd(value, delta, model, change) {
    return value.union(delta);
  }

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

  /** @override */
  _applyChangeSubtract(value, delta, model, change) {
    return value.difference(delta);
  }

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

  /** @override */
  _applyChangeUpgrade(value, delta, model, change) {
    return delta.isSupersetOf(value) ? delta : value;
  }

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

  /** @override */
  _applyChangeDowngrade(value, delta, model, change) {
    return delta.isSubsetOf(value) ? delta : value;
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.SchemaField} that represents the root schema node of a DataModel.
 */
class DataModelSchemaField extends SchemaField {
  /**
   * @param {typeof DataModel} model          The class of DataModel which should be embedded in this field
   * @param {DataFieldOptions} [options]      Options which configure the behavior of the field
   * @param {DataFieldContext} [context]      Additional context which describes the field
   */
  constructor(model, options={}, context={}) {
    if ( !isSubclass(model, foundry.abstract.DataModel) ) {
      throw new Error("An EmbeddedDataField must specify a DataModel class as its type");
    }

    // Create an independent copy of the model schema
    const fields = Object.freeze(model.defineSchema());
    super(fields, options, context);

    /**
     * The base DataModel definition which is contained in this field.
     * @type {typeof DataModel}
     */
    this.model = model;
  }

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

  /** @override */
  _getField(parts, {source}={}) {
    if ( !parts.length ) return this;
    const name = parts.pop();
    const field = this.get(name);
    if ( !field ) return undefined;
    let type;
    if ( (source !== undefined) && (source !== null) ) {
      if ( typeof source !== "object" ) return undefined;
      if ( this.fields.type instanceof DocumentTypeField ) type = source.type;
      source = source[name] ?? null;
    }
    return field._getField(parts, {source, type});
  }

  /* ---------------------------------------- */
  /*  Field Cleaning                          */
  /* ---------------------------------------- */

  /** @inheritDoc */
  clean(value, options={}, _state={}) {
    const modelCls = this.model.implementation ?? this.model;
    options = {...options};

    // Recreate and reset state to be based on this model node
    _state = {..._state};
    delete _state.documentType; // To be recorded during _preCleanData
    delete _state.documentId;

    if ( isPlainObject(value) ) SchemaField.expandObject(value, options, _state);

    modelCls._preCleanData(value, options, _state); // DataModel._preCleanData workflow
    Object.freeze(options); // Freeze after pre-cleaning which may alter cleaning options

    // SchemaField#clean workflow
    value = super.clean(value, options, _state);

    // DataModel._cleanData workflow
    if ( options.model ) modelCls._cleanData(value, options, _state);
    return value;
  }

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

  /** @override */
  _cast(value) {
    if ( typeof value.toObject === "function" ) value = value.toObject();
    return isPlainObject(value) ? value : {};
  }

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

  /** @override */
  _migrate(value, options, _state) {
    if ( isPlainObject(value) ) (this.model.implementation ?? this.model).migrateDataSafe(value, options);
    return value;
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.DataModelSchemaField} used for embedded data models.
 */
class EmbeddedDataField extends DataModelSchemaField {

  /** @override */
  initialize(value, model, options={}) {
    if ( !value ) return value;
    // FIXME avoid reconstruction unless necessary #12001
    return new this.model(value, {parent: model, schema: this, ...options, clean: false});
  }

  /* ---------------------------------------- */
  /*  Field Validation                        */
  /* ---------------------------------------- */

  /** @override */
  _validateModel(changes, options) {
    this.model.validateJoint(changes);
  }

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

  /** @inheritDoc */
  _updateDiff(key, value, options, state) {
    let result = value;
    if ( value instanceof DataFieldOperator ) result = applyDataOperators(value);
    if ( !result ) return super._updateDiff(key, value, options, state);
    const priorSource = (state.source[key] || {});

    // Pre-validation of the requested value
    let failure = this.validate(result, {strict: false, phase: "pre", partial: true});
    if ( failure?.unresolved ) {
      state.failure.fields[key] = failure;
      state.failure.unresolved = true;
      return;
    }

    // Update a pre-existing model
    let model;
    if ( !isEmpty(priorSource) ) {
      model = this.model.fromSource(priorSource, {strict: false, clean: false});
      try {
        const modelDiff = model.updateSource(value, {clean: false});
        if ( isEmpty(modelDiff) ) return; // No changes
      } catch(err) {
        if ( err instanceof DataModelValidationError ) failure = err.getFailure();
        else return; // Unhandled error
      }
    }

    // Create a new model
    else {
      try {
        model = new this.model(result);
      } catch(err) {
        if ( err instanceof DataModelValidationError ) failure = err.getFailure();
        else return; // Unhandled error
      }
    }

    // Post-validation of the resulting value
    failure ||= this.validate(model._source, {strict: false, phase: "post", partial: true});
    if ( failure && !failure?.empty && failure.unresolved ) {
      state.failure.fields[key] = failure;
      state.failure.unresolved = true;
      return;
    }

    // Assign results to both diff and source
    state.diff[key] = value;
    state.source[key] = model._source;
  }

  /* ---------------------------------------- */
  /*  Field Transformation                    */
  /* ---------------------------------------- */

  /** @override */
  toObject(value) {
    if ( !value ) return value;
    return value.toObject(false);
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.ArrayField} which supports an embedded Document collection.
 * Invalid elements will be dropped from the collection during validation rather than failing for the field entirely.
 * @extends {ArrayField<typeof Document>}
 */
class EmbeddedCollectionField extends ArrayField {
  /**
   * @param {typeof Document} element     The type of Document which belongs to this embedded collection
   * @param {DataFieldOptions} [options]  Options which configure the behavior of the field
   * @param {DataFieldContext} [context]  Additional context which describes the field
   */
  constructor(element, options={}, context={}) {
    super(element, options, context);
    this.readonly = true; // Embedded collections are always immutable
  }

  /** @override */
  static _validateElementType(element) {
    if ( isSubclass(element, foundry.abstract.Document) ) return element;
    throw new Error("An EmbeddedCollectionField must specify a Document subclass as its type");
  }

  /**
   * The Collection implementation to use when initializing the collection.
   * @type {typeof EmbeddedCollection}
   */
  static get implementation() {
    return EmbeddedCollection;
  }

  /** @override */
  static hierarchical = true;

  /**
   * A reference to the DataModel subclass of the embedded document element
   * @type {typeof Document}
   */
  get model() {
    return this.element.implementation;
  }

  /**
   * The DataSchema of the contained Document model.
   * @type {SchemaField}
   */
  get schema() {
    return this.model.schema;
  }

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

  /** @override */
  initialize(value, model, options={}) {
    const collection = model.collections[this.name];
    collection.initialize(options);
    return collection;
  }

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

  /**
   * Return the embedded document(s) as a Collection.
   * @param {Document} parent  The parent document.
   * @returns {DocumentCollection}
   */
  getCollection(parent) {
    return parent[this.name];
  }

  /* -------------------------------------------- */
  /*  Field Cleaning                              */
  /* -------------------------------------------- */

  /** @inheritDoc */
  _cast(value) {
    if ( !(value instanceof Map) ) return super._cast(value);
    const arr = [];
    for ( const [id, v] of value.entries() ) {
      if ( !("_id" in v) ) v._id = id;
      arr.push(v);
    }
    return super._cast(arr);
  }

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

  /** @override */
  _cleanType(value, options, _state) {
    const sourceIdMap = {};
    if ( _state.source ) {
      for ( const record of _state.source ) sourceIdMap[record._id] = record;
    }
    const collection = _state.model?.[this.name] instanceof EmbeddedCollection ? _state.model[this.name] : null;
    for ( let i = 0; i < value.length; i++ ) {
      const v = value[i];
      const source = sourceIdMap[v._id];
      const model = collection?.get(v._id) ?? null;

      // Propagate inner state. Values without matching source are being created as part of a parent update.
      const _innerState = {..._state, source, modelSource: source, model, creation: _state.creation || !source};
      if ( this.model.hasTypeData ) {
        let documentType = DataFieldOperator.get(v.type) ?? source?.type;
        if ( !documentType && this.model.metadata.baseTypeAllowed ) documentType = BASE_DOCUMENT_TYPE;
        _innerState.documentId = source?._id;
        _innerState.documentType = documentType;
      }
      value[i] = this._cleanElement(v, options, _innerState);
    }
    return value;
  }

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

  /** @override */
  _cleanElement(value, options, _state) {
    if ( !options.partial ) value._id ||= randomID(16);
    return this.model.cleanData(value, {...options, copy: false}, _state);
  }

  /* -------------------------------------------- */
  /*  Field Validation                            */
  /* -------------------------------------------- */

  /** @override */
  _validateRecursive(value, options) {
    const collectionFailure = new DataModelValidationFailure("EmbeddedCollectionField#_validateRecursive",
      {fieldPath: this.fieldPath, unresolved: false});
    const collection = options.model?.[this.fieldPath];
    for ( let i=value.length-1; i>=0; i-- ) { // Iterate backwards so we can splice as we go
      const v = value[i];
      const m = collection?.get(v._id);
      const validationOptions = {...options, partial: false, model: m, strict: false};
      const failure = this.schema.validate(v, validationOptions);
      if ( failure ) {
        collectionFailure.elements.push({id: i, failure});
        EmbeddedCollectionField._handleValidationFailure(this.schema, value, i, collectionFailure, failure, {
          model: options.model,
          fallback: false, // Embedded documents cannot fallback
          dropInvalidEmbedded: !!options.dropInvalidEmbedded
        });
      }
    }

    // Throw for any failure (even if resolved)
    collectionFailure.unresolved = collectionFailure.elements.some(e => e.failure.unresolved);
    if ( !collectionFailure.empty ) {
      collectionFailure.elements.reverse();
      throw collectionFailure;
    }
  }

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

  /**
   * @see {@link SchemaField._handleValidationFailure}
   * @internal
   */
  static _handleValidationFailure(field, value, index, parentFailure, fieldFailure, options) {
    SchemaField._handleValidationFailure(field, value, index, parentFailure, fieldFailure, options);
    if ( fieldFailure.unresolved && options.dropInvalidEmbedded ) {
      // Do not drop elements from the source so that they can be included in the collection's invalid documents set
      // and later retrieved via getInvalid.
      fieldFailure.dropped = true;
      fieldFailure.unresolved = false;
    }
  }

  /* -------------------------------------------- */
  /*  Field Updates                               */
  /* -------------------------------------------- */

  /** @override */
  _updateDiff(key, value, options, state) {

    // * -> null
    const result = DataFieldOperator.get(value);
    if ( !result ) return super._updateDiff(key, value, options, state);

    // Pre-validation of the requested value
    let failure = this.validate(result, {strict: false, phase: "pre", partial: true});
    if ( failure?.unresolved ) {
      state.failure.fields[key] = failure;
      state.failure.unresolved = true;
      return; // Prevent assignment
    }

    // Otherwise create or diff individual array members
    const isReplacement = value instanceof ForcedReplacement;
    const source = (state.source[key] ||= []);
    const newSource = isReplacement ? [] : source;
    const sourceMap = {};
    for ( const obj of source.values() ) sourceMap[obj._id] = obj;
    const diff = [];
    failure ||= new DataModelValidationFailure("EmbeddedCollectionField#_updateDiff", {fieldPath: this.fieldPath});
    const collection = this.getCollection(state.model);

    // Recursive update and validation of embedded Documents
    const ctx = {source, newSource, isReplacement, diff, collection, state, failure};
    for ( const v of result ) {
      const existingSource = sourceMap[v._id];
      this._updateElement(v, existingSource, ctx);
    }
    if ( !diff.length && !isReplacement ) return; // No changes

    // Post-validation of the resulting value
    failure ||= this.validate(result, {strict: false, phase: "post", partial: true});
    if ( failure && !failure?.empty && failure.unresolved ) {
      state.failure.fields[key] = failure;
      state.failure.unresolved = true;
      return; // Prevent assignment
    }

    // Assign results to diff
    state.diff[key] = isReplacement ? value : diff;
    if ( isReplacement ) state.source[key] = newSource;
  }

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

  /**
   * Apply an embedded collection update for a single element of the requested value array. Each element is one of:
   * an update to an existing record, the creation of a new record, or (for subclasses such as
   * {@link EmbeddedCollectionDeltaField}) a tombstone marking a deletion. Subclasses may override this method to
   * intercept element-level update behavior.
   * @param {object} v                                The element being processed.
   * @param {object|undefined} existingSource         The existing source record matched by `_id`, if any.
   * @param {EmbeddedCollectionUpdateContext} ctx     Loop-local context shared across elements.
   * @protected
   */
  _updateElement(v, existingSource, ctx) {

    // Update an existing record
    if ( existingSource ) {
      const doc = ctx.collection.createDocument(existingSource);
      try {
        const d = doc.updateSource(v, {clean: false, recursive: !ctx.isReplacement,
          modifiedTime: ctx.state.modifiedTime, user: ctx.state.user});
        if ( !isEmpty(d) ) {
          d._id = v._id;
          ctx.diff.push(d);
        }
        if ( ctx.isReplacement ) ctx.newSource.push(existingSource);
      } catch(error) {
        if ( error instanceof DataModelValidationError ) {
          ctx.failure.elements.push({id: v._id, failure: error.getFailure()});
        }
        else throw new Error("Unexpected Error class", {cause: error});
      }
      return;
    }

    // Create a new record
    const createData = applyDataOperators(v);
    if ( !createData._id ) createData._id = randomID();
    try {
      const doc = ctx.collection.createDocument(createData, {strict: true, creation: true,
        modifiedTime: ctx.state.modifiedTime, user: ctx.state.user});
      ctx.diff.push(doc._source);
      ctx.newSource.push(doc._source);
    } catch(err) {
      ctx.failure.elements.push({id: createData._id, failure: err});
    }
  }

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

  /** @override */
  _updateCommit(source, key, value, diff, options) {
    const src = source[key];

    // Special Cases: * -> undefined, * -> null, undefined -> *, null -> *
    if ( !src || !value ) {
      source[key] = value;
      return;
    }

    // Map the existing source objects
    const existing = {};
    for ( const obj of src ) existing[obj._id] = obj;
    const changed = {};
    for ( const obj of diff ) {
      if ( !obj ) continue;
      changed[obj._id] = obj;
    }

    // Reconstruct the source array, retaining object references
    src.length = 0;
    for ( const obj of value ) this._commitElement(obj, src, existing, changed, options);
  }

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

  /**
   * Commit a single element into the destination source array.
   * Subclasses may override this method to apply specialized logic for individual elements.
   * @param {object} obj                            The element being committed
   * @param {object[]} src                          The destination source array under construction
   * @param {Record<string, object>} existing       Map of pre-update source records
   * @param {Record<string, object>} changed        Map of diff entries
   * @param {DataModelUpdateOptions} options        Update options
   * @protected
   */
  _commitElement(obj, src, existing, changed, options) {
    const prior = existing[obj._id];
    if ( prior ) {
      const d = changed[obj._id];
      if ( d ) this.schema._updateCommit({_source: prior}, "_source", obj, d, options);
      src.push(prior);
      return;
    }
    src.push(obj);
  }

  /* -------------------------------------------- */
  /*  Field Transformations                       */
  /* -------------------------------------------- */

  /** @override */
  toObject(value) {
    return value.toObject(false);
  }

  /** @override */
  apply(fn, value=[], options={}) {

    // Include this field in the options since it's not introspectable from the SchemaField
    options = {...options, collection: this};

    // Apply to this EmbeddedCollectionField
    const thisFn = typeof fn === "string" ? this[fn] : fn;
    thisFn?.call(this, value, options);

    // Recursively apply to inner fields
    const results = [];
    if ( !value.length && options.initializeArrays ) value = [undefined];
    for ( const v of value ) {
      const r = this.schema.apply(fn, v, options);
      if ( !options.filter || !isEmpty(r) ) results.push(r);
    }
    return results;
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.EmbeddedCollectionField} which manages a collection of delta objects
 * relative to another collection.
 */
class EmbeddedCollectionDeltaField extends EmbeddedCollectionField {
  /** @override */
  static get implementation() {
    return EmbeddedCollectionDelta;
  }

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

  /** @override */
  _cleanElement(value, options, _state) {
    if ( value._tombstone ) return foundry.data.TombstoneData.schema.clean(value, options, _state);
    return super._cleanElement(value, options, _state);
  }

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

  /** @override */
  _validateRecursive(value, options) {
    const collectionFailure = new DataModelValidationFailure("EmbeddedCollectionDeltaField#_validateRecursive",
      {fieldPath: this.fieldPath, unresolved: false});
    for ( const v of value ) {
      const schema = v._tombstone ? foundry.data.TombstoneData.schema : this.schema;
      const failure = schema.validate(v, {...options, source: v});
      if ( failure && !options.dropInvalidEmbedded ) {
        collectionFailure.elements.push({id: v._id, name: v.name, failure});
        collectionFailure.unresolved ||= failure.unresolved;
      }
    }
    if ( collectionFailure.elements.length ) return collectionFailure;
  }

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

  /** @inheritDoc */
  _updateElement(v, existingSource, ctx) {

    // Ignore updates to an existing Tombstone unless made via ForcedReplacement
    if ( existingSource?._tombstone && !ctx.isReplacement ) return;

    // Create new Tombstones
    if ( v._tombstone ) {
      const tombstoneFailure = foundry.data.TombstoneData.schema.validate(v, {strict: false, source: v});
      if ( tombstoneFailure?.unresolved ) {
        ctx.failure.elements.push({id: v._id, failure: tombstoneFailure});
        return;
      }
      if ( existingSource && !ctx.isReplacement ) ctx.source.splice(ctx.source.indexOf(existingSource), 1);
      ctx.diff.push(v);
      ctx.newSource.push(v);
      return;
    }

    // Replace an existing Tombstone with a new Document
    if ( existingSource?._tombstone ) existingSource = null;
    return super._updateElement(v, existingSource, ctx);
  }

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

  /** @inheritDoc */
  _commitElement(obj, src, existing, changed, options) {
    if ( obj._tombstone || existing[obj._id]?._tombstone ) src.push(obj);
    else super._commitElement(obj, src, existing, changed, options);
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.EmbeddedDataField} which supports a single embedded Document.
 */
class EmbeddedDocumentField extends EmbeddedDataField {
  /**
   * @param {typeof Document} model       The type of Document which is embedded.
   * @param {DataFieldOptions} [options]  Options which configure the behavior of the field.
   * @param {DataFieldContext} [context]  Additional context which describes the field
   */
  constructor(model, options={}, context={}) {
    if ( !isSubclass(model, foundry.abstract.Document) ) {
      throw new Error("An EmbeddedDocumentField must specify a Document subclass as its type.");
    }
    super(model.implementation, options, context);
  }

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {nullable: true});
  }

  /** @override */
  static hierarchical = true;

  /** @override */
  initialize(value, model, options={}) {
    if ( !value ) return value;
    if ( model[this.name] ) {
      model[this.name]._initialize(options);
      return model[this.name];
    }
    return new this.model(value, {...options, parent: model, parentCollection: this.name, clean: false, schema: this});
  }

  /* -------------------------------------------- */
  /*  Embedded Document Operations                */
  /* -------------------------------------------- */

  /**
   * Return the embedded document(s) as a Collection.
   * @param {Document} parent  The parent document.
   * @returns {Collection<string, Document>}
   */
  getCollection(parent) {
    const collection = new SingletonEmbeddedCollection(this.name, parent, []);
    const doc = parent[this.name];
    if ( !doc ) return collection;
    collection.set(doc.id, doc, { modifySource: false });
    return collection;
  }
}

/* -------------------------------------------- */
/*  Special Field Types                         */
/* -------------------------------------------- */

/**
 * A subclass of {@link foundry.data.fields.StringField} which provides the primary _id for a Document.
 * The field may be initially null, but it must be non-null when it is saved to the database.
 */
class DocumentIdField extends StringField {

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {
      required: true,
      blank: false,
      nullable: true,
      readonly: true,
      validationError: "is not a valid Document ID string"
    });
  }

  /** @override */
  _cast(value) {
    if ( !value && this.nullable ) return null;
    if ( value instanceof foundry.abstract.Document ) return value._id;
    else return String(value);
  }

  /** @override */
  _validateType(value, _options) {
    if ( !isValidId(value) ) throw new Error("must be a valid 16-character alphanumeric ID");
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.StringField} which supports referencing some other Document by its UUID.
 * This field may not be blank, but may be null to indicate that no UUID is referenced.
 */
class DocumentUUIDField extends StringField {
  /**
   * @param {DocumentUUIDFieldOptions} [options] Options which configure the behavior of the field
   * @param {DataFieldContext} [context]    Additional context which describes the field
   */
  constructor(options, context) {
    super(options, context);
  }

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {
      required: true,
      blank: false,
      nullable: true,
      initial: null,
      type: undefined,
      embedded: undefined,
      relative: false
    });
  }

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

  /** @inheritDoc */
  _cleanType(value, options, state) {
    value = super._cleanType(value, options, state);
    if ( !value.startsWith(".") ) return value;
    if ( !this.relative || !state?.model ) return value;
    try {
      value = buildRelativeUuid(value, state.model);
    } catch(err) {}
    return value;
  }

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

  /** @override */
  _validateType(value, options={}) {
    if ( value.startsWith(".") ) {
      if ( !this.relative ) throw Error("must not be a relative document UUID");
      else if ( !(options.model instanceof foundry.abstract.Document) ) throw Error("cannot be resolved without a document instance");
    }
    const p = parseUuid(value, {relative: options.model});
    if ( !p ) throw new Error("must be a valid document UUID");
    if ( this.type ) {
      if ( p.type !== this.type ) throw new Error(`must reference document type "${this.type}"`);
    }
    else if ( p.type && !ALL_DOCUMENT_TYPES.includes(p.type) ) throw new Error("must reference a valid document type");
    if ( (this.embedded === true) && !p.embedded.length ) throw new Error("must reference an embedded document");
    if ( (this.embedded === false) && p.embedded.length ) throw new Error("may not reference an embedded document");
    if ( !isValidId(p.documentId) ) throw new Error("must contain a valid document ID");
  }

  /* -------------------------------------------- */
  /*  Form Field Integration                      */
  /* -------------------------------------------- */

  /** @override */
  _toInput(config) {
    Object.assign(config, {type: this.type, single: true});
    if ( this.relative && config.model && config.value ) {
      config.value = parseUuid(config.value, {relative: config.model})?.uuid ?? config.value;
    }
    return foundry.applications.elements.HTMLDocumentTagsElement.create(config);
  }
}

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

/**
 * A special class of {@link foundry.data.fields.StringField} field which references another DataModel by its id.
 * This field may also be null to indicate that no foreign model is linked.
 */
class ForeignDocumentField extends DocumentIdField {
  /**
   * @param {typeof Document} model  The foreign DataModel class definition which this field links to
   * @param {StringFieldOptions} [options]    Options which configure the behavior of the field
   * @param {DataFieldContext} [context]      Additional context which describes the field
   */
  constructor(model, options={}, context={}) {
    super(options, context);
    if ( !isSubclass(model, foundry.abstract.DataModel) ) {
      throw new Error("A ForeignDocumentField must specify a DataModel subclass as its type");
    }
    /**
     * A reference to the model class which is stored in this field
     * @type {typeof Document}
     */
    this.model = model;
  }

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {nullable: true, readonly: false, idOnly: false});
  }

  /** @override */
  _cast(value) {
    if ( !value && this.nullable ) return null;
    if ( typeof value === "string" ) return value;
    if ( (value instanceof this.model) ) return value._id;
    throw new Error(`The value provided to a ForeignDocumentField must be a ${this.model.name} instance.`);
  }

  /** @inheritdoc */
  initialize(value, model, options={}) {
    if ( this.idOnly ) return value;
    if ( value === null ) return () => null;
    if ( model?.pack && !foundry.utils.isSubclass(this.model, foundry.documents.BaseFolder) ) return () => null;
    if ( !game.collections ) return value; // Server-side
    return () => this.model?.get(value, {pack: model?.pack, ...options}) ?? null;
  }

  /** @inheritdoc */
  toObject(value) {
    return value?._id ?? value;
  }

  /* -------------------------------------------- */
  /*  Form Field Integration                      */
  /* -------------------------------------------- */

  /** @override */
  _toInput(config) {
    config.choices ??= this.choices;

    // Prepare passed choices
    StringField._prepareChoiceConfig(config);

    // Prepare visible Document instances as options
    const collection = game.collections.get(this.model.documentName);
    if ( collection && !config.options ) {
      const current = collection.get(config.value);
      let hasCurrent = false;
      const options = collection.reduce((arr, doc) => {
        if ( !doc.visible ) return arr;
        if ( doc === current ) hasCurrent = true;
        arr.push({value: doc.id, label: doc.name});
        return arr;
      }, []);
      if ( current && !hasCurrent ) options.unshift({value: config.value, label: current.name});
      config.options = options;
    }

    // Allow blank
    if ( !this.required || this.nullable ) config.blank ??= "";

    // Create select input
    return foundry.applications.fields.createSelectInput(config);
  }
}

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

/**
 * A special {@link foundry.data.fields.StringField} which records a standardized CSS color string.
 */
class ColorField extends StringField {

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {nullable: true, initial: null, blank: false});
  }

  /** @override */
  initialize(value, model, options={}) {
    if ( (value === null) || (value === undefined) ) return value;
    return Color.from(value);
  }

  /** @override */
  _cast(value) {
    return Color.from(value).css;
  }

  /** @inheritdoc */
  _validateType(value, options) {
    if ( !isColorString(value) ) throw new Error("must be a valid color string");
    return super._validateType(value, options);
  }

  /* -------------------------------------------- */
  /*  Form Field Integration                      */
  /* -------------------------------------------- */

  /** @override */
  _toInput(config) {
    if ( (config.placeholder === undefined) && !this.nullable && typeof this.initial !== "function" ) {
      config.placeholder = this.initial;
    }
    return foundry.applications.elements.HTMLColorPickerElement.create(config);
  }
}

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

/**
 * A special {@link foundry.data.fields.StringField} which records a file path or inline base64 data.
 *
 * When using the `FilePathField` in a data model that is persisted to the database, for example a Document sub-type, it is essential to declare this field in the package manifest so that it receives proper server-side validation of its contents.
 * See {@link foundry.packages.types.ServerSanitizationFields} for information about this structure.
 *
 * @property {string[]} categories      A set of categories in CONST.FILE_CATEGORIES which this field supports
 * @property {boolean} base64=false     Is embedded base64 data supported in lieu of a file path?
 * @property {boolean} texture=false    Does the file path field allow specifying a virtual file path which must begin
 *                                      with the "#" character?
 * @property {boolean} wildcard=false   Does this file path field allow wildcard characters?
 */
class FilePathField extends StringField {
  /**
   * @param {FilePathFieldOptions} [options]  Options which configure the behavior of the field
   * @param {DataFieldContext} [context]      Additional context which describes the field
   */
  constructor(options={}, context={}) {
    super(options, context);
    if ( this.categories.includes("MEDIA") ) {
      foundry.utils.logCompatibilityWarning('The "MEDIA" file category is deprecated. '
        + "Use CONST.MEDIA_FILE_CATEGORIES instead.", {since: 13, until: 15, once: true});
      this.categories = Array.from(new Set(this.categories.filter(c => c !== "MEDIA").concat(CONST.MEDIA_FILE_CATEGORIES)));
      if ( "categories" in this.options ) this.options.categories = this.categories;
    }
    if ( !this.categories.length || this.categories.some(c => !(c in FILE_CATEGORIES)) ) {
      throw new Error("The categories of a FilePathField must be keys in CONST.FILE_CATEGORIES");
    }
  }

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {
      categories: [],
      base64: false,
      wildcard: false,
      virtual: false,
      nullable: true,
      blank: false,
      initial: null
    });
  }

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

  /** @inheritdoc */
  _validateType(value, _options) {

    // Wildcard or virtual paths
    if ( this.virtual && (value[0] === "#") && value.length > 1 ) return true;
    if ( this.wildcard && value.includes("*") ) return true;

    // Allowed extension or base64
    const isValid = this.categories.some(c => {
      const category = FILE_CATEGORIES[c];
      if ( hasFileExtension(value, Object.keys(category)) ) return true;
      return isBase64Data(value, Object.values(category));
    });

    // Throw an error for invalid paths
    if ( !isValid ) {
      let err = "does not have a valid file extension";
      if ( this.base64 ) err += " or provide valid base64 data";
      throw new Error(err);
    }
  }

  /* -------------------------------------------- */
  /*  Form Field Integration                      */
  /* -------------------------------------------- */

  /** @override */
  _toInput(config) {
    // FIXME: This logic is fragile and would require a mapping between CONST.FILE_CATEGORIES and FilePicker.TYPES
    if ( config.type === undefined ) {
      if ( this.categories.length === 1 ) config.type = this.categories[0].toLowerCase();
      else if ( (this.categories.length === 2) && this.categories.includes("IMAGE") && this.categories.includes("VIDEO") ) {
        config.type = "imagevideo";
      }
      else config.type = "any";
    }
    return foundry.applications.elements.HTMLFilePickerElement.create(config);
  }
}

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

/**
 * A special {@link foundry.data.fields.NumberField} which represents an angle of rotation in degrees between 0 and 360.
 * @property {boolean} normalize Whether the angle should be normalized to [0,360) before being clamped to [0,360]. The
 *                               default is true.
 */
class AngleField extends NumberField {
  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {
      required: true,
      nullable: false,
      initial: 0,
      normalize: true,
      min: 0,
      max: 360,
      validationError: "is not a number between 0 and 360"
    });
  }

  /** @inheritdoc */
  _cast(value) {
    value = super._cast(value);
    if ( !this.normalize ) return value;
    value = Math.normalizeDegrees(value);
    return value;
  }
}

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

/**
 * A special {@link foundry.data.fields.NumberField} represents a number between 0 and 1.
 */
class AlphaField extends NumberField {
  static get _defaults() {
    return Object.assign(super._defaults, {
      required: true,
      nullable: false,
      initial: 1,
      min: 0,
      max: 1,
      validationError: "is not a number between 0 and 1"
    });
  }
}

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

/**
 * A special {@link foundry.data.fields.NumberField} represents a number between 0 (inclusive) and 1 (exclusive).
 * Its values are normalized (modulo 1) to the range [0, 1) instead of being clamped.
 */
class HueField extends NumberField {
  static get _defaults() {
    return Object.assign(super._defaults, {
      required: true,
      nullable: false,
      initial: 0,
      min: 0,
      max: 1,
      validationError: "is not a number between 0 (inclusive) and 1 (exclusive)"
    });
  }

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

  /** @inheritdoc */
  _cast(value) {
    value = super._cast(value) % 1;
    if ( value < 0 ) value += 1;
    return value;
  }

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

  /** @override */
  _toInput(config) {
    return foundry.applications.elements.HTMLHueSelectorSlider.create(config);
  }
}

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

/**
 * A special {@link foundry.data.fields.ForeignDocumentField} which defines the original author of a document.
 * This can only be changed later by GM users.
 */
class DocumentAuthorField extends ForeignDocumentField {

  /** @inheritDoc */
  static get _defaults() {
    return Object.assign(super._defaults, {
      nullable: false,
      gmOnly: false, // Server-side sanitization rules prevent changing authors
      initial: () => game.user?.id ?? null
    });
  }
}

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

/**
 * A special {@link foundry.data.fields.ObjectField} which captures a mapping of User IDs to Document permission levels.
 */
class DocumentOwnershipField extends ObjectField {

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {
      initial: {default: DOCUMENT_OWNERSHIP_LEVELS.NONE},
      validationError: "is not a mapping of user IDs and document permission levels",
      gmOnly: false // Server-side sanitization rules prevent changing ownership
    });
  }

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

  /** @inheritDoc */
  _cleanType(data, options, _state) {
    super._cleanType(data, options, _state);
    if ( !options.partial ) data.default ??= DOCUMENT_OWNERSHIP_LEVELS.NONE;
    return data;
  }

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

  /** @override */
  _validateType(value, _options) {
    for ( const [k, v] of Object.entries(value) ) {
      if ( (k !== "default") && !isValidId(k) ) return false;
      if ( !Object.values(DOCUMENT_OWNERSHIP_LEVELS).includes(v) ) return false;
    }
  }
}

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

/**
 * A special {@link foundry.data.fields.StringField} which contains serialized JSON data.
 */
class JSONField extends StringField {
  constructor(options, context) {
    super(options, context);
    this.blank = false;
    this.trim = false;
    this.choices = undefined;
  }

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {
      blank: false,
      trim: false,
      initial: undefined,
      validationError: "is not a valid JSON string"
    });
  }

  /** @inheritDoc */
  clean(value, options, _state) {
    if ( value === "" ) return '""';  // Special case for JSON fields
    return super.clean(value, options, _state);
  }

  /** @override */
  _cast(value) {
    if ( (typeof value !== "string") || !isJSON(value) ) return JSON.stringify(value);
    return value;
  }

  /** @override */
  _validateType(value, _options) {
    if ( (typeof value !== "string") || !isJSON(value) ) throw new Error("must be a serialized JSON string");
  }

  /** @override */
  initialize(value, model, options={}) {
    if ( (value === undefined) || (value === null) ) return value;
    return JSON.parse(value);
  }

  /** @override */
  toObject(value) {
    if ( (value === undefined) || (this.nullable && (value === null)) ) return value;
    return JSON.stringify(value);
  }

  /* -------------------------------------------- */
  /*  Form Field Integration                      */
  /* -------------------------------------------- */

  /**
   * @param {FormInputConfig & CodeMirrorInputConfig} config
   * @override
   */
  _toInput(config) {
    config.language = "json";
    config.indent ??= 2;
    config.value = foundry.data.validators.isJSON(config.value)
      ? JSON.stringify(JSON.parse(config.value), null, config.indent)
      : JSON.stringify(config.value, null, config.indent);
    return foundry.applications.elements.HTMLCodeMirrorElement.create(config);
  }
}

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

/**
 * A special subclass of {@link foundry.data.fields.DataField} which can contain any value of any type.
 * Any input is accepted and is treated as valid. Setting the serializable option to true will apply some light cleaning
 * and validation, ensuring that a candidate value will not be appreciably changed when serialized for writing to disk.
 * It is not recommended to use this class except in very specific circumstances.
 */
class AnyField extends DataField {
  /** @inheritDoc */
  static get _defaults() {
    return Object.assign(super._defaults, {nullable: true, serializable: false});
  }

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

  /** @inheritDoc */
  clean(value, options, _state) {
    if ( !this.serializable ) return super.clean(value, options, _state);
    if ( typeof value === "string" ) value = value.trim();
    else if ( this.nullable && ((value === Infinity) || (value === -Infinity)) ) value = null;
    return super.clean(value, options, _state);
  }

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

  /** @override */
  _validateType(value) {
    if ( !this.serializable ) return;
    const stringified = JSON.stringify(value);
    if ( !equals(value, JSON.parse(stringified)) ) {
      throw new Error(`"${stringified}" ${this.validationError}`);
    }
  }

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

  /** @override */
  _toInput(config) {
    if ( !this.serializable ) throw new Error("Only serializable AnyField values have form inputs.");
    config.elementType ??= "code-mirror";
    if ( config.elementType !== "code-mirror" ) {
      config.value = typeof config.value === "string" ? config.value : JSON.stringify(config.value);
      return foundry.applications.fields.createTextInput(config);
    }
    config.language = "json";
    config.indent ??= 2;
    config.value = JSON.stringify(config.value, null, config.indent);
    return foundry.applications.elements.HTMLCodeMirrorElement.create(config);
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.StringField} which contains a sanitized HTML string.
 * This class does not override any StringField behaviors, but is used by the server-side to identify fields which
 * require sanitization of user input.
 *
 * When using the `HTMLField` in a data model that is persisted to the database, for example a Document sub-type, it is essential to declare this field in the package manifest so that it receives proper server-side validation of its contents.
 * See {@link foundry.packages.types.ServerSanitizationFields} for information about this structure.
 */
class HTMLField extends StringField {

  /** @inheritDoc */
  static get _defaults() {
    return Object.assign(super._defaults, {required: true, blank: true});
  }

  /** @inheritDoc */
  toFormGroup(groupConfig={}, inputConfig={}) {
    groupConfig.stacked ??= inputConfig.elementType !== "input";
    return super.toFormGroup(groupConfig, inputConfig);
  }

  /** @inheritDoc */
  _toInput(config) {
    config.elementType ??= "prose-mirror";
    return super._toInput(config);
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.NumberField} which is used for storing integer sort keys.
 */
class IntegerSortField extends NumberField {
  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {
      required: true,
      nullable: false,
      integer: true,
      initial: 0
    });
  }
}
/* ---------------------------------------- */

/**
 * A subclass of {@link foundry.data.fields.TypedObjectField} that is used specifically for the Document "flags" field.
 */
class DocumentFlagsField extends TypedObjectField {
  /**
   * @param {DataFieldOptions} [options]    Options which configure the behavior of the field
   * @param {DataFieldContext} [context]    Additional context which describes the field
   */
  constructor(options, context) {
    super(new ObjectField(), options, context);
  }

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {
      validateKey: k => {
        try {
          foundry.packages.BasePackage.validateId(k);
        } catch {
          return false;
        }
        return true;
      }
    });
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.SchemaField} which stores document metadata in the _stats field.
 * @mixes DocumentStats
 */
class DocumentStatsField extends SchemaField {
  /**
   * @param {DataFieldOptions} [options]        Options which configure the behavior of the field
   * @param {DataFieldContext} [context]        Additional context which describes the field
   */
  constructor(options={}, context={}) {
    super({
      coreVersion: new StringField({required: true, blank: false, nullable: true, initial: () => game.release.version}),
      systemId: new StringField({required: true, blank: false, nullable: true, initial: () => game.system?.id ?? null}),
      systemVersion: new StringField({
        required: true,
        blank: false,
        nullable: true,
        initial: () => game.system?.version ?? null
      }),
      createdTime: new NumberField({required: true}),
      modifiedTime: new NumberField({required: true}),
      lastModifiedBy: new ForeignDocumentField(foundry.documents.BaseUser, {idOnly: true}),
      compendiumSource: new DocumentUUIDField(),
      duplicateSource: new DocumentUUIDField(),
      exportSource: new SchemaField({
        worldId: new StringField({required: true, blank: false, nullable: true}),
        uuid: new DocumentUUIDField({initial: undefined}),
        coreVersion: new StringField({required: true, blank: false, nullable: true}),
        systemId: new StringField({required: true, blank: false, nullable: true}),
        systemVersion: new StringField({required: true, blank: false, nullable: true})
      }, {nullable: true})
    }, options, context);
  }

  /**
   * All Document stats.
   * @type {string[]}
   */
  static fields = [
    "coreVersion", "systemId", "systemVersion", "createdTime", "modifiedTime", "lastModifiedBy", "compendiumSource",
    "duplicateSource", "exportSource"
  ];

  /**
   * These fields are managed by the server and are ignored if they appear in creation or update data.
   * @type {string[]}
   */
  static managedFields = ["coreVersion", "systemId", "systemVersion", "createdTime", "modifiedTime", "lastModifiedBy"];

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

  /**
   * Migrate deprecated core flags to `_stats` properties.
   * @param {typeof Document} document
   * @param {object} source
   * @internal
   */
  static _migrateData(document, source) {

    /**
     * Migrate flags.exportSource.
     * @deprecated since v13
     */
    if ( source.flags ) {
      document._addDataFieldMigration(source, "flags.exportSource", "_stats.exportSource", d => {
        const exportSource = foundry.utils.getProperty(d, "flags.exportSource");
        if ( !exportSource ) return null;
        return {
          worldId: exportSource.world ?? null,
          uuid: null,
          coreVersion: exportSource.coreVersion ?? null,
          systemId: exportSource.system ?? null,
          systemVersion: exportSource.systemVersion ?? null
        };
      });
    }
  }

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

  /**
   * Shim the deprecated core flag `exportSource` on Document source data.
   * @param {typeof Document} document
   * @param {object} source
   * @param {object} [options]
   * @internal
   */
  static _shimData(document, source, options) {
    if ( source.flags ) {
      /**
       * Shim flags.exportSource.
       * @deprecated since v13
       */
      Object.defineProperty(source.flags, "exportSource", {
        get: () => {
          document._logDataFieldMigration("flags.exportSource", "_stats.exportSource", {since: 13, until: 15});
          const exportSource = source._stats.exportSource;
          if ( !exportSource ) return undefined;
          return {
            world: exportSource.worldId,
            coreVersion: exportSource.coreVersion,
            system: exportSource.systemId,
            systemVersion: exportSource.systemVersion
          };
        },
        configurable: true,
        enumerable: false
      });
    }
  }

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

  /**
   * Shim the deprecated core flag `exportSource` on Documents.
   * @param {typeof Document} document
   * @internal
   */
  static _shimDocument(document) {
    /**
     * Shim flags.exportSource.
     * @deprecated since v13
     */
    Object.defineProperty(document.flags, "exportSource", {
      get: () => {
        document.constructor._logDataFieldMigration("flags.exportSource", "_stats.exportSource", {since: 13, until: 15});
        const exportSource = document._stats.exportSource;
        if ( !exportSource ) return undefined;
        return {
          world: exportSource.worldId,
          coreVersion: exportSource.coreVersion,
          system: exportSource.systemId,
          systemVersion: exportSource.systemVersion
        };
      },
      configurable: true,
      enumerable: false
    });
  }

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

  /** @ignore */
  _sanitizeType(value, options, _state) {
    if ( !options.deleteStats ) return value;
    const clean = this.clean({});
    if ( !isPlainObject(value) ) return clean;
    for ( const fieldName of this.constructor.managedFields ) value[fieldName] = clean[fieldName];
    return value;
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.StringField} that is used specifically for the Document "type" field.
 */
class DocumentTypeField extends StringField {
  /**
   * @param {typeof Document} documentClass  The base document class which belongs in this field
   * @param {StringFieldOptions} [options]  Options which configure the behavior of the field
   * @param {DataFieldContext} [context]    Additional context which describes the field
   */
  constructor(documentClass, options={}, context={}) {
    options.choices = () => documentClass.TYPES;
    options.validationError = `is not a valid type for the ${documentClass.documentName} Document class`;
    super(options, context);
  }

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {required: true, nullable: false, blank: false});
  }

  /** @override */
  _validateType(value, options) {
    if ( (typeof value !== "string") || !value ) throw new Error("must be a non-blank string");
    if ( this._isValidChoice(value) ) return true;
    // Allow unrecognized types if we are allowed to fallback (non-strict validation)
    if ( options.fallback ) return true;
    throw new Error(`"${value}" ${this.options.validationError}`);
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.ObjectField} which supports a type-specific data object.
 */
class TypeDataField extends ObjectField {
  /**
   * @param {typeof Document} document      The base document class which belongs in this field
   * @param {DataFieldOptions} [options]    Options which configure the behavior of the field
   * @param {DataFieldContext} [context]    Additional context which describes the field
   */
  constructor(document, options={}, context={}) {
    super(options, context);
    /**
     * The canonical document name of the document type which belongs in this field
     * @type {typeof Document}
     */
    this.document = document;
  }

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {required: true, nullable: false});
  }

  /** @override */
  static recursive = true;

  /**
   * Return the package that provides the sub-type for the given model.
   * @param {DataModel} model       The model instance created for this sub-type.
   * @returns {System|Module|null}
   */
  static getModelProvider(model) {
    const document = model.parent;
    if ( !document ) return null;
    const documentClass = document.constructor;
    const documentName = documentClass.documentName;
    const type = document.type;

    // Unrecognized type
    if ( !documentClass.TYPES.includes(type) ) return null;

    // Core-defined sub-type
    const coreTypes = documentClass.metadata.coreTypes;
    if ( coreTypes.includes(type) ) return null;

    // System-defined sub-type
    const systemTypes = game.system.documentTypes[documentName];
    if ( systemTypes && (type in systemTypes) ) return game.system;

    // Module-defined sub-type
    const moduleId = type.substring(0, type.indexOf("."));
    return game.modules.get(moduleId) ?? null;
  }

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

  /**
   * A convenience accessor for the name of the document type associated with this TypeDataField
   * @type {string}
   */
  get documentName() {
    return this.document.documentName;
  }

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

  /**
   * Get the DataModel definition that should be used for this type of document.
   * @param {string} type              The Document instance type
   * @returns {typeof DataModel|null}  The DataModel class or null
   */
  getModelForType(type) {
    if ( !type ) return null;
    return globalThis.CONFIG?.[this.documentName]?.dataModels?.[type] ?? null;
  }

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

  /** @override */
  _getField(parts, {source, type}={}) {
    if ( !parts.length ) return this;
    if ( !type ) return undefined;
    const model = this.getModelForType(type);
    return model?.schema?._getField(parts, {source});
  }

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

  /** @override */
  initialize(value, model, options={}) {
    const cls = this.getModelForType(model._source.type);
    if ( cls ) {
      const instance = new cls(value, {parent: model, ...options, clean: false});
      if ( !("modelProvider" in instance) ) Object.defineProperty(instance, "modelProvider", {
        value: this.constructor.getModelProvider(instance),
        writable: false
      });
      return instance;
    }
    return deepClone(value);
  }

  /* ---------------------------------------- */
  /*  Field Cleaning                          */
  /* ---------------------------------------- */

  /** @inheritDoc */
  clean(value, options, _state) {
    if ( !value ) return this.getInitialValue({type: _state.documentType});
    return super.clean(value, options, _state);
  }

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

  /** @override */
  _migrate(value, options, _state) {
    if ( !isPlainObject(value) ) return value;
    const cls = this.getModelForType(_state.documentType);
    if ( cls ) {
      const migrated = cls.migrateDataSafe(value, options);
      /** @deprecated since v14 */
      if ( migrated === undefined ) {
        foundry.utils.logCompatibilityWarning(`${cls.name} has a migrateData implementation that does not return a `
          + "value. Such implementations will be unsupported in a future version.", { since: 14, until: 16 });
        return value;
      }
      return migrated;
    }
    return value;
  }

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

  /** @override */
  getInitialValue(source) {
    const initial = super.getInitialValue(source) || {};
    return this._cleanType(initial, {partial: false}, {documentType: source.type});
  }

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

  /** @override */
  _cleanType(value, options, _state) {
    const type = _state.documentType;

    // Use a defined DataModel
    const cls = this.getModelForType(type);
    if ( cls ) {
      const _innerState = {..._state, model: _state.model?._getInnerModel?.(this, { value }, options) ?? null};
      return cls.cleanData(value, {...options, copy: false}, _innerState);
    }
    else super._cleanType(value, options, _state); // Clean as an object field
    if ( options.partial ) return value;

    // Use a defined template.json
    /** @deprecated since v14 until v16 */
    const template = game?.model[this.documentName]?.[type];
    if ( template ) {
      const insertKeys = (type === BASE_DOCUMENT_TYPE) || !game?.system?.strictDataCleaning;
      return mergeObject(template, value, {insertKeys, inplace: false});
    }
    return value;
  }

  /* ---------------------------------------- */
  /*  Field Validation                        */
  /* ---------------------------------------- */

  /** @override */
  _validateRecursive(value, options) {
    const source = options.model?._source;
    const cls = this.getModelForType(source?.type);
    if ( !cls ) return;
    return cls.schema.validate(value, {...options, source, strict: true});
  }

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

  /** @override */
  _validateModel(changes, options={}) {
    const cls = this.getModelForType(options.source?.type);
    return cls?.validateJoint(changes);
  }

  /* ---------------------------------------- */
  /*  Field Updates                           */
  /* ---------------------------------------- */

  /** @inheritDoc */
  _updateDiff(key, value, options, state) {
    const cls = this.getModelForType(state.source.type);
    if ( cls ) return cls.schema._updateDiff(key, value, options, state);
    super._updateDiff(key, value, options, state);

    // Without a registered model the value is diffed as a plain object, which omits a replacement matching the current
    // source. Retain it, as SchemaField does, so the type change it accompanies remains applicable.
    if ( value instanceof ForcedReplacement ) state.diff[key] = value;
  }

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

  /** @inheritDoc */
  _updateCommit(source, key, value, diff, options) {
    if ( diff instanceof ForcedReplacement ) {
      source[key] = applyDataOperators(diff);
      return;
    }
    const cls = this.getModelForType(source.type);
    if ( cls ) cls.schema._updateCommit(source, key, value, diff, options);
    else super._updateCommit(source, key, value, diff, options);
  }

  /* ---------------------------------------- */
  /*  Field Transformations                   */
  /* ---------------------------------------- */

  /** @override */
  toObject(value) {
    return typeof value.toObject === "function" ? value.toObject(false) : deepClone(value);
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.DataField} that defines a union of schema-constrained objects discriminable
 * via a `type` property.
 */
class TypedSchemaField extends DataField {
  /**
   * @param {Record<string, DataSchema|SchemaField|typeof DataModel>} types The different types this field can represent
   * @param {DataFieldOptions} [options]                                    Options for configuring the field
   * @param {DataFieldContext} [context]                                    Additional context describing the field
   */
  constructor(types, options, context) {
    super(options, context);
    this.types = this.#configureTypes(types);
  }

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

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {required: true});
  }

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

  /** @override */
  static recursive = true;

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

  /**
   * The types of this field.
   * @type {{[type: string]: SchemaField}}
   */
  types;

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

  /**
   * Initialize and validate the structure of the provided type definitions.
   * @param {{[type: string]: DataSchema|SchemaField|typeof DataModel}} types The provided field definitions
   * @returns {{[type: string]: SchemaField}}                                 The validated fields
   */
  #configureTypes(types) {
    if ( (typeof types !== "object") ) {
      throw new Error("A DataFields must be an object with string keys and DataField values.");
    }
    types = {...types};
    for ( let [type, field] of Object.entries(types) ) {
      if ( isSubclass(field, foundry.abstract.DataModel) ) field = new EmbeddedDataField(field, {name: type});
      if ( isPlainObject(field) ) {
        const schema = {...field};
        if ( !("type" in schema) ) {
          schema.type = new StringField({required: true, blank: false, initial: type,
            validate: value => value === type, validationError: `must be equal to "${type}"`});
        }
        field = new SchemaField(schema, {name: type});
      }
      if ( !(field instanceof SchemaField) ) {
        throw new Error(`The "${type}" field is not an instance of the SchemaField class or a subclass of DataModel.`);
      }
      field.name ??= type;
      if ( field.name !== type ) throw new Error(`The name of the "${this.fieldPath}.${type}" field must be "${type}".`);
      if ( field.parent !== undefined ) {
        throw new Error(`The "${field.fieldPath}" field already belongs to some other parent and may not be reused.`);
      }
      types[type] = field;
      field.parent = this;
      if ( !field.required ) throw new Error(`The "${field.fieldPath}" field must be required.`);
      if ( field.nullable ) throw new Error(`The "${field.fieldPath}" field must not be nullable.`);
      const typeField = field.fields.type;
      if ( !(typeField instanceof StringField) ) throw new Error(`The "${field.fieldPath}" field must have a "type" StringField.`);
      if ( !typeField.required ) throw new Error(`The "${typeField.fieldPath}" field must be required.`);
      if ( typeField.nullable ) throw new Error(`The "${typeField.fieldPath}" field must not be nullable.`);
      if ( typeField.blank ) throw new Error(`The "${typeField.fieldPath}" field must not be blank.`);

      // Ensure the final type is valid
      const valueFailure = typeField.validate(type, {fallback: false});
      if ( valueFailure ) {
        valueFailure.message = `"${type}" must be a valid type of "${typeField.fieldPath}".`;
        throw valueFailure;
      }
    }
    return types;
  }

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

  /**
   * Get the schema for the given type.
   * @param {*} type
   * @returns {SchemaField|void}
   */
  #getTypeSchema(type) {
    if ( typeof type !== "string" ) return;
    if ( !Object.hasOwn(this.types, type) ) return;
    return this.types[type];
  }

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

  /** @override */
  _getField(parts, {source}={}) {
    let schema;
    if ( source === undefined ) {
      if ( !parts.length ) return this;
      schema = this.#getTypeSchema(parts.pop());
    } else if ( source !== null ) {
      if ( typeof source !== "object" ) return undefined;
      schema = this.#getTypeSchema(source.type);
    }
    return schema?._getField(parts, {source});
  }

  /* -------------------------------------------- */
  /*  Data Field Methods                          */
  /* -------------------------------------------- */

  /** @override */
  _cast(value) {
    if ( typeof value.toObject === "function" ) value = value.toObject();
    return isPlainObject(value) ? value : {};
  }

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

  /** @override */
  _cleanType(value, options, _state) {
    const type = DataFieldOperator.get(value.type) ?? _state.source?.type;
    const schema = this.#getTypeSchema(type);
    if ( !schema ) return value;
    if ( options.addTypes ) value.type = type;
    if ( type !== _state.source?.type ) options = Object.freeze({...options, partial: false}); // Require full clean
    return schema.clean(value, options, _state);
  }

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

  /** @override */
  _validateType(value, options) {
    if ( typeof value !== "object" ) throw new Error("does not have a valid type");
    let typeValue = value.type || options.model?._source[this.fieldPath]?.type;
    typeValue = DataFieldOperator.get(typeValue);
    const schema = this.#getTypeSchema(typeValue);
    if ( !schema ) throw new Error("does not have a valid type");
    if ( options.recursive === false ) return;
    return schema.validate(value, options);
  }

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

  /** @override */
  initialize(value, model, options) {
    const schema = this.#getTypeSchema(value?.type);
    if ( !schema ) return value;
    return schema.initialize(value, model, options);
  }

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

  /** @inheritDoc */
  _updateDiff(key, value, options, state) {

    // * -> null, or DataFieldOperator
    if ( !value || (value instanceof DataFieldOperator) ) return super._updateDiff(key, value, options, state);

    // Prevent type changes except by ForcedReplacement
    const existing = state.source[key];
    const typeValue = value.type instanceof DataFieldOperator
      ? DataFieldOperator.get(value.type)
      : (value.type ?? existing?.type);
    const typeChanged = ("type" in value) && existing && (typeValue !== existing.type);
    if ( typeChanged && !(value.type instanceof ForcedReplacement) ) {
      throw new Error("The type of a TypedSchemaField can be changed only by ForcedReplacement.");
    }

    // Call updateDiff for the new type schema
    const schema = this.#getTypeSchema(typeValue);
    if ( schema ) schema._updateDiff(key, value, options, state);
    else super._updateDiff(key, value, options, state);
  }

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

  /** @inheritDoc */
  _updateCommit(source, key, value, diff, options) {
    const schema = this.#getTypeSchema(value?.type ?? source[key]?.type);
    if ( schema && diff ) {
      if ( "type" in diff ) source[key] = undefined;
      schema._updateCommit(source, key, value, diff, options);
    }
    else super._updateCommit(source, key, value, diff, options);
  }

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

  /** @override */
  toObject(value) {
    if ( !value ) return value;
    return this.#getTypeSchema(value.type)?.toObject(value) ?? value;
  }

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

  /** @override */
  apply(fn, data={}, options={}) {

    // Apply to this TypedSchemaField
    const thisFn = typeof fn === "string" ? this[fn] : fn;
    thisFn?.call(this, data, options);

    // Apply to the schema of the type
    const schema = this.#getTypeSchema(data.type);
    return schema?.apply(fn, data, options) ?? {};
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.StringField} which contains JavaScript code.
 */
class JavaScriptField extends StringField {
  /**
   * @param {JavaScriptFieldOptions} [options] Options which configure the behavior of the field
   * @param {DataFieldContext} [context]    Additional context which describes the field
   */
  constructor(options, context) {
    super(options, context);
    this.choices = undefined;
  }

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {required: true, blank: true, nullable: false, async: false});
  }

  /** @inheritdoc */
  _validateType(value, options) {
    const result = super._validateType(value, options);
    if ( result !== undefined ) return result;
    try {
      new (this.async ? AsyncFunction : Function)(value);
    } catch(err) {
      const scope = this.async ? "an asynchronous" : "a synchronous";
      err.message = `must be valid JavaScript for ${scope} scope:\n${err.message}`;
      throw new Error(err);
    }
  }

  /* -------------------------------------------- */
  /*  Form Field Integration                      */
  /* -------------------------------------------- */

  /** @override */
  toFormGroup(groupConfig={}, inputConfig={}) {
    groupConfig.stacked ??= true;
    return super.toFormGroup(groupConfig, inputConfig);
  }

  /**
   * @param {FormInputConfig & CodeMirrorInputConfig} config
   * @override
   */
  _toInput(config) {
    config.language = "javascript";
    config.indent ??= 2;
    return foundry.applications.elements.HTMLCodeMirrorElement.create(config);
  }
}

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

/**
 * A nullable set field used to reference a set of specific Scene Level documents.
 */
class SceneLevelsSetField extends SetField {
  /**
   * @param {ArrayFieldOptions} [options]  Options which configure the behavior of the field
   * @param {DataFieldContext} [context]   Additional context which describes the field
   */
  constructor(options={}, context={}) {
    super(new DocumentIdField({nullable: false, readonly: false}), options, context);
  }

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {required: true, initial: []});
  }
}

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

/**
 * A subclass of {@link foundry.data.fields.ArrayField} for shapes.
 */
class ShapesField extends ArrayField {
  /**
   * @param {ArrayFieldOptions} [options]  Options which configure the behavior of the field
   * @param {DataFieldContext} [context]   Additional context which describes the field
   */
  constructor(options, context) {
    super(new TypedSchemaField(foundry.data.BaseShapeData.TYPES), options, context);
  }

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

  /** @override */
  initialize(value, model, options={}) {
    if ( !value ) return value;
    return value.map((v, i) => {
      const shape = this.element.initialize(v, model, options);
      shape._index = i;
      return shape;
    });
  }
}

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

/**
 * The field for a grid offset.
 */
class GridOffsetField extends SchemaField {
  /**
   * @param {GridOffsetFieldOptions} [options]  Options which configure the behavior of the field
   * @param {DataFieldContext} [context]        Additional context which describes the field
   */
  constructor(options={}, context={}) {
    const {dimensions=2} = options;
    if ( ![2, 3].includes(dimensions) ) throw new Error("Invalid number of dimensions");
    const schema = {
      i: new NumberField({required: true, nullable: false, integer: true, initial: undefined}),
      j: new NumberField({required: true, nullable: false, integer: true, initial: undefined})
    };
    if ( dimensions === 3 ) schema.k = new NumberField({required: true, nullable: false, integer: true});
    super(schema, options, context);
  }

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {dimensions: 2});
  }

  /** @inheritDoc */
  _cast(value) {
    if ( typeof value === "string" ) {
      const coords = value.split(".");
      if ( coords.length === this.dimensions ) {
        if ( this.dimensions === 2 ) return {i: coords[0], j: coords[1]};
        return {i: coords[0], j: coords[1], k: coords[2]};
      }
    }
    return super._cast(value);
  }
}

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

/**
 * The field of an array/set of grid offsets.
 */
class GridOffsetsField extends ArrayField {
  /**
   * @param {GridOffsetsFieldOptions} [options]  Options which configure the behavior of the field
   * @param {DataFieldContext} [context]         Additional context which describes the field
   */
  constructor(options, context) {
    super(new GridOffsetField({dimensions: options?.dimensions ?? 2}), options, context);
  }

  /** @inheritdoc */
  static get _defaults() {
    return Object.assign(super._defaults, {dimensions: 2});
  }

  /* -------------------------------------------- */
  /*  Form Field Integration                      */
  /* -------------------------------------------- */

  /** @override */
  _toInput(config) {
    if ( config.value ) config.value = config.value.map(offset => {
      if ( typeof offset === "string" ) return offset;
      if ( "k" in offset ) return `${offset.i}.${offset.j}.${offset.k}`;
      return `${offset.i}.${offset.j}`;
    });
    const elementClass = this.dimensions === 2 ? foundry.applications.elements.HTMLGridOffset2DTagsElement
      : foundry.applications.elements.HTMLGridOffset3DTagsElement;
    return elementClass.create(config);
  }
}

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

// Exports need to be at the bottom so that class names appear correctly in JSDoc
export {
  AlphaField,
  AngleField,
  AnyField,
  ArrayField,
  BooleanField,
  ColorField,
  DataField,
  DataModelSchemaField,
  DocumentAuthorField,
  DocumentFlagsField,
  DocumentIdField,
  DocumentOwnershipField,
  DocumentStatsField,
  DocumentTypeField,
  DocumentUUIDField,
  EmbeddedCollectionDeltaField,
  EmbeddedCollectionField,
  EmbeddedDataField,
  EmbeddedDocumentField,
  FilePathField,
  ForeignDocumentField,
  GridOffsetField,
  GridOffsetsField,
  HTMLField,
  HueField,
  IntegerSortField,
  JSONField,
  JavaScriptField,
  NumberField,
  ObjectField,
  SceneLevelsSetField,
  SchemaField,
  SetField,
  ShapesField,
  StringField,
  TypeDataField,
  TypedObjectField,
  TypedSchemaField
};
