import {DataField, NumberField, SchemaField, StringField} from "@common/data/fields.mjs";

/**
 * @import { DataFieldOptions, DataFieldValidationOptions, DataFieldContext } from "@common/data/_types.mjs"
 */

/**
 * @template ReferenceType
 * @typedef {Object} VFXReference
 * @property {string} reference
 * @property {string} [property]
 **/

/**
 * This specialized data field allows storing a data structure that will be later dynamically resolved.
 * This field can be used for a value type that is a single property.
 * If that property is numeric, the reference field can store a relative delta.
 *
 * @template {string|number|boolean|null} [ValueType=any]
 * @typedef {{reference: string; property?: string }|ValueType} VFXReferenceFieldData
 */
export default class VFXReferenceField extends DataField {
  /**
   * Construct a VFXReferenceField by providing the inner field schema that it wraps.
   * @param {DataField} valueField
   * @param {DataFieldOptions} options
   * @param {DataFieldContext} context
   */
  constructor(valueField, options, context) {
    super({...valueField.options, ...options}, context);
    this.valueField = valueField;
    valueField.parent = this;
  }

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

  /**
   * The schema of a reference object.
   * @type {SchemaField}
   */
  static referenceField = new SchemaField({
    reference: new StringField({required: true, nullable: false}),
    property: new StringField({required: false, nullable: true}),
    delta: new NumberField({required: false, nullable: false})
  }, {required: true, nullable: false});

  /** @type {DataField} */
  valueField;

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

  /** @override */
  _cleanType(value, options, _state) {
    if ( this.constructor.isReference(value) ) return this.constructor.referenceField.clean(value, options, _state);
    return this.valueField.clean(value, options, _state);
  }

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

  /** @override */
  _validateType(value, options) {
    if ( this.constructor.isReference(value) ) return this.constructor.referenceField.validate(value, options);
    return this.valueField.validate(value, options);
  }

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

  /** @override */
  initialize(value, model, options) {
    // A ReferenceField is initialized as its valueField or undefined
    if ( this.constructor.isReference(value) ) return undefined;
    return this.valueField.initialize(value, model, options);
  }

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

  /**
   * Resolve the value of a VFXReferenceObjectField
   * @param {any} value                         The initial value of the field which may contain a reference
   * @param {Record<string, any>} references    Provided references
   * @returns {ValueType|null|undefined}        The resulting resolved value with references applied, if possible
   */
  resolve(value, references) {
    if ( !this.constructor.isReference(value) ) return this.valueField.clean(value);
    let result = references[value.reference];
    if ( value.property ) result = foundry.utils.getProperty(result, value.property);
    if ( Number.isNumeric(value.delta) && Number.isNumeric(result) ) result += value.delta;
    return this.valueField.clean(result);
  }

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

  /**
   * Test whether a value is a reference.
   * @param {any} value
   * @returns {boolean}
   */
  static isReference(value) {
    try {
      this.referenceField.validate(value, {strict: true});
      return true;
    } catch(err) {
      return false;
    }
  }
}
