/* 
 * This file is part of the warpgate module (https://github.com/trioderegion/warpgate)
 * Copyright (c) 2021 Matthew Haentschke.
 * 
 * This program is free software: you can redistribute it and/or modify  
 * it under the terms of the GNU General Public License as published by  
 * the Free Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License 
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

import {
  logger
} from './logger.js';
import {
  MODULE
} from './module.js'

/** @typedef {import('./api.js').Shorthand} Shorthand */

/** 
 * @typedef {Object} MutationData
 * @property {Shorthand} delta
 * @property {string} user
 * @property {Object<string, string>} comparisonKeys
 * @property {Shorthand} updateOpts
 * @property {object} overrides
 * @property {string} name
 */

/**
 * The following class and its utility methods allows safer and more direct modification of the mutation stack,
 * which is stored on a token's actor. This mutation stack stores the information needed to _revert_ the changes
 * made by a mutation. This could be used, for example, to deal with rollover damage where the hit point value
 * being reverted to is lower than when the mutation was first applied.
 * 
 * Searching and querying a given mutation is a quick, read-only process. When the mutation stack is modified 
 * via one of its class methods, the actor's mutation data at that point in time will be copied for fast, local updates.
 *
 * No changes will be made to the actor's serialized data until the changes have been commited ({@link MutationStack#commit}).
 * The MutationStack object will then be locked back into a read-only state sourced with this newly updated data.
 */
export class MutationStack {
  constructor(tokenDoc) {

    this.actor = tokenDoc instanceof TokenDocument ? tokenDoc.actor :
                    tokenDoc instanceof Token ? tokenDoc.document.actor :
                    tokenDoc instanceof Actor ? tokenDoc :
                    null;

    if(!this.actor) {
      throw new Error(MODULE.localize('error.stack.noActor'));
    }

  }

  /**
   * Private copy of the working stack (mutable)
   * @type {Array<MutationData>}
   */
  #stack = [];

  /** indicates if the stack has been duplicated for modification */
  #locked = true;

  /**
   * Current stack, according to the remote server (immutable)
   * @const
   * @type {Array<MutationData>}
   */
  get #liveStack() {
    // @ts-ignore
    return this.actor?.getFlag(MODULE.data.name, 'mutate') ?? [] 
  }

  /** 
   * Mutation stack according to its lock state.
   * @type {Array<MutationData>} 
   */
  get stack() {
    return this.#locked ? this.#liveStack : this.#stack ;
  }

  /**
   * @callback FilterFn
   * @param {MutationData} mutation
   * @returns {boolean} provided mutation meets criteria
   * @memberof MutationStack
   */

  /**
   * Searches for an element of the mutation stack that satisfies the provided predicate
   *
   * @param {FilterFn} predicate Receives the argments of `Array.prototype.find` 
   *  and should return a boolean indicating if the current element satisfies the predicate condition
   * @return {MutationData|undefined} Element of the mutation stack that matches the predicate, or undefined if none.
   */
  find(predicate) {
    if (this.#locked) return this.#liveStack.find(predicate);

    return this.#stack.find(predicate);
  }

  /**
   * Searches for an element of the mutation stack that satisfies the provided predicate and returns its
   * stack index
   *
   * @param {FilterFn} predicate Receives the argments of {@link Array.findIndex} and returns a Boolean indicating if the current
   *                             element satisfies the predicate condition
   * @return {Number} Index of the element of the mutation stack that matches the predicate, or undefined if none.
   */
  #findIndex( predicate ) {

    if (this.#locked) return this.#liveStack.findIndex(predicate);

    return this.#stack.findIndex(predicate);
  }

  /**
   * Retrieves an element of the mutation stack that matches the provided name
   *
   * @param {String} name Name of mutation (serves as a unique identifier)
   * @return {MutationData|undefined} Element of the mutation stack matching the provided name, or undefined if none
   */
  getName(name) {
    return this.find((element) => element.name === name);
  }

  /**
   * Retrieves that last mutation added to the mutation stack (i.e. the "newest"), 
   * or undefined if none present
   * @type {MutationData}
   */
  get last() {
    return this.stack[this.stack.length - 1];
  }

  /**
   * Updates the mutation matching the provided name with the provided mutation info.
   * The mutation info can be a subset of the full object if (and only if) overwrite is false.
   *
   * @param {string} name name of mutation to update
   * @param {MutationData} data New information, can include 'name'.
   * @param {object} options
   * @param {boolean} [options.overwrite = false] default will merge the provided info
   *            with the current values. True will replace the entire entry and requires
   *            at least the 'name' field.
   *  
   * @return {MutationStack} self, unlocked for writing and updates staged if update successful
   */
  update(name, data, {
    overwrite = false
  }) {
    const index = this.#findIndex((element) => element.name === name);

    if (index < 0) {
      return this;
    }

    this.#unlock();

    if (overwrite) {

      /* we need at LEAST a name to identify by */
      if (!data.name) {
        logger.error(MODULE.localize('error.incompleteMutateInfo'));
        this.#locked=true; 
        return this;
      }

      /* if no user is provided, input current user. */
      if (!data.user) data.user = game.user.id;
      this.#stack[index] = data;

    } else {
      /* incomplete mutations are fine with merging */
      mergeObject(this.#stack[index], data);
    }

    return this;
  }

  /**
   * Applies a given change or tranform function to the current buffer,
   * unlocking if needed.
   *
   * @param {MutationData|function(MutationData) : MutationData} transform Object to merge or function to generate an object to merge from provided {@link MutationData}
   * @param {FilterFn} [filterFn = () => true] Optional function returning a boolean indicating 
   *   if this element should be modified. By default, affects all elements of the mutation stack.
   * @return {MutationStack} self, unlocked for writing and updates staged.
   */
  updateAll(transform, filterFn = () => true) {

    const innerUpdate = (transform) => {
      if (typeof transform === 'function') {
        /* if we are applying a transform function */
        return (element) => mergeObject(element, transform(element));
      } else {
        /* if we are applying a constant change */
        return (element) => mergeObject(element, transform);
      }
    }

    this.#unlock();

    this.#stack.forEach((element) => {
      if (filterFn(element)) {
        innerUpdate(transform)(element);
      }
    });

    return this;
  }

  /**
   * Deletes all mutations from this actor's stack, effectively making
   * the current changes permanent.
   *
   * @param {function(MutationData):boolean} [filterFn = () => true] Optional function returning a boolean indicating if this
   *                   element should be delete. By default, deletes all elements of the mutation stack.
   * @return {MutationStack} self, unlocked for writing and updates staged.
   */
  deleteAll(filterFn = () => true) {
    this.#unlock();

    this.#stack = this.#stack.filter((element) => !filterFn(element))

    return this;
  }

  /**
   * Updates the owning actor with the mutation stack changes made. Will not commit a locked buffer.
   *
   * @return {Promise<MutationStack>} self, locked for writing
   */
  async commit() {

    if(this.#locked) {
      logger.error(MODULE.localize('error.stackLockedOrEmpty'))
    }

    await this.actor.update({
      flags: {
        [MODULE.data.name]: {
          'mutate': this.#stack
        }
      }
    });

    /* return to a locked read-only state */
    this.#locked = true;
    this.#stack.length = 0;

    return this;
  }

  /**
   * Unlocks the current buffer for writing by copying the mutation stack into this object.
   *
   * @return {boolean} Indicates if the unlock occured. False indicates the buffer was already unlocked.
   */
  #unlock() {

    if (!this.#locked) {
      return false;
    }

    this.#stack = duplicate(this.#liveStack)
    this.#locked = false;
    return true;
  }

}
