import Document from "../abstract/document.mjs";
import {mergeObject} from "../utils/helpers.mjs";
import * as CONST from "../constants.mjs";
import * as fields from "../data/fields.mjs";
import GridlessGrid from "../grid/gridless.mjs";
import SquareGrid from "../grid/square.mjs";
import HexagonalGrid from "../grid/hexagonal.mjs";

/**
 * @import BaseLevel from "./level.mjs";
 */

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

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

  /** @inheritdoc */
  static metadata = Object.freeze(mergeObject(super.metadata, {
    name: "Scene",
    collection: "scenes",
    indexed: true,
    compendiumIndexFields: ["_id", "name", "thumb", "sort", "folder"],
    embedded: {
      AmbientLight: "lights",
      AmbientSound: "sounds",
      Drawing: "drawings",
      Note: "notes",
      Region: "regions",
      Level: "levels",
      Tile: "tiles",
      Token: "tokens",
      Wall: "walls"
    },
    label: "DOCUMENT.Scene",
    labelPlural: "DOCUMENT.Scenes",
    preserveOnImport: [...super.metadata.preserveOnImport, "active"],
    defaultLevelId: "defaultLevel0000",
    schemaVersion: "14.354"
  }, {inplace: false}));

  /** @inheritDoc */
  static defineSchema() {
    const documents = foundry.documents;
    // Define reusable ambience schema for environment
    const environmentData = init => new fields.SchemaField({
      hue: new fields.HueField({required: true, initial: init.hue}),
      intensity: new fields.AlphaField({required: true, nullable: false, initial: init.intensity}),
      luminosity: new fields.NumberField({required: true, nullable: false, initial: init.luminosity, min: -1, max: 1}),
      saturation: new fields.NumberField({required: true, nullable: false, initial: init.saturation, min: -1, max: 1}),
      shadows: new fields.NumberField({required: true, nullable: false, initial: init.shadows, min: 0, max: 1})
    });
    // Reuse parts of the LightData schema for the global light
    const lightDataSchema = foundry.data.LightData.defineSchema();

    return {
      _id: new fields.DocumentIdField(),
      name: new fields.StringField({required: true, blank: false, textSearch: true}),

      // Navigation
      active: new fields.BooleanField(),
      navigation: new fields.BooleanField({initial: true}),
      navOrder: new fields.NumberField({required: true, nullable: false, integer: true, initial: 0}),
      navName: new fields.StringField({textSearch: true}),

      // Canvas Dimensions
      thumb: new fields.FilePathField({categories: ["IMAGE"]}),
      width: new fields.NumberField({integer: true, positive: true, initial: 4000}),
      height: new fields.NumberField({integer: true, positive: true, initial: 3000}),
      padding: new fields.NumberField({required: true, nullable: false, min: 0, max: 0.5, step: 0.05, initial: 0.25}),
      shiftX: new fields.NumberField({required: true, integer: true, initial: 0}),
      shiftY: new fields.NumberField({required: true, integer: true, initial: 0}),
      initial: new fields.SchemaField({
        x: new fields.NumberField({integer: true, required: true}),
        y: new fields.NumberField({integer: true, required: true}),
        scale: new fields.NumberField({required: true, positive: true, step: .001})
      }),
      initialLevel: new fields.DocumentIdField({readonly: false}),

      // Grid Configuration
      grid: new fields.SchemaField({
        type: new fields.NumberField({required: true, choices: Object.values(CONST.GRID_TYPES),
          initial: () => game.system.grid.type, validationError: "must be a value in CONST.GRID_TYPES"}),
        size: new fields.NumberField({required: true, nullable: false, integer: true, min: CONST.GRID_MIN_SIZE,
          initial: 100, validationError: `must be an integer number of pixels, ${CONST.GRID_MIN_SIZE} or greater`}),
        style: new fields.StringField({required: true, blank: false, initial: "solidLines"}),
        thickness: new fields.NumberField({required: true, nullable: false, positive: true, integer: true, initial: 1}),
        color: new fields.ColorField({required: true, nullable: false, initial: "#000000"}),
        alpha: new fields.AlphaField({initial: 0.2}),
        distance: new fields.NumberField({required: true, nullable: false, positive: true,
          initial: () => game.system.grid.distance}),
        units: new fields.StringField({required: true, initial: () => game.system.grid.units})
      }),

      // Vision Configuration
      tokenVision: new fields.BooleanField({initial: true}),
      fog: new fields.SchemaField({
        mode: new fields.NumberField({required: true, choices: Object.values(CONST.FOG_EXPLORATION_MODES),
          initial: CONST.FOG_EXPLORATION_MODES.INDIVIDUAL, validationError: "must be a value in CONST.FOG_EXPLORATION_MODES"}),
        reset: new fields.NumberField({required: false, initial: undefined}),
        colors: new fields.SchemaField({
          explored: new fields.ColorField(),
          unexplored: new fields.ColorField()
        })
      }),

      // Environment Configuration
      environment: new fields.SchemaField({
        darknessLevel: new fields.AlphaField({initial: 0}),
        darknessLock: new fields.BooleanField({initial: false}),
        globalLight: new fields.SchemaField({
          enabled: new fields.BooleanField({required: true, initial: false}),
          alpha: lightDataSchema.alpha,
          bright: new fields.BooleanField({required: true, initial: false}),
          color: lightDataSchema.color,
          coloration: lightDataSchema.coloration,
          luminosity: new fields.NumberField({required: true, nullable: false, initial: 0, min: 0, max: 1}),
          saturation: lightDataSchema.saturation,
          contrast: lightDataSchema.contrast,
          shadows: lightDataSchema.shadows,
          darkness: lightDataSchema.darkness
        }),
        cycle: new fields.BooleanField({initial: true}),
        base: environmentData({hue: 0, intensity: 0, luminosity: 0, saturation: 0, shadows: 0}),
        dark: environmentData({hue: 257/360, intensity: 0, luminosity: -0.25, saturation: 0, shadows: 0})
      }),

      // Transition Configuration
      transition: new fields.SchemaField({
        type: new fields.StringField({required: true, nullable: true, blank: false, initial: null}),
        duration: new fields.NumberField({required: true, nullable: false, integer: true,
          initial: 1500, min: 500, max: 10000, step: 100}),
        activeOnly: new fields.BooleanField()
      }),

      // Embedded Collections
      drawings: new fields.EmbeddedCollectionField(documents.BaseDrawing),
      tokens: new fields.EmbeddedCollectionField(documents.BaseToken),
      levels: new fields.EmbeddedCollectionField(documents.BaseLevel),
      lights: new fields.EmbeddedCollectionField(documents.BaseAmbientLight),
      notes: new fields.EmbeddedCollectionField(documents.BaseNote),
      sounds: new fields.EmbeddedCollectionField(documents.BaseAmbientSound),
      regions: new fields.EmbeddedCollectionField(documents.BaseRegion),
      tiles: new fields.EmbeddedCollectionField(documents.BaseTile),
      walls: new fields.EmbeddedCollectionField(documents.BaseWall),

      // Linked Documents
      playlist: new fields.ForeignDocumentField(documents.BasePlaylist),
      playlistSound: new fields.ForeignDocumentField(documents.BasePlaylistSound, {idOnly: true}),
      journal: new fields.ForeignDocumentField(documents.BaseJournalEntry),
      journalEntryPage: new fields.ForeignDocumentField(documents.BaseJournalEntryPage, {idOnly: true}),
      weather: new fields.StringField({required: true}),

      // Permissions
      folder: new fields.ForeignDocumentField(documents.BaseFolder),
      sort: new fields.IntegerSortField(),
      ownership: new fields.DocumentOwnershipField(),
      flags: new fields.DocumentFlagsField(),
      _stats: new fields.DocumentStatsField()
    };
  }

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

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

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

  /**
   * A mapping of top-level scene properties to their corresponding properties on the child level.
   * @type {Readonly<string[][]>}
   * @internal
   */
  static _LEVELS_PROPERTY_MAP = Object.freeze([
    ["background.src", "background.src"],
    ["background.tint", "background.tint"],
    ["background.alphaThreshold", "background.alphaThreshold"],
    ["background.anchorX", "textures.anchorX"],
    ["background.anchorY", "textures.anchorY"],
    ["background.fit", "textures.fit"],
    ["background.scaleX", "textures.scaleX"],
    ["background.scaleY", "textures.scaleY"],
    ["background.rotation", "textures.rotation"],
    ["fog.overlay", "fog.src"],
    ["foreground", "foreground.src"],
    ["foregroundElevation", "elevation.top"],
    ["backgroundColor", "background.color"]
  ]);

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

  /**
   * The default grid defined by the system.
   * @type {BaseGrid}
   */
  static get defaultGrid() {
    if ( !BaseScene.#defaultGrid ) BaseScene.#initializeDefaultGrids();
    return BaseScene.#defaultGrid;
  }

  static #defaultGrid;

  /**
   * The gridless version of the default grid defined by the system.
   * @type {GridlessGrid}
   */
  static get defaultGridlessGrid() {
    if ( !BaseScene.#defaultGridlessGrid ) BaseScene.#initializeDefaultGrids();
    return BaseScene.#defaultGridlessGrid;
  }

  static #defaultGridlessGrid;

  /**
   * Initialize both the grid instance and its gridless version.
   */
  static #initializeDefaultGrids() {
    const T = CONST.GRID_TYPES;
    const {type, ...config} = game.system.grid;
    config.size = 100;
    BaseScene.#defaultGridlessGrid = new GridlessGrid(config);

    // Gridless grid
    if ( type === T.GRIDLESS ) BaseScene.#defaultGrid = BaseScene.#defaultGridlessGrid;

    // Square grid
    if ( type === T.SQUARE ) BaseScene.#defaultGrid = new SquareGrid(config);

    // Hexagonal grid
    if ( type.between(T.HEXODDR, T.HEXEVENQ) ) {
      config.columns = (type === T.HEXODDQ) || (type === T.HEXEVENQ);
      config.even = (type === T.HEXEVENR) || (type === T.HEXEVENQ);
      BaseScene.#defaultGrid = new HexagonalGrid(config);
    }
  }

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

  /**
   * The initial Level of the Scene. By default the first Level.
   * @type {BaseLevel}
   */
  get initialLevel() {
    const id = this._source.initialLevel;
    if ( !id ) return this.firstLevel;
    return this.levels.get(id) ?? this.firstLevel;
  }

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

  /**
   * A convenience getter for the Scene's first created Level. This should not be relied on in multi-level scenes to
   * mean the first level by sort order.
   * @type {BaseLevel}
   */
  get firstLevel() {
    return this.levels.values().next()?.value;
  }

  /* -------------------------------------------- */
  /*  Data Management                             */
  /* -------------------------------------------- */

  /** @inheritDoc */
  _initialize(options) {
    super._initialize(options);
    fields.DocumentStatsField._shimDocument(this);
    BaseScene.#shimFogExploration(this);
  }

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

  /** @inheritDoc */
  updateSource(changes={}, options={}) {
    if ( "tokens" in changes ) {
      for ( const tokenChange of changes.tokens ) {
        this.tokens.get(tokenChange._id)?._prepareDeltaUpdate(tokenChange, options);
      }
    }
    return super.updateSource(changes, options);
  }

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

  /** @inheritdoc */
  static migrateData(source, options) {
    fields.DocumentStatsField._migrateData(this, source);
    return super.migrateData(source, options);
  }

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

  /** @inheritdoc */
  static shimData(source, options) {
    if ( Object.isSealed(source) ) return super.shimData(source, options);
    fields.DocumentStatsField._shimData(this, source, options);
    const firstLevel = source.levels?.[0];

    // Foreground
    Object.defineProperty(source, "foreground", {
      get: () => firstLevel?.foreground?.src,
      configurable: true,
      enumerable: false
    });

    // Foreground Elevation
    Object.defineProperty(source, "foregroundElevation", {
      get: () => firstLevel?.elevation?.top,
      configurable: true,
      enumerable: false
    });

    // Fog Overlay
    if ( source.fog ) {
      Object.defineProperty(source.fog, "overlay", {
        get: () => firstLevel?.[0]?.fog?.src,
        configurable: true,
        enumerable: false
      });
    }

    // Background
    Object.defineProperty(source, "background", {
      get: () => {
        const background = {};
        for ( const [oldKey, newKey] of BaseScene._LEVELS_PROPERTY_MAP ) {
          if ( !oldKey.startsWith("background.") ) continue;
          const key = oldKey.slice(11);
          foundry.utils.setProperty(background, key, foundry.utils.getProperty(firstLevel, newKey));
        }
        background.offsetX = source.shiftX;
        background.offsetY = source.shiftY;
        return background;
      },
      configurable: true,
      enumerable: false
    });

    // Background Color
    Object.defineProperty(source, "backgroundColor", {
      get: () => firstLevel?.background?.color,
      configurable: true,
      enumerable: false
    });

    this._addDataFieldShim(source, "fog.exploration", "fog.mode", {since: 14, until: 16,
      warning: "BaseScene#fog.exploration is deprecated in favor of BaseScene#fog.mode"});
    return super.shimData(source, options);
  }

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

  /**
   * Put an alias for deprecated fog.exploration
   * @param {BaseScene} scene
   * @deprecated since v14
   */
  static #shimFogExploration(scene) {
    const fog = scene.fog;
    if ( !fog ) return;
    if ( Object.prototype.hasOwnProperty.call(fog, "exploration") ) return;
    Object.defineProperty(fog, "exploration", {
      get() {
        foundry.utils.logCompatibilityWarning(
          "BaseScene#fog.exploration is deprecated in favor of BaseScene#fog.mode", {since: 14, until: 16}
        );
        return !!this.mode;
      },
      set(value) {
        foundry.utils.logCompatibilityWarning(
          "BaseScene#fog.exploration is deprecated in favor of BaseScene#fog.mode", {since: 14, until: 16}
        );
        this.mode = value ? CONST.FOG_EXPLORATION_MODES.INDIVIDUAL : CONST.FOG_EXPLORATION_MODES.DISABLED;
      },
      configurable: true,
      enumerable: false
    });
  }
}
