import { makeAutoObservable } from 'mobx';
import { swapItems } from '../utils/list/swapItems';

/**
 * Minimum requirements values for an item in the list
 */
export interface IListItem {
  /**
   * Unique id for the item
   */
  id: string;
}

/**
 * A model to handle lists of items
 */
export class List<T extends IListItem> {
  /**
   * Key value pairs of all items in the list
   */
  private stack: { [id: string]: T } = {};
  /**
   * Array of ids only, it keeps track of the order of the items
   */
  private order: string[];
  /**
   * Initializes the list
   * The list is initialized empty by default
   */
  constructor(list?: T[]) {
    // the order of the items is the array of ids
    // this needs to be initialized first
    this.order = list?.map((item) => item.id) || [];
    this.set(list || []);
    makeAutoObservable(this);
  }

  /**
   * Get the array of all items in the list, the list will be ordered
   * @returns array of items
   */
  get values(): T[] {
    // note that the item is not find it will return null
    return this.order.map((id) => this.stack[id]);
  }

  /**
   * returns the count of items in the list
   */
  get length(): number {
    return this.order.length;
  }

  /**
   * Update the list with an array of items
   * use this method to update the list with an array of items
   * this method resets the list with theses items
   * use addValues instead if you want to append to the list
   * @param values array of items to initialize the list with
   */
  private updateValues(values?: T[]) {
    values?.forEach((item) => {
      this.updateValue(item);
    });
  }

  /**
   * Update the list with an item
   * If the item already exists it will be updated
   * If the item does not exist it will be created
   * @param value item to be added/updated in the list
   */
  private updateValue(value?: T) {
    if (value) {
      this.stack[value.id] = value;
      if (!this.order.includes(value.id)) {
        this.order = [...this.order, value.id];
      }
    }
  }
  /**
   * Utility method to get the index of an item in the list
   * @param id id of the item
   * @returns the index of the item or -1 if not found
   */
  private getIndex = (id: string): number => {
    const list = this.order;
    return list.findIndex((item) => item === id);
  };

  /**
   * Get an item from the list by id
   * @param id id of the item or the index of the item in the list
   * @returns the item or undefined if not found
   */
  get = (id: string | number): T | undefined => {
    if (typeof id === 'number') {
      return this.getByIndex(id);
    }

    return this.stack[id];
  };

  /**
   * Get an item from the list by index
   * @param index the index of the item in the list
   * @returns the item or undefined if not found
   */
  private getByIndex = (index: number): T | undefined => {
    const id = this.order[index];
    return this.get(id);
  };

  /**
   * Add an item to the list or update it if it already exists
   * @param item item to be added/updated in the list
   */
  set = (item: T | T[]) => {
    if (typeof item === 'object' && Array.isArray(item)) {
      this.updateValues(item);
    } else {
      this.updateValue(item);
    }
  };

  /**
   * Deletes the last item in the list and returns that item
   * @returns the last item in the list
   */
  pop = (): T | undefined => {
    const lastItemId = this.order[this.order.length - 1];
    if (lastItemId) {
      const lastItem = this.stack[lastItemId];
      this.delete(lastItemId);
      return lastItem;
    }
    return undefined;
  };

  /**
   * Deletes the first item in the list and returns that item
   * @returns the first item in the list
   */
  shift = (): T | undefined => {
    const firstItemId = this.order[0];
    if (firstItemId) {
      const firstItem = this.stack[firstItemId];
      this.delete(firstItemId);
      return firstItem;
    }
    return undefined;
  };

  /**
   * Get the last item of the list
   * @returns the last item in the list
   */
  peakLast = () => {
    const lastItemId = this.order[this.order.length - 1];
    if (lastItemId) {
      return this.stack[lastItemId];
    }
    return undefined;
  };

  /**
   * Get the first item of the list
   * @returns the first item in the list
   */
  peakFirst = () => {
    const firstItemId = this.order[0];
    if (firstItemId) {
      return this.stack[firstItemId];
    }
    return undefined;
  };

  /**
   * Delete an item from the list
   * @param id id of the item to be removed
   * @returns true if item was removed, false if item was not found
   */
  delete = (id: string): boolean => {
    // first remove the id from the order
    this.order = this.order.filter((itemId) => itemId !== id);
    // then delete the item from the stack
    if (this.stack[id]) {
      delete this.stack[id];
      return true;
    }

    return false;
  };

  /**
   * Swaps two items in the list
   * @param index1 index of the first item to be swapped
   * @param index2 index of the second item to be swapped
   */
  swap = (index1: number, index2: number): void => {
    const list = this.order;
    const updatedList = swapItems<string>(list, index1, index2);
    this.order = updatedList;
  };

  /**
   * Swaps two items in the list by id
   * @param id1 the id of the first item to be swapped
   * @param id2 the id of the second item to be swapped
   */
  swapById = (id1: string, id2: string): void => {
    const index1 = this.getIndex(id1);
    const index2 = this.getIndex(id2);
    this.swap(index1, index2);
  };

  /**
   * clears the list
   */
  reset = () => {
    this.order = [];
    this.stack = {};
  };
}
