/**
 * Clase genérica para manejar una colección de elementos.
 * Ofrece métodos básicos para manipular, consultar y recorrer los elementos.
 */
export class Collection<T> {
  protected items: T[];
  protected totalItems: number;

  /**
   * Crea una nueva colección.
   * @param items - Elementos iniciales de la colección.
   * @param totalItems - Total de elementos esperados (opcional). Si no se define, se usa la longitud del array inicial.
   */
  constructor(items: T[] = [], totalItems: number | null = null) {
    this.items = [...items];
    this.totalItems = totalItems ?? items.length;
  }

  /**
   * Vacía la colección y reinicia el total de elementos.
   */
  reset(): void {
    this.items = [];
    this.totalItems = 0;
  }

  /**
   * Agrega un nuevo elemento a la colección.
   * @param item - Elemento a agregar.
   */
  addCollection(collection: Collection<T>): boolean {
    this.items.push(...collection.items);
    if (this.totalItems !== null) {
      this.totalItems = this.totalItems + collection.totalItems;
    }
    return true;
  }

  /**
   * Agrega un nuevo elemento a la colección.
   * @param item - Elemento a agregar.
   */
  add(item: T): boolean {
    this.items.push(item);
    if (this.totalItems !== null) {
      this.totalItems++;
    }
    return true;
  }

  /**
   * Elimina un elemento de la colección, si existe.
   * @param item - Elemento a eliminar.
   * @returns `true` si el elemento fue eliminado, `false` si no se encontró.
   */
  remove(item: T): boolean {
    const index = this.items.indexOf(item);
    return this.removeByIndex(index);
  }

  removeByIndex(index: number) {
    if (index !== -1) {
      this.items.splice(index, 1);
      if (this.totalItems !== null) {
        this.totalItems--;
      }
      return true;
    }
    return false;
  }

  /**
   * Devuelve una copia de todos los elementos de la colección.
   * @returns Array de elementos.
   */
  getAll(): T[] {
    return [...this.items];
  }

  /**
   * Devuelve la cantidad actual de elementos en la colección.
   * @returns Número de elementos presentes.
   */
  size(): number {
    return this.items.length;
  }

  /**
   * Devuelve el total de elementos esperados en la colección.
   * Puede diferir de `size()` si los datos son paginados, por ejemplo.
   * @returns Número total o `null` si no se especificó.
   */
  total(): number {
    return this.totalItems;
  }

  /**
   * Performs the specified action for each element in an array.
   * @param callbackfn A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array.
   * @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
   */
  forEach<U>(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void {
    this.items.forEach(callbackfn);
  }

  /**
   * Aplica una función a cada elemento y devuelve un nuevo array con los resultados.
   * @param callback - Función transformadora.
   * @returns Nuevo array con los elementos transformados.
   */
  map<U>(callback: (item: T, index: number, array: T[]) => U): U[] {
    return this.items.map(callback);
  }

  /**
   * Devuelve un array con los elementos que cumplen la condición del callback.
   * @param callback - Función de filtrado.
   * @returns Nuevo array con los elementos filtrados.
   */
  filter(callback: (item: T, index: number, array: T[]) => boolean): T[] {
    return this.items.filter(callback);
  }

  /**
   * Devuelve el primer elemento que cumple la condición del callback.
   * @param callback - Función de búsqueda.
   * @returns El primer elemento que cumple la condición, o `undefined` si no hay coincidencias.
   */
  find(callback: (item: T, index: number, array: T[]) => boolean): T | undefined {
    return this.items.find(callback);
  }

  /**
   * Verifica si al menos un elemento cumple la condición dada.
   * @param callback - Función de evaluación.
   * @returns `true` si al menos un elemento cumple, `false` en caso contrario.
   */
  some(callback: (item: T, index: number, array: T[]) => boolean): boolean {
    return this.items.some(callback);
  }

  /**
   * Verifica si todos los elementos cumplen la condición dada.
   * @param callback - Función de evaluación.
   * @returns `true` si todos cumplen, `false` si alguno no.
   */
  every(callback: (item: T, index: number, array: T[]) => boolean): boolean {
    return this.items.every(callback);
  }
}
