/**
 * A symbol used to reference the operator value which ensures it does not collide with a proxied key of that value.
 * @type {symbol}
 */
export const OPERATOR_VALUE = Symbol("DataFieldOperatorValue");

/**
 * A unique string used in serialization to identify that an object should be deserialized to a DataFieldOperator.
 * @type {string}
 */
export const OPERATOR_IDENTIFIER = "__$OPERATOR$__";

/**
 * A base class used for all special database operations.
 */
export class DataFieldOperator {
  constructor(value) {
    /**
     * The value that a field should be assigned to.
     * @type {any}
     */
    this[OPERATOR_VALUE] = value;
  }

  /**
   * Represent the data of the DataFieldOperator as an object suitable for JSON serialization.
   * @returns {object}     Structured data that can be serialized into JSON
   */
  toJSON() {
    const v = this[OPERATOR_VALUE];
    const value = typeof v?.toJSON === "function" ? v.toJSON() : v;
    return {[OPERATOR_IDENTIFIER]: this.constructor.name, value};
  }

  /**
   * Create a DataFieldOperator using a provided value.
   * @param {any} value
   * @returns {DataFieldOperator}
   */
  static create(value) {
    return new this(value);
  }

  /**
   * Retrieve the inner value of the DataFieldOperator, or return the value passed if not a DataFieldOperator.
   * @param {any} value
   * @returns {any}
   */
  static get(value) {
    return value instanceof DataFieldOperator ? value[OPERATOR_VALUE] : value;
  }

  /**
   * Assign the inner value of the DataFieldOperator.
   * @param {DataFieldOperator} operator
   * @param {any} value
   * @returns {any}
   */
  static set(operator, value) {
    return operator[OPERATOR_VALUE] = value;
  }

  /**
   * A comparison helper function that asserts whether two values are equal when one or both values may be
   * DataFieldOperator instances.
   * @param {DataFieldOperator|any} a
   * @param {DataFieldOperator|any} b
   */
  static equals(a, b) {
    const av = a instanceof DataFieldOperator ? DataFieldOperator.get(a) : a;
    const bv = b instanceof DataFieldOperator ? DataFieldOperator.get(b) : b;
    return foundry.utils.equals(av, bv);
  }
}

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

/**
 * Force the deletion of a certain DataModel field, resetting its value back to undefined.
 */
export class ForcedDeletion extends DataFieldOperator {
  constructor(_value) {
    super();
  }
}

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

/**
 * Force the replacement of a certain DataModel field, assigning it to some explicit value without inner recursion.
 */
export class ForcedReplacement extends DataFieldOperator {
  constructor(value) {
    super(ForcedReplacement.get(value));
  }

  /**
   * Create a ForcedReplacement instance that is wrapped in a Proxy so that it can be inspected.
   * @param {any} value
   * @returns {ForcedReplacement}
   */
  static create(value) {
    return new Proxy(new ForcedReplacement(value), {
      get(target, prop, receiver) {
        if ( Reflect.has(target, prop) ) return Reflect.get(target, prop, receiver);
        const v = target[OPERATOR_VALUE];
        if ( v && (typeof v === "object") ) return Reflect.get(v, prop, receiver);
      },
      has(target, key) {
        if ( Reflect.has(target, key) ) return true;
        const v = target[OPERATOR_VALUE];
        if ( v && (typeof v === "object") ) return Reflect.has(v, key);
      },
      set(target, prop, value) {
        if ( Reflect.has(target, prop) ) return Reflect.set(target, prop, value);
        const v = target[OPERATOR_VALUE];
        if ( v && (typeof v === "object") ) return Reflect.set(v, prop, value);
      },
      ownKeys(target) {
        const v = target[OPERATOR_VALUE];
        return ( v && (typeof v === "object") ) ? Reflect.ownKeys(v) : [];
      },
      getOwnPropertyDescriptor(target, prop) {
        const v = target[OPERATOR_VALUE];
        if ( v && (typeof v === "object") ) {
          const descriptor = Reflect.getOwnPropertyDescriptor(v, prop);
          // Satisfy trap invariants.
          if ( descriptor?.configurable === false ) Object.defineProperty(target, prop, descriptor);
          return descriptor;
        }
      }
    });
  }
}

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

/**
 * Reconstruct a DataFieldOperator instance from a serialized object.
 * @param {{[OPERATOR_IDENTIFIER]: string, value: any}} obj
 */
export function reconstructOperator(obj) {
  const operator = obj[OPERATOR_IDENTIFIER];
  if ( operator === "ForcedDeletion" ) return new ForcedDeletion();
  else if ( operator === "ForcedReplacement" ) return ForcedReplacement.create(obj.value);
  else throw new Error(`Unrecognized DataFieldOperator identifier "${operator}" provided`);
}
