/**
 * The collection of data schema and document definitions for primary documents which are shared between the both the
 * client and the server.
 * @module data
 */

import DataModel from "../abstract/data.mjs";
import * as fields from "./fields.mjs";
import * as utils from "../utils/_module.mjs";

/**
 * @import EmbeddedCollectionDelta from "../abstract/embedded-collection-delta.mjs";
 * @import BaseActor from "../documents/actor.mjs";
 * @import {Point} from "@common/_types.mjs";
 * @import {GridOffset2D} from "@common/grid/_types.mjs";
 * @import {PrototypeTokenData} from "@common/documents/_types.mjs";
 * @import {TokenShapeType} from "@common/constants.mjs";
 * @import {TextureDataFitMode} from "../constants.mjs";
 * @import {DataFieldOptions, FilePathFieldOptions, LightAnimationData} from "./_types.mjs";
 */

/**
 * A reusable document structure for the internal data used to render the appearance of a light source.
 * This is re-used by both the AmbientLightData and TokenData classes.
 *
 * @property {boolean} negative           Is this light source a negative source? (i.e. darkness source)
 * @property {number} priority            The priority of this source
 * @property {number} alpha               An opacity for the emitted light, if any
 * @property {number} angle               The angle of emission for this point source
 * @property {number} bright              The allowed radius of bright vision or illumination
 * @property {number} color               A tint color for the emitted light, if any
 * @property {number} coloration          The coloration technique applied in the shader
 * @property {number} contrast            The amount of contrast this light applies to the background texture
 * @property {number} dim                 The allowed radius of dim vision or illumination
 * @property {number} attenuation         Fade the difference between bright, dim, and dark gradually?
 * @property {number} luminosity          The luminosity applied in the shader
 * @property {number} saturation          The amount of color saturation this light applies to the background texture
 * @property {number} shadows             The depth of shadows this light applies to the background texture
 * @property {LightAnimationData} animation  An animation configuration for the source
 * @property {{min: number, max: number}} darkness  A darkness range (min and max) for which the source should be active
 */
class LightData extends DataModel {
  static defineSchema() {
    return {
      negative: new fields.BooleanField(),
      priority: new fields.NumberField({required: true, nullable: false, integer: true, initial: 0, min: 0}),
      alpha: new fields.AlphaField({initial: 0.5}),
      angle: new fields.AngleField({initial: 360, normalize: false}),
      bright: new fields.NumberField({required: true, nullable: false, initial: 0, min: 0, step: 0.01}),
      color: new fields.ColorField({}),
      coloration: new fields.NumberField({required: true, integer: true, initial: 1}),
      dim: new fields.NumberField({required: true, nullable: false, initial: 0, min: 0, step: 0.01}),
      attenuation: new fields.AlphaField({initial: 0.5}),
      luminosity: new fields.NumberField({required: true, nullable: false, initial: 0.5, min: 0, max: 1}),
      saturation: new fields.NumberField({required: true, nullable: false, initial: 0, min: -1, max: 1}),
      contrast: new fields.NumberField({required: true, nullable: false, initial: 0, min: -1, max: 1}),
      shadows: new fields.NumberField({required: true, nullable: false, initial: 0, min: 0, max: 1}),
      animation: new fields.SchemaField({
        type: new fields.StringField({nullable: true, blank: false, initial: null}),
        speed: new fields.NumberField({required: true, nullable: false, integer: true, initial: 5, min: 0, max: 10,
          validationError: "Light animation speed must be an integer between 0 and 10"}),
        intensity: new fields.NumberField({required: true, nullable: false, integer: true, initial: 5, min: 1, max: 10,
          validationError: "Light animation intensity must be an integer between 1 and 10"}),
        reverse: new fields.BooleanField()
      }),
      darkness: new fields.SchemaField({
        min: new fields.AlphaField({initial: 0, placeholder: "0"}),
        max: new fields.AlphaField({initial: 1, placeholder: "1"})
      }, {
        validate: d => (d.min ?? 0) <= (d.max ?? 1),
        validationError: "darkness.max may not be less than darkness.min"
      })
    };
  }

  /** @override */
  static LOCALIZATION_PREFIXES = ["LIGHT"];
}

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

/**
 * A data model intended to be used as an inner EmbeddedDataField which defines a geometric shape.
 *
 * @property {string} type                The type of shape, a value in ShapeData.TYPES.
 *                                        For rectangles, the x/y coordinates are the top-left corner.
 *                                        For circles, the x/y coordinates are the center of the circle.
 *                                        For polygons, the x/y coordinates are the first point of the polygon.
 * @property {number|null} width          For rectangles, the pixel width of the shape.
 * @property {number|null} height         For rectangles, the pixel width of the shape.
 * @property {number|null} radius         For circles, the pixel radius of the shape.
 * @property {number[]} [points]          For polygons, the array of polygon coordinates which comprise the shape.
 */
class ShapeData extends DataModel {
  static defineSchema() {
    return {
      type: new fields.StringField({required: true, blank: false, choices: Object.values(this.TYPES), initial: "r"}),
      width: new fields.NumberField({required: true, integer: true, min: 0}),
      height: new fields.NumberField({required: true, integer: true, min: 0}),
      radius: new fields.NumberField({required: true, integer: true, positive: true}),
      points: new fields.ArrayField(new fields.NumberField({required: true, nullable: false}))
    };
  }

  /**
   * The primitive shape types which are supported
   * @enum {string}
   */
  static TYPES = {
    RECTANGLE: "r",
    CIRCLE: "c",
    ELLIPSE: "e",
    POLYGON: "p"
  };
}

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

/**
 * A data model intended to be used as an inner EmbeddedDataField which defines a geometric shape.
 * @abstract
 *
 * @property {string} type             The type of shape, a value in BaseShapeData.TYPES
 * @property {boolean} hole            Is this shape a hole?
 * @property {number} rotation         The rotation
 */
class BaseShapeData extends DataModel {
  constructor(data, options) {
    super(data, options);
    this.rotation ??= 0;
  }

  /** @override */
  static LOCALIZATION_PREFIXES = ["SHAPE.TYPES.base"];

  /**
   * The possible shape types.
   * @type {Readonly<{
   *   rectangle: foundry.data.RectangleShapeData;
   *   circle: foundry.data.CircleShapeData;
   *   ellipse: foundry.data.EllipseShapeData;
   *   emanation: foundry.data.EmanationShapeData;
   *   cone: foundry.data.ConeShapeData;
   *   ring: foundry.data.RingShapeData;
   *   line: foundry.data.LineShapeData;
   *   polygon: foundry.data.PolygonShapeData;
   *   token: foundry.data.TokenShapeData;
   *   grid: foundry.data.GridShapeData;
   * }>}
   */
  static get TYPES() {
    return BaseShapeData.#TYPES ??= Object.freeze({
      [RectangleShapeData.TYPE]: foundry.data.RectangleShapeData,
      [CircleShapeData.TYPE]: foundry.data.CircleShapeData,
      [EllipseShapeData.TYPE]: foundry.data.EllipseShapeData,
      [EmanationShapeData.TYPE]: foundry.data.EmanationShapeData,
      [ConeShapeData.TYPE]: foundry.data.ConeShapeData,
      [RingShapeData.TYPE]: foundry.data.RingShapeData,
      [LineShapeData.TYPE]: foundry.data.LineShapeData,
      [PolygonShapeData.TYPE]: foundry.data.PolygonShapeData,
      [TokenShapeData.TYPE]: foundry.data.TokenShapeData,
      [GridShapeData.TYPE]: foundry.data.GridShapeData
    });
  }

  static #TYPES;

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

  /**
   * The type of this shape.
   * @type {string}
   */
  static TYPE = "";

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

  /** @override */
  static defineSchema() {
    return {
      type: new fields.StringField({required: true, blank: false, initial: this.TYPE,
        validate: value => value === this.TYPE, validationError: `must be equal to "${this.TYPE}"`}),
      hole: new fields.BooleanField()
    };
  }

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

  /**
   * The index of this shape within the array of shapes in its parent.
   * See {@link foundry.data.fields.ShapesField}.
   * @type {number|undefined}
   * @internal
   */
  _index;
}

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

/**
 * The data model for a rectangle shape.
 *
 * @property {number} x               The x-coordinate of the origin in pixels.
 * @property {number} y               The y-coordinate of the origin in pixels.
 * @property {number} width           The width of the rectangle in pixels.
 * @property {number} height          The height of the rectangle in pixels.
 * @property {number} anchorX         The x-coordinate of the anchor.
 * @property {number} anchorY         The y-coordinate of the anchor.
 * @property {number} rotation        The rotation around the origin of the rectangle in degrees.
 * @property {boolean} gridBased      If the shape is grid-based, its dimensions are converted into grid units
 *                                    by dividing each by the grid size and multiplying by the grid distance. The shape
 *                                    is then constructed using these dimensions conforming to the grid's metric.
 */
class RectangleShapeData extends BaseShapeData {

  /** @override */
  static LOCALIZATION_PREFIXES = ["SHAPE.TYPES.rectangle", "SHAPE.TYPES.base"];

  static {
    Object.defineProperty(this, "TYPE", {value: "rectangle"});
  }

  /** @inheritdoc */
  static defineSchema() {
    return Object.assign(super.defineSchema(), {
      x: new fields.NumberField({required: true, nullable: false, initial: undefined}),
      y: new fields.NumberField({required: true, nullable: false, initial: undefined}),
      width: new fields.NumberField({required: true, nullable: false, initial: undefined, min: 0}),
      height: new fields.NumberField({required: true, nullable: false, initial: undefined, min: 0}),
      anchorX: new fields.NumberField({required: true, nullable: false, initial: 0}),
      anchorY: new fields.NumberField({required: true, nullable: false, initial: 0}),
      rotation: new fields.AngleField(),
      gridBased: new fields.BooleanField()
    });
  }
}

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

/**
 * The data model for a circle shape.
 *
 * @property {number} x               The x-coordinate of the center point in pixels.
 * @property {number} y               The y-coordinate of the center point in pixels.
 * @property {number} radius          The radius of the circle in pixels.
 * @property {boolean} gridBased      If the shape is grid-based, its dimensions are converted into grid units
 *                                    by dividing each by the grid size and multiplying by the grid distance. The shape
 *                                    is then constructed using these dimensions conforming to the grid's metric.
 */
class CircleShapeData extends BaseShapeData {

  /** @override */
  static LOCALIZATION_PREFIXES = ["SHAPE.TYPES.circle", "SHAPE.TYPES.base"];

  static {
    Object.defineProperty(this, "TYPE", {value: "circle"});
  }

  /** @inheritdoc */
  static defineSchema() {
    return Object.assign(super.defineSchema(), {
      x: new fields.NumberField({required: true, nullable: false, initial: undefined}),
      y: new fields.NumberField({required: true, nullable: false, initial: undefined}),
      radius: new fields.NumberField({required: true, nullable: false, initial: undefined, min: 0}),
      gridBased: new fields.BooleanField()
    });
  }
}

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

/**
 * The data model for an ellipse shape.
 *
 * @property {number} x               The x-coordinate of the center point in pixels.
 * @property {number} y               The y-coordinate of the center point in pixels.
 * @property {number} radiusX         The x-radius of the circle in pixels.
 * @property {number} radiusY         The y-radius of the circle in pixels.
 * @property {number} rotation        The rotation around the center of the rectangle in degrees.
 * @property {boolean} gridBased      If the shape is grid-based, its dimensions are converted into grid units
 *                                    by dividing each by the grid size and multiplying by the grid distance. The shape
 *                                    is then constructed using these dimensions conforming to the grid's metric.
 */
class EllipseShapeData extends BaseShapeData {

  /** @override */
  static LOCALIZATION_PREFIXES = ["SHAPE.TYPES.ellipse", "SHAPE.TYPES.base"];

  static {
    Object.defineProperty(this, "TYPE", {value: "ellipse"});
  }

  /** @inheritdoc */
  static defineSchema() {
    return Object.assign(super.defineSchema(), {
      x: new fields.NumberField({required: true, nullable: false, initial: undefined}),
      y: new fields.NumberField({required: true, nullable: false, initial: undefined}),
      radiusX: new fields.NumberField({required: true, nullable: false, initial: undefined, min: 0}),
      radiusY: new fields.NumberField({required: true, nullable: false, initial: undefined, min: 0}),
      rotation: new fields.AngleField(),
      gridBased: new fields.BooleanField()
    });
  }
}

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

/**
 * The data model for an emanation shape.
 *
 * @property {BaseShapeData} base     The base shape of the emanation.
 * @property {number} radius          The radius of the emanation in pixels.
 * @property {boolean} gridBased      If the shape is grid-based, its dimensions are converted into grid units
 *                                    by dividing each by the grid size and multiplying by the grid distance. The shape
 *                                    is then constructed using these dimensions conforming to the grid's metric.
 */
class EmanationShapeData extends BaseShapeData {

  /** @override */
  static LOCALIZATION_PREFIXES = ["SHAPE.TYPES.emanation", "SHAPE.TYPES.base"];

  static {
    Object.defineProperty(this, "TYPE", {value: "emanation"});
  }

  /** @inheritdoc */
  static defineSchema() {
    const {emanation: _emanation, ring: _ring, ...baseTypes} = BaseShapeData.TYPES;
    return Object.assign(super.defineSchema(), {
      base: new fields.TypedSchemaField(baseTypes),
      radius: new fields.NumberField({required: true, nullable: false, min: 0, initial: undefined}),
      gridBased: new fields.BooleanField()
    });
  }
}

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

/**
 * The data model for a cone shape.
 *
 * @property {number} x               The x-coordinate of the center point in pixels.
 * @property {number} y               The y-coordinate of the center point in pixels.
 * @property {number} radius          The radius of the cone in pixels.
 * @property {number} angle           The angle of the cone in degrees.
 * @property {number} rotation        The direction of the cone in degrees.
 * @property {"round"|"flat"|"semicircle"} curvature    The curvature.
 * @property {boolean} gridBased      If the shape is grid-based, its dimensions are converted into grid units
 *                                    by dividing each by the grid size and multiplying by the grid distance. The shape
 *                                    is then constructed using these dimensions conforming to the grid's metric.
 */
class ConeShapeData extends BaseShapeData {

  /** @override */
  static LOCALIZATION_PREFIXES = ["SHAPE.TYPES.cone", "SHAPE.TYPES.base"];

  static {
    Object.defineProperty(this, "TYPE", {value: "cone"});
  }

  /** @inheritdoc */
  static defineSchema() {
    return Object.assign(super.defineSchema(), {
      x: new fields.NumberField({required: true, nullable: false, initial: undefined}),
      y: new fields.NumberField({required: true, nullable: false, initial: undefined}),
      radius: new fields.NumberField({required: true, nullable: false, initial: undefined, min: 0}),
      angle: new fields.AngleField({initial: undefined, min: 0, normalize: false}),
      rotation: new fields.AngleField(),
      curvature: new fields.StringField({required: true, initial: "round", choices: {
        round: "SHAPE.TYPES.cone.CURVATURES.round.label",
        flat: "SHAPE.TYPES.cone.CURVATURES.flat.label",
        semicircle: "SHAPE.TYPES.cone.CURVATURES.semicircle.label"
      }}),
      gridBased: new fields.BooleanField()
    });
  }

  /** @override */
  static validateJoint(data) {
    switch ( data.curvature ) {
      case "flat":
        if ( data.angle > 90 ) throw new Error("The angle of a cone with a flat curvature must not be greater than 90 degrees");
        break;
      case "semicircle":
        if ( data.angle > 180 ) throw new Error("The angle of a cone with a semicircle curvature cannot be greater than 180 degrees");
        break;
    }
  }
}

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

/**
 * The data model for a ring shape.
 *
 * @property {number} x               The x-coordinate of the origin in pixels.
 * @property {number} y               The y-coordinate of the origin in pixels.
 * @property {number} radius          The radius of the ring in pixels.
 * @property {number} innerWidth      The inner width of the ring in pixels.
 * @property {number} outerWidth      The inner width of the ring in pixels.
 * @property {boolean} gridBased      If the shape is grid-based, its dimensions are converted into grid units
 *                                    by dividing each by the grid size and multiplying by the grid distance. The shape
 *                                    is then constructed using these dimensions conforming to the grid's metric.
 */
class RingShapeData extends BaseShapeData {

  /** @override */
  static LOCALIZATION_PREFIXES = ["SHAPE.TYPES.ring", "SHAPE.TYPES.base"];

  static {
    Object.defineProperty(this, "TYPE", {value: "ring"});
  }

  /** @inheritdoc */
  static defineSchema() {
    return Object.assign(super.defineSchema(), {
      x: new fields.NumberField({required: true, nullable: false, initial: undefined}),
      y: new fields.NumberField({required: true, nullable: false, initial: undefined}),
      radius: new fields.NumberField({required: true, nullable: false, initial: undefined, min: 0}),
      innerWidth: new fields.NumberField({required: true, nullable: false, initial: undefined, min: 0}),
      outerWidth: new fields.NumberField({required: true, nullable: false, initial: undefined, min: 0}),
      gridBased: new fields.BooleanField()
    });
  }
}

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

/**
 * The data model for a line shape.
 *
 * @property {number} x               The x-coordinate of the origin in pixels.
 * @property {number} y               The y-coordinate of the origin in pixels.
 * @property {number} length          The length of the line in pixels.
 * @property {number} width           The width of the line in pixels.
 * @property {number} rotation        The rotation around the origin of the line in degrees.
 * @property {boolean} gridBased      If the shape is grid-based, its dimensions are converted into grid units
 *                                    by dividing each by the grid size and multiplying by the grid distance. The shape
 *                                    is then constructed using these dimensions conforming to the grid's metric.
 */
class LineShapeData extends BaseShapeData {

  /** @override */
  static LOCALIZATION_PREFIXES = ["SHAPE.TYPES.line", "SHAPE.TYPES.base"];

  static {
    Object.defineProperty(this, "TYPE", {value: "line"});
  }

  /** @inheritdoc */
  static defineSchema() {
    return Object.assign(super.defineSchema(), {
      x: new fields.NumberField({required: true, nullable: false, initial: undefined}),
      y: new fields.NumberField({required: true, nullable: false, initial: undefined}),
      length: new fields.NumberField({required: true, nullable: false, initial: undefined, min: 0}),
      width: new fields.NumberField({required: true, nullable: false, initial: undefined, min: 0}),
      rotation: new fields.AngleField(),
      gridBased: new fields.BooleanField()
    });
  }
}

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

/**
 * The data model for a polygon shape.
 *
 * @property {number[]} points      The points of the polygon ([x0, y0, x1, y1, ...]).
 *                                  The polygon must not be self-intersecting if it is supposed to be filled.
 *                                  The polygon must not contained zero-length edges except for
 *                                  the edge from the last to the first point.
 * @property {Point|null} origin    The origin of the polygon. If null, it defaults to the center-of-mass.
 */
class PolygonShapeData extends BaseShapeData {

  /** @override */
  static LOCALIZATION_PREFIXES = ["SHAPE.TYPES.polygon", "SHAPE.TYPES.base"];

  static {
    Object.defineProperty(this, "TYPE", {value: "polygon"});
  }

  /** @inheritdoc */
  static defineSchema() {
    return Object.assign(super.defineSchema(), {
      points: new fields.ArrayField(new fields.NumberField({required: true, nullable: false, initial: undefined}), {
        min: 4, validate: value => {
          if ( value.length % 2 !== 0 ) throw new Error("must have an even length");
        }}),
      origin: new fields.SchemaField({
        x: new fields.NumberField({required: true, nullable: false, initial: undefined}),
        y: new fields.NumberField({required: true, nullable: false, initial: undefined})
      }, {nullable: true})
    });
  }
}

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

/**
 * The data model for a token shape.
 *
 * @property {number} x                  The top-left x-coordinate in pixels (integer).
 * @property {number} y                  The top-left y-coordinate in pixels (integer).
 * @property {number} width              The width in grid spaces (positive).
 * @property {number} height             The height in grid spaces (positive).
 * @property {TokenShapeType} shape      The shape type (see {@link CONST.TOKEN_SHAPES}).
 */
class TokenShapeData extends BaseShapeData {

  /** @override */
  static LOCALIZATION_PREFIXES = ["SHAPE.TYPES.token", "SHAPE.TYPES.base"];

  static {
    Object.defineProperty(this, "TYPE", {value: "token"});
  }

  /** @inheritdoc */
  static defineSchema() {
    return Object.assign(super.defineSchema(), {
      x: new fields.NumberField({required: true, nullable: false, integer: true, initial: undefined}),
      y: new fields.NumberField({required: true, nullable: false, integer: true, initial: undefined}),
      width: new fields.NumberField({required: true, nullable: false, positive: true, initial: undefined}),
      height: new fields.NumberField({required: true, nullable: false, positive: true, initial: undefined}),
      shape: new fields.NumberField({required: true, choices: Object.entries(CONST.TOKEN_SHAPES)
        .reduce((shapes, [key, value]) => {
          shapes[value] = `TOKEN.SHAPES.${key}.label`;
          return shapes;
        }, {}), initial: undefined})
    });
  }
}

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

/**
 * The data model for a shape that is the union of grid spaces.
 *
 * @property {GridOffset2D[]} offsets    The grid offsets covered by this shape
 * @property {Point|null} origin         The optional grid space origin, which is by default
 *                                       the center point of the first grid space in `offsets`
 */
class GridShapeData extends BaseShapeData {

  /** @override */
  static LOCALIZATION_PREFIXES = ["SHAPE.TYPES.grid", "SHAPE.TYPES.base"];

  static {
    Object.defineProperty(this, "TYPE", {value: "grid"});
  }

  /** @inheritdoc */
  static defineSchema() {
    return Object.assign(super.defineSchema(), {
      offsets: new fields.GridOffsetsField(),
      origin: new fields.SchemaField({
        x: new fields.NumberField({required: true, nullable: false, initial: undefined}),
        y: new fields.NumberField({required: true, nullable: false, initial: undefined})
      }, {nullable: true})
    });
  }
}

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

/**
 * A {@link foundry.data.fields.SchemaField} subclass used to represent texture data.
 * @property {string|null} src            The URL of the texture source.
 * @property {number} anchorX             The X coordinate of the texture anchor.
 * @property {number} anchorY             The Y coordinate of the texture anchor.
 * @property {number} scaleX              The scale of the texture in the X dimension.
 * @property {number} scaleY              The scale of the texture in the Y dimension.
 * @property {TextureDataFitMode} fit     The texture fit mode.
 * @property {string} tint                The tint applied to the texture.
 * @property {number} alphaThreshold      Only pixels with an alpha value at or above this value are consider solid
 *                                        w.r.t. to occlusion testing and light/weather blocking.
 */
class TextureData extends fields.SchemaField {
  /**
   * @param {DataFieldOptions} options        Options which are forwarded to the SchemaField constructor
   * @param {Pick<FilePathFieldOptions, "categories"|"initial"|"wildcard"|"label">} srcOptions
   *                                          Additional options for the src field
   */
  constructor(options={}, {categories=["IMAGE", "VIDEO"], initial={}, wildcard=false, label=""}={}) {
    super({
      src: new fields.FilePathField({required: true, categories, initial: initial.src ?? null, label,
        virtual: !wildcard, wildcard}),
      anchorX: new fields.NumberField({required: true, nullable: false, initial: initial.anchorX ?? 0}),
      anchorY: new fields.NumberField({required: true, nullable: false, initial: initial.anchorY ?? 0}),
      fit: new fields.StringField({required: true, initial: initial.fit ?? "fill", choices: CONST.TEXTURE_DATA_FIT_MODES}),
      scaleX: new fields.NumberField({required: true, nullable: false, initial: initial.scaleX ?? 1}),
      scaleY: new fields.NumberField({required: true, nullable: false, initial: initial.scaleY ?? 1}),
      tint: new fields.ColorField({required: true, nullable: false, initial: initial.tint ?? "#ffffff"}),
      alphaThreshold: new fields.AlphaField({nullable: false, initial: initial.alphaThreshold ?? 0})
    }, options);
  }
}

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

/**
 * Extend the base TokenData to define a PrototypeToken which exists within a parent Actor.
 * @extends DataModel<PrototypeTokenData>
 * @property {boolean} randomImg      Does the prototype token use a random wildcard image?
 */
class PrototypeToken extends DataModel {
  constructor(data={}, options={}) {
    super(data, options);
    Object.defineProperty(this, "apps", {value: {}});
  }

  /** @override */
  static defineSchema() {
    const included = new Set(["name", "displayName", "actorLink", "width", "height", "depth", "texture", "lockRotation",
      "rotation", "alpha", "disposition", "displayBars", "bar1", "bar2", "light", "sight", "detectionModes",
      "occludable", "ring", "turnMarker", "movementAction", "flags"]);
    const schema = foundry.documents.BaseToken.defineSchema();
    for ( const fieldName in schema ) {
      if ( !included.has(fieldName) ) delete schema[fieldName];
    }
    schema.name.textSearch = schema.name.options.textSearch = false;
    schema.randomImg = new fields.BooleanField();
    schema.appendNumber = new fields.BooleanField();
    schema.prependAdjective = new fields.BooleanField();
    return schema;
  }

  /** @override */
  static LOCALIZATION_PREFIXES = ["DOCUMENT", "TOKEN"];

  /**
   * The Actor which owns this Prototype Token
   * @type {BaseActor}
   */
  get actor() {
    return this.parent;
  }

  /** @ignore */
  static get database() {
    return globalThis.CONFIG.DatabaseBackend;
  }

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

  /** @inheritDoc */
  _initializeSource(data, options={}) {
    if ( data instanceof PrototypeToken ) data = data.toObject();
    PrototypeTokenOverrides.applyOverrides(data, this.parent?.type);
    return super._initializeSource(data, options);
  }

  /* -------------------------------------------- */
  /*  Document Compatibility Methods              */
  /* -------------------------------------------- */

  /**
   * @see {@link foundry.abstract.Document#update}
   * @ignore
   */
  update(data, options) {
    return this.actor.update({prototypeToken: data}, options);
  }

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

  /**
   * @see {@link foundry.abstract.Document#getFlag}
   * @ignore
   */
  getFlag(...args) {
    return foundry.abstract.Document.prototype.getFlag.call(this, ...args);
  }

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

  /**
   * @see {@link foundry.abstract.Document#getFlag}
   * @ignore
   */
  setFlag(...args) {
    return foundry.abstract.Document.prototype.setFlag.call(this, ...args);
  }

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

  /**
   * @see {@link foundry.abstract.Document#unsetFlag}
   * @ignore
   */
  async unsetFlag(...args) {
    return foundry.abstract.Document.prototype.unsetFlag.call(this, ...args);
  }

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

  /**
   * @see {@link foundry.abstract.Document#testUserPermission}
   * @ignore
   */
  testUserPermission(user, permission, {exact=false}={}) {
    return this.actor.testUserPermission(user, permission, {exact});
  }

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

  /**
   * @see {@link foundry.documents.BaseActor#isOwner}
   * @ignore
   */
  get isOwner() {
    return this.actor.isOwner;
  }
}

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

/**
 * The data model for the the core.prototypeTokenOverrides setting.
 */
class PrototypeTokenOverrides extends DataModel {
  /** @override */
  static defineSchema() {
    const labelFor = n => `TOKEN.FIELDS.${n}.label`;
    return getDocumentClass("Actor").TYPES.reduce((types, type) => {
      const prototypeSchema = PrototypeToken.defineSchema();

      // Select a subset of the prototype schema, make all non-recursive fields nullable, and set an initial of null.
      const subSchema = {
        sight: new fields.SchemaField({
          enabled: new fields.BooleanField({required: false, initial: undefined, label: labelFor("sight.enabled")})
        }),
        ring: new fields.SchemaField({
          enabled: new fields.BooleanField({required: false, initial: undefined, label: labelFor("ring.enabled")})
        }),
        turnMarker: prototypeSchema.turnMarker
      };
      for ( const fieldName of ["displayName", "displayBars", "disposition", "lockRotation"] ) {
        const field = prototypeSchema[fieldName];
        field.label = labelFor(fieldName);
        field.required = false;
        field.initial = undefined;
        subSchema[fieldName] = field;
      }
      for (const field of Object.values(subSchema.turnMarker.fields)) {
        field.label = labelFor(`turnMarker.${field.name}`);
        field.required = false;
        field.initial = undefined;
      }
      types[type] = new fields.SchemaField(subSchema);
      return types;
    }, {});
  }

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

  /**
   * Localize all non-recursive data fields on first load of the application.
   * @param {fields.DataField[]} [fields] Subfields of a recursive field
   * @param {Record<string, string>} [cache] A running cache of localization results
   */
  static localizeFields(fields, cache={}) {
    const Cls = PrototypeTokenOverrides;
    if ( Cls.#localized ) return;
    fields ??= Object.values(Cls.schema.fields).flatMap(f => Object.values(f.fields));
    for ( const field of fields ) {
      if ( field instanceof foundry.data.fields.SchemaField ) {
        Cls.localizeFields(Object.values(field.fields, cache));
        continue;
      }
      field.label = (cache[field.label] ??= _loc(field.label));
    }
    if ( !fields ) Cls.#localized = true;
  }

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

  /** @override */
  static LOCALIZATION_PREFIXES = ["TOKEN"];

  /**
   * The named of the world setting that stores the prototype token overrides
   * @type {"prototypeTokenOverrides"}
   */
  static SETTING = "prototypeTokenOverrides";

  /**
   * A cached copy of the currently-configured overrides
   * @returns {PrototypeTokenOverrides}
   */
  static get overrides() {
    return this.#overrides ??= game.settings.get("core", PrototypeTokenOverrides.SETTING);
  }

  /**
   * Set or clear the cached overrides.
   * @param {PrototypeTokenOverrides|null} value
   */
  static set overrides(value) {
    this.#overrides = value;
  }

  /**
   * @type {PrototypeTokenOverrides|null}
   */
  static #overrides = null;

  /** Have the fields been localized? Done later since this model may be initialized before i18n is ready. */
  static #localized = false;

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

  /**
   * Apply configured overrides to prototype token data.
   * @param {object} source The prototype token source data on which to operate
   * @param {string} [actorType] The prototype parent's actor type: used to retrieve type-specific overrides
   */
  static applyOverrides(source, actorType) {
    if ( !game.settings || !game.model ) return; // Server-side or Setup
    const setting = PrototypeTokenOverrides.overrides;
    const global = utils.flattenObject(setting.base);
    const typed = utils.flattenObject(setting[actorType] ?? {});
    for ( const [path, baseValue] of Object.entries(global) ) {
      const override = (typed[path] === undefined) ? baseValue : typed[path];
      if ( override !== undefined ) utils.setProperty(source, path, override);
    }
  }

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

  /**
   * Apply configured overrides to all Actor documents within the World.
   */
  static applyAll() {
    PrototypeTokenOverrides.#overrides = null;
    for ( const actor of game.actors ) {
      this.applyOverrides(actor.prototypeToken._source, actor.type);
      actor.prototypeToken.reset();
      actor.render();
    }
  }
}

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

/**
 * A minimal data model used to represent a tombstone entry inside an {@link foundry.abstract.EmbeddedCollectionDelta}.
 *
 * @property {string} _id              The _id of the base Document that this tombstone represents.
 * @property {boolean} _tombstone      A property that identifies this entry as a tombstone.
 */
class TombstoneData extends DataModel {
  /** @override */
  static defineSchema() {
    return {
      _id: new fields.DocumentIdField(),
      _tombstone: new fields.BooleanField({initial: true, validate: v => v === true, validationError: "must be true"})
    };
  }
}

// Exports need to be at the bottom so that class names appear correctly in JSDoc
export {
  LightData,
  PrototypeToken,
  PrototypeTokenOverrides,
  ShapeData,
  BaseShapeData,
  RectangleShapeData,
  CircleShapeData,
  EllipseShapeData,
  EmanationShapeData,
  ConeShapeData,
  RingShapeData,
  LineShapeData,
  PolygonShapeData,
  TokenShapeData,
  GridShapeData,
  TextureData,
  TombstoneData
};

export {default as ActiveEffectTypeDataModel} from "./active-effect.mjs";
