import Document from "../abstract/document.mjs";
import {deepClone, isEmpty, mergeObject} from "../utils/helpers.mjs";
import * as fields from "../data/fields.mjs";
import Color from "../utils/color.mjs";
import {isValidId} from "../data/validators.mjs";
import BaseScene from "./scene.mjs";
import GridlessGrid from "../grid/gridless.mjs";
import BaseToken from "./token.mjs";

/**
 * @import {RegionData, UserData} from "./_types.mjs";
 * @import {DocumentPermissionTest} from "@common/abstract/_types.mjs";
 * @import BaseGrid from "@common/grid/base.mjs";
 */

/**
 * The Region Document.
 * Defines the DataSchema and common behaviors for a Region which are shared between both client and server.
 * @extends {Document<RegionData>}
 * @mixes RegionData
 * @category Documents
 */
export default class BaseRegion extends Document {

  /* -------------------------------------------- */
  /*  Model Configuration                         */
  /* -------------------------------------------- */

  /** @inheritdoc */
  static metadata = Object.freeze(mergeObject(super.metadata, {
    name: "Region",
    collection: "regions",
    label: "DOCUMENT.Region",
    labelPlural: "DOCUMENT.Regions",
    isEmbedded: true,
    embedded: {
      RegionBehavior: "behaviors"
    },
    permissions: {
      create: BaseRegion.#canCreate,
      update: BaseRegion.#canUpdate,
      delete: "OWNER"
    },
    schemaVersion: "14.361"
  }, {inplace: false}));

  /** @inheritdoc */
  static defineSchema() {
    const {BaseRegionBehavior} = foundry.documents;
    return {
      _id: new fields.DocumentIdField(),
      name: new fields.StringField({required: true, blank: false, textSearch: true}),
      color: new fields.ColorField({required: true, nullable: false,
        initial: () => Color.fromHSV([Math.random(), 0.8, 0.8]).css}),
      shapes: new fields.ShapesField(),
      elevation: new fields.SchemaField({
        bottom: new fields.NumberField({required: true}), // Treat null as -Infinity
        top: new fields.NumberField({required: true}), // Treat null as +Infinity
        topInclusive: new fields.BooleanField()
      }, {
        validate: d => (d.bottom ?? -Infinity) <= (d.top ?? Infinity),
        validationError: "elevation.top may not be less than elevation.bottom"
      }),
      levels: new fields.SceneLevelsSetField(),
      restriction: new fields.SchemaField({
        enabled: new fields.BooleanField(),
        type: new fields.StringField({required: true, choices: CONST.EDGE_RESTRICTION_TYPES, initial: "move"}),
        priority: new fields.NumberField({required: true, nullable: false, integer: true, initial: 0, min: 0})
      }),
      attachment: new fields.SchemaField({
        token: new fields.ForeignDocumentField(BaseToken, {idOnly: true, nullable: true, initial: null})
      }),
      behaviors: new fields.EmbeddedCollectionField(BaseRegionBehavior),
      visibility: new fields.NumberField({required: true,
        initial: CONST.REGION_VISIBILITY.LAYER_UNLOCKED,
        choices: Object.values(CONST.REGION_VISIBILITY)}),
      highlightMode: new fields.StringField({required: true, initial: "shapes", choices:
        {shapes: "REGION.HIGHLIGHT_MODES.shapes.label", coverage: "REGION.HIGHLIGHT_MODES.coverage.label"}}),
      displayMeasurements: new fields.BooleanField(),
      hidden: new fields.BooleanField(),
      locked: new fields.BooleanField(),
      ownership: new fields.DocumentOwnershipField(),
      flags: new fields.DocumentFlagsField(),
      /** @internal */
      _shapeConstraints: new fields.ArrayField(
        new fields.ArrayField(new fields.NumberField({required: true, nullable: false, initial: undefined})),
        {nullable: true, initial: null}
      )
    };
  }

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

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

  /** @override */
  static canUserCreate(user) {
    return user.hasPermission("REGION_CREATE");
  }

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

  /**
   * Is a User able to create a new Region?
   * The REGION_CREATE permission is required.
   * Only GMs can create Regions with Behaviors.
   * @type {DocumentPermissionTest}
   */
  static #canCreate(user, doc, data) {
    if ( !user.hasPermission("REGION_CREATE") ) return false;
    if ( isEmpty(data.behaviors) ) return true;
    if ( !user.isGM ) return false;
    if ( data.behaviors.some(b => b.type === "executeScript") && !user.hasPermission("MACRO_SCRIPT") ) return false;
    return true;
  }

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

  /**
   * Is a User able to update a Region?
   * The OWNER permission is required.
   * Only GMs can update Behaviors.
   * @type {DocumentPermissionTest}
   */
  static #canUpdate(user, doc, data) {
    if ( !doc.testUserPermission(user, "OWNER") ) return false;
    if ( isEmpty(data.behaviors) ) return true;
    if ( !user.isGM ) return false;
    if ( data.behaviors.some(b => b.type === "executeScript") && !user.hasPermission("MACRO_SCRIPT") ) return false;
    return true;
  }

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

  /** @override */
  getUserLevel(user) {
    user ||= game.user;
    if ( this.pack ) return this.compendium.getUserLevel(user);
    const level = this.ownership[user.id] ?? this.ownership.default ?? CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE;
    if ( level === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT ) return CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE;
    return level;
  }

  /* -------------------------------------------- */
  /*  Database Update Operations                  */
  /* -------------------------------------------- */

  /** @override */
  async _preUpdate(changes, options, user) {
    if ( !options._updateShapeConstraints?.[this.id] ) delete changes._shapeConstraints;
  }

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

  /**
   * Migrate MeasuredTemplate data to Region data.
   * @param {object} template                             The MeasuredTemplate data
   * @param {object} [context]                            The migration context
   * @param {BaseGrid} [context.grid]                     The grid
   * @param {boolean} [context.gridTemplates]             Grid-shaped?
   * @param {"round"|"flat"} [context.coneTemplateType]   The cone curvature
   * @param {UserData[]} [context.users]                  The users
   * @returns {RegionData}                                The Region data
   * @internal
   */
  static _migrateMeasuredTemplateData(template, {grid=BaseScene.defaultGrid, gridTemplates=false, coneTemplateType="round", users=[]}={}) {
    const authorId = isValidId(template.author) ? template.author : undefined;
    const authorName = authorId ? users.find(u => u._id === authorId)?.name : undefined;
    const t = template.t || "circle";
    const x = Math.round(template.x || 0);
    const y = Math.round(template.y || 0);
    const elevation = template.elevation || 0;
    const distance = Math.abs(template.distance || 0);
    const direction = Math.normalizeDegrees(template.direction || 0);
    const angle = Math.clamp(template.angle === null ? 90 : (template.angle || 0), 0, 360);
    const width = Math.abs(template.width || 0);
    const fillColor = template.fillColor || "#ff0000";
    const hidden = template.hidden || false;

    // Create the shape
    const gridBased = gridTemplates === true;
    if ( !gridBased && !grid.isGridless ) grid = new GridlessGrid({size: grid.size, distance: grid.distance});
    const distancePixels = grid.size / grid.distance;
    let shape;
    switch ( t ) {
      case "circle": {
        shape = {type: "circle", x, y, radius: distance * distancePixels, gridBased};
        break;
      }
      case "cone": {
        const curvature = gridBased || (coneTemplateType === "round") ? "round" : "flat";
        shape = {type: "cone", x, y, radius: distance * distancePixels, angle, rotation: direction, curvature, gridBased};
        break;
      }
      case "rect": {
        const {x: x1, y: y1} = grid.getTranslatedPoint({x, y}, direction, distance);
        let width = grid.measurePath([{x, y}, {x: x1, y}]).distance * distancePixels;
        let height = grid.measurePath([{x, y}, {x, y: y1}]).distance * distancePixels;
        const rotation = direction.toNearest(90, "floor");
        if ( (rotation === 90) || (rotation === 270) ) [width, height] = [height, width];
        shape = {type: "rectangle", x, y, width, height, anchorX: 0, anchorY: 0, rotation, gridBased};
        break;
      }
      case "ray": {
        shape = {type: "line", x, y, length: distance * distancePixels, width: width * distancePixels, rotation: direction, gridBased};
        break;
      }
    }

    // Create the Region data
    let name = `${shape.type.capitalize()} Template`;
    if ( authorName ) name += ` [${authorName}]`;
    const ownership = {default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE};
    if ( authorId ) ownership[authorId] = CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER;
    return {
      _id: template._id ?? null,
      name,
      color: fillColor,
      shapes: [shape],
      elevation: {bottom: elevation, top: null},
      levels: [],
      restriction: {enabled: false, type: "move", priority: 0},
      attachment: {token: null},
      behaviors: [],
      visibility: hidden ? CONST.REGION_VISIBILITY.OBSERVER : CONST.REGION_VISIBILITY.ALWAYS,
      highlightMode: "coverage",
      displayMeasurements: true,
      hidden: false,
      locked: false,
      ownership,
      flags: deepClone(template.flags || {})
    };
  }
}
