Source: src/js/collections/DataONEObjects.js

"use strict";

define(["backbone", "models/DataONEObject", "common/DateUtility"], (
  Backbone,
  DataONEObject,
  DateUtility,
) => {
  /**
   * @class DataONEObjects
   * @classdesc A collection of DataONEObject models.
   * @classcategory Collections/SearchSelect
   * @since 2.37.0
   */
  const DataONEObjects = Backbone.Collection.extend({
    /** @lends DataONEObjects.prototype */

    /** @inheritdoc */
    model: DataONEObject,

    /** @inheritdoc */
    comparator: "dateUploaded",

    /**
     * Determine if all models in the collection are hidden by the UI based on
     * the `hiddenByUI` property.
     * @returns {boolean} True if all models are hidden, false otherwise.
     */
    allHidden() {
      return (
        this.length > 0 &&
        this.every((model) => model.get("hiddenByUI") === true)
      );
    },

    /**
     * Return models ordered by version-chain position relative to a reference
     * PID, newest to oldest. Models without a valid chain index are placed at
     * the end in deterministic identifier order.
     * @param {string} referencePid PID used as the versionHistory index key.
     * @returns {Backbone.Model[]} Ordered model array (new array).
     */
    getChainOrdered(referencePid) {
      if (!referencePid) {
        throw new Error("referencePid is required to order by version chain");
      }

      const getIndex = (model) => {
        const index = model.get("versionHistory")?.[referencePid];
        return Number.isFinite(index) ? index : null;
      };

      return [...this.models].sort((a, b) => {
        const indexA = getIndex(a);
        const indexB = getIndex(b);
        const hasIndexA = Number.isFinite(indexA);
        const hasIndexB = Number.isFinite(indexB);

        if (hasIndexA && hasIndexB && indexA !== indexB) {
          return indexB - indexA; // newer (+) first, older (-) last
        }
        if (hasIndexA !== hasIndexB) {
          return hasIndexA ? -1 : 1;
        }

        const idA = a.get("identifier") || "";
        const idB = b.get("identifier") || "";
        if (idA === idB) return 0;
        return idA < idB ? -1 : 1;
      });
    },

    /**
     * Determine if the object with the given identifier is the newest in the
     * collection based on the `dateUploaded` property.
     * @param {string} identifier - The identifier of the object to check.
     * @returns {boolean} True if the object is the newest, false otherwise.
     */
    isNewest(identifier) {
      if (
        !identifier ||
        !this.length ||
        this.findWhere({ identifier }) == null
      ) {
        throw new Error("Identifier not found in collection");
      }
      const sorted = this.sortBy((obj) => obj.get("dateUploaded"));
      const newest = sorted[sorted.length - 1];
      return newest.get("identifier") === identifier;
    },

    /**
     * Determine if the object with the given identifier is the oldest in the
     * collection based on the `dateUploaded` property.
     * @param {string} identifier - The identifier of the object to check.
     * @returns {boolean} True if the object is the oldest, false otherwise.
     */
    isOldest(identifier) {
      if (
        !identifier ||
        !this.length ||
        this.findWhere({ identifier }) == null
      ) {
        throw new Error("Identifier not found in collection");
      }
      const sorted = this.sortBy((obj) => obj.get("dateUploaded"));
      const oldest = sorted[0];
      return oldest.get("identifier") === identifier;
    },

    /**
     * Get the date range of the collection based on the `dateUploaded`
     * property. Returns null if there are no valid dates.
     * @param {object} [options] Options for calculating date range.
     * @param {string} [options.dateProp] Model property to read.
     * @returns {{minDate: Date, maxDate: Date, minModel: Backbone.Model,
     * maxModel: Backbone.Model}|null} Date range info or null if no valid
     * dates.
     */
    getDateRange({ dateProp = "dateUploaded" } = {}) {
      if (!this.length) return null;

      let minDate = null;
      let maxDate = null;
      let idMinDate = null;
      let idMaxDate = null;

      // Pluck the date & id for all models
      this.each((model) => {
        const dateValue = model.get(dateProp);
        const date = DateUtility.toDate(dateValue);
        if (date) {
          if (!minDate || date < minDate) {
            minDate = date;
            idMinDate = model.get("identifier");
          }
          if (!maxDate || date > maxDate) {
            maxDate = date;
            idMaxDate = model.get("identifier");
          }
        }
      });

      const minModel = this.get(idMinDate);
      const maxModel = this.get(idMaxDate);

      if (minDate && maxDate) {
        return {
          minDate,
          maxDate,
          minModel,
          maxModel,
        };
      }
      return null;
    },

    /**
     * Group models by calendar day in the chosen timezone.
     * @param {object} [options] Options for grouping
     * @param {string} [options.dateProp] Model property to read.
     * @param {("local"|"UTC")} [options.groupingTimeZone] Timezone used
     * to determine day boundaries.
     * @returns {Array.<{date: (Date|null), models: Array.<Backbone.Model>}>}
     * Grouped models.
     */
    groupByDate({
      dateProp = "dateUploaded",
      groupingTimeZone = "local",
    } = {}) {
      if (!this.length) return [];

      const normalizedTimeZone = groupingTimeZone === "UTC" ? "UTC" : "local";
      const groups = new Map();

      this.models.forEach((obj) => {
        const rawDate = obj.get(dateProp);
        let dateKey = null;
        let groupDate = null;

        if (rawDate) {
          try {
            const parsedDate = DateUtility.toDate(rawDate);
            if (!parsedDate) {
              throw new Error("Invalid Date");
            }
            groupDate = DateUtility.toMidnightDate(
              parsedDate,
              normalizedTimeZone,
            );
            dateKey = groupDate.getTime();
          } catch (e) {
            // eslint-disable-next-line no-console
            console.warn(
              "Error parsing dateUploaded for grouping:",
              rawDate,
              e,
            );
          }
        }

        if (!groups.has(dateKey)) {
          groups.set(dateKey, { date: groupDate, models: [] });
        }
        groups.get(dateKey).models.push(obj);
      });

      return Array.from(groups.values());
    },
  });

  return DataONEObjects;
});