Source: src/js/views/CanonicalDatasetHandlerView.js

define([
  "backbone",
  "views/CitationView",
  "models/CitationModel",
  "models/CrossRefModel",
], (Backbone, CitationView, CitationModel, CrossRefModel) => {
  // The "Type" property of the annotation view
  const ANNO_VIEW_TYPE = "AnnotationView";
  // The URI for the schema.org:sameAs annotation
  const SCHEMA_ORG_SAME_AS = "http://www.w3.org/2002/07/owl#sameAs";
  // The URI for the prov:wasDerivedFrom annotation
  const PROV_WAS_DERIVED_FROM = "http://www.w3.org/ns/prov#wasDerivedFrom";

  // The text to show in the alert box at the top of the MetadataView
  const ALERT_TEXT =
    "This version of the dataset is a replica or minor variant of the original dataset:";
  // The text to display in the info tooltip to explain what the info icon means
  const INFO_ICON_TOOLTIP_TEXT =
    "This dataset is replica or minor variant of another, original dataset.";
  // In the citation modal, the heading to use for the dataone version citation
  const CITATION_TITLE_DATAONE = "This Version of the Dataset";
  // In the citation modal, the heading to use for the canonical dataset
  // citation
  const CITATION_TITLE_CANONICAL = "Canonical Dataset";
  // The class to use for the info icon
  const INFO_ICON_CLASS = "info";
  // The bootstrap icon name to use for the info icon
  const INFO_ICON_NAME = "icon-copy";

  // Class names used in this view
  const CLASS_NAMES = {
    alertBox: ["alert", "alert-info", "alert-block"], // TODO: need alert-block?
    alertIcon: ["icon", "icon-info-sign", "icon-on-left"],
    alertCitation: "canonical-citation",
  };

  // The following properties are used to identify parts of the MetadataView.
  // If the MetadataView changes, these properties may need to be updated.
  const METADATA_VIEW = {
    // The selector for the container that contains the info icons and metrics buttons
    controlsSelector: "#metadata-controls-container",
    // The name of the property on the MetadataView that contains the citation
    // modal
    citationModalProp: "citationModal",
    // The name of the property on the MetadataView that contains subviews
    subviewProp: "subviews",
  };

  /**
   * @class CanonicalDatasetHandlerView
   * @classdesc A scoped subview responsible for inspecting the rendered DOM
   * within the MetadataView to identify and highlight the canonical (original)
   * dataset based on schema.org:sameAs and prov:derivedFrom annotations. This
   * view modifies specific parts of the MetadataView when a canonical dataset
   * is detected, providing a clearer distinction between original and derived
   * datasets.
   * @classcategory Views
   * @augments Backbone.View
   * @class
   * @since 2.32.0
   * @screenshot views/CanonicalDatasetHandlerViewView.png
   */
  const CanonicalDatasetHandlerView = Backbone.View.extend(
    /** @lends CanonicalDatasetHandlerView.prototype */
    {
      /** @inheritdoc */
      type: "CanonicalDatasetHandlerView",

      /**
       * The MetadataView instance this view is scoped to.
       * @type {MetadataView}
       */
      metdataView: null,

      /**
       * Initialize the CanonicalDatasetHandlerView.
       * @param {object} options - A set of options to initialize the view with.
       * @param {MetadataView} options.metadataView - The MetadataView instance
       * this view is scoped to. Required.
       */
      initialize(options) {
        this.metadataView = options?.metadataView;
        this.citationModel = new CitationModel();
        if (!this.metadataView) {
          throw new Error(
            "The CanonicalDatasetHandlerView requires a MetadataView instance.",
          );
        }
      },

      /** @inheritdoc */
      render() {
        // In case it's a re-render, remove any modifications made previously
        this.reset();
        const hasCanonical = this.detectCanonicalDataset();
        if (!hasCanonical) return this;
        this.infoIcon = this.addInfoIcon();
        this.alertBox = this.addAlertBox();
        this.getCitationInfo();
        this.modifyCitationModal();
        this.hideAnnotations();
        return this;
      },

      /**
       * Resets the MetadataView to its original state by removing any changes
       * made by this view.
       */
      reset() {
        this.infoIcon?.remove();
        this.alertBox?.remove();
        this.showAnnotations();
        this.citationModel.reset();
        this.canonicalUri = null;
      },

      /**
       * Inspects the MetadataView DOM to determine if a canonical dataset is
       * present based on schema.org:sameAs and prov:wasDerivedFrom annotations.
       * If a canonical dataset is detected, this method sets the appropriate
       * properties on the view instance.
       * @returns {boolean} True if a canonical dataset is detected, false
       * otherwise.
       */
      detectCanonicalDataset() {
        const matches = this.findCanonicalAnnotations();
        if (!matches) return false;
        this.canonicalUri = matches.uri;
        return true;
      },

      /**
       * Given a set annotation views for the sameAs property and a set of
       * annotation views for the derivedFrom property, this method finds any
       * matches between the two sets. A match is found if the URI of the sameAs
       * annotation is the same as the URI of the derivedFrom annotation.
       * @returns {{sameAs: AnnotationView, derivedFrom: AnnotationView, uri: string}}
       * An object containing the matching sameAs and derivedFrom annotation and
       * the URI they share.
       */
      findCanonicalAnnotations() {
        // The annotation views provide the URI and value of annotations on the
        // metadata. We consider the dataset to be canonical if the sameAs and
        // derivedFrom annotations both point to the same URI.
        const sameAs = this.getSameAsAnnotationViews();
        if (!sameAs?.length) return null;
        const derivedFrom = this.getDerivedFromAnnotationViews();
        if (!derivedFrom?.length) return null;

        const sameAsUnique = this.removeDuplicateAnnotations(sameAs);
        const derivedFromUnique = this.removeDuplicateAnnotations(derivedFrom);

        // Find any matches between the two sets
        const matches = [];
        sameAsUnique.forEach((sameAsAnno) => {
          derivedFromUnique.forEach((derivedFromAnno) => {
            if (sameAsAnno.value.uri === derivedFromAnno.value.uri) {
              matches.push({
                sameAs: sameAsAnno,
                derivedFrom: derivedFromAnno,
                uri: sameAsAnno.value.uri,
              });
            }
          });
        });
        // There can only be one canonical dataset. If multiple matches are
        // found, we cannot determine the canonical dataset.
        if (!matches.length || matches.length > 1) return null;
        return matches[0];
      },

      /**
       * Removes duplicate annotations from an array of AnnotationView instances.
       * @param {AnnotationView[]} annotationViews An array of AnnotationView all
       * with the same property URI.
       * @returns {AnnotationView[]} An array of AnnotationView instances with
       * duplicates removed.
       */
      removeDuplicateAnnotations(annotationViews) {
        return annotationViews.filter(
          (anno, i, self) =>
            i === self.findIndex((a) => a.value.uri === anno.value.uri),
        );
      },

      /**
       * Gets all annotation views from the MetadataView.
       * @returns {AnnotationView[]} An array of AnnotationView instances.
       */
      getAnnotationViews() {
        return this.metadataView[METADATA_VIEW.subviewProp].filter(
          (view) => view?.type === ANNO_VIEW_TYPE,
        );
      },

      /**
       * Gets the AnnotationView for the schema.org:sameAs annotation.
       * @returns {AnnotationView[]} An array of sameAs AnnotationViews.
       */
      getSameAsAnnotationViews() {
        return this.getAnnotationViews().filter(
          (view) => view.property.uri === SCHEMA_ORG_SAME_AS,
        );
      },

      /**
       * Gets the AnnotationView for the prov:wasDerivedFrom annotation.
       * @returns {AnnotationView[]} An array of derivedFrom AnnotationViews.
       */
      getDerivedFromAnnotationViews() {
        return this.getAnnotationViews().filter(
          (view) => view.property.uri === PROV_WAS_DERIVED_FROM,
        );
      },

      /**
       * Given the canonical dataset URI, fetches citation information for the
       * canonical dataset, like the title, authors, publication date, etc. Saves
       * this information in a CitationModel instance.
       */
      getCitationInfo() {
        const view = this;
        // Set the URL as the url for now, incase it is not a DOI or we fail to
        // fetch the citation information.
        this.citationModel.set({
          pid: this.canonicalUri,
          pid_url: this.canonicalUri,
        });

        this.crossRef = new CrossRefModel({
          doi: this.canonicalUri,
        });
        this.stopListening(this.crossRef);
        this.listenToOnce(this.crossRef, "sync", () => {
          view.citationModel.setSourceModel(this.crossRef);
          view.updateAlertBox();
        });
        this.crossRef.fetch();
      },

      /**
       * Hides the sameAs and derivedFrom annotations from the MetadataView.
       * This is done to prevent redundancy in the metadata display.
       */
      hideAnnotations() {
        // Sometimes the MetadataView re-renders, so we must always query for
        // the annotation views when we want to remove them.
        const sameAs = this.getSameAsAnnotationViews();
        const derivedFrom = this.getDerivedFromAnnotationViews();
        sameAs.forEach((sameAsAnno) => {
          if (sameAsAnno?.value.uri === this.canonicalUri) {
            const view = sameAsAnno;
            view.el.style.display = "none";
            if (!this.hiddenSameAs) this.hiddenSameAs = [];
            this.hiddenSameAs.push(sameAsAnno);
          }
        });
        derivedFrom.forEach((derivedFromAnno) => {
          if (derivedFromAnno?.value.uri === this.canonicalUri) {
            const view = derivedFromAnno;
            view.el.style.display = "none";
            if (!this.hiddenDerivedFrom) this.hiddenDerivedFrom = [];
            this.hiddenDerivedFrom.push(derivedFromAnno);
          }
        });
      },

      /** Show previously hidden annotations in the MetadataView. */
      showAnnotations() {
        this.hiddenSameAs?.el.style.removeProperty("display");
        this.hiddenSameAs = null;
        this.hiddenDerivedFrom?.el.style.removeProperty("display");
        this.hiddenDerivedFrom = null;
      },

      /**
       * Adds an alert box to the top of the MetadataView to indicate that the
       * dataset being displayed is a replica or minor variant of the original
       * dataset.
       * @returns {Element} The alert box element that was added to the view.
       */
      addAlertBox() {
        const controls = this.metadataView.el.querySelector(
          METADATA_VIEW.controlsSelector,
        );

        const alertBox = document.createElement("section");
        alertBox.classList.add(...CLASS_NAMES.alertBox);

        const icon = document.createElement("i");
        icon.classList.add(...CLASS_NAMES.alertIcon);

        const heading = document.createElement("h5");
        heading.textContent = ALERT_TEXT;

        // Add a div that will contain the citation information
        const citeContainer = document.createElement("div");
        citeContainer.classList.add(CLASS_NAMES.alertCitation);
        // Just add the URI for now
        citeContainer.textContent = this.canonicalUri;

        heading.prepend(icon);
        alertBox.append(heading, citeContainer);

        alertBox.style.marginTop = "-1rem";
        heading.style.marginTop = "0";
        citeContainer.style.marginLeft = "1rem";

        this.alertBox = alertBox;

        // Insert the citation view before the metadata controls
        controls.before(alertBox);

        return alertBox;
      },

      /** Updates the citation information in the alert box. */
      updateAlertBox() {
        const alertBox = this.alertBox || this.addAlertBox();
        const citeContainer = alertBox.querySelector(
          `.${CLASS_NAMES.alertCitation}`,
        );
        const { citationModel } = this;
        const citationView = new CitationView({
          model: citationModel,
          // Don't use styles from default class
          className: "",
          createTitleLink: false,
          openLinkInNewTab: true,
        }).render();
        citeContainer.innerHTML = "";
        citeContainer.appendChild(citationView.el);
      },

      /** Open the citation modal. */
      openCitationModal() {
        this.metadataView[METADATA_VIEW.citationModalProp].show();
      },

      /**
       * Modifies the CitationModalView to add the citation information for the
       * canonical dataset in addition to the citation information for the
       * current dataset.
       */
      modifyCitationModal() {
        const view = this;
        // The CitationModalView is recreated each time it is shown.
        const citationModalView =
          this.metadataView[METADATA_VIEW.citationModalProp];
        this.listenToOnce(citationModalView, "rendered", () => {
          citationModalView.canonicalDatasetMods = true;
          // Add heading for each citation
          const heading = document.createElement("h5");
          heading.textContent = CITATION_TITLE_DATAONE;
          citationModalView.citationContainer.prepend(heading);

          // Add the citation for the canonical dataset
          citationModalView.insertCitation(view.citationModel, false);

          // Add a heading for the canonical dataset citation
          const headingOriginal = document.createElement("h5");
          headingOriginal.textContent = CITATION_TITLE_CANONICAL;
          citationModalView.citationContainer.prepend(headingOriginal);
        });
      },

      /**
       * Adds a icon to the header of the MetadataView to indicate that the
       * dataset being displayed is essentially a duplicate
       * @returns {Element} The info icon element that was added to the view.
       */
      addInfoIcon() {
        const infoIcon = this.metadataView.addInfoIcon(
          "duplicate",
          INFO_ICON_NAME,
          INFO_ICON_CLASS,
          INFO_ICON_TOOLTIP_TEXT,
        );
        infoIcon.style.cursor = "pointer";
        infoIcon.addEventListener("click", () => this.openCitationModal());
        return infoIcon;
      },

      /** Called when the view is removed. */
      onClose() {
        this.reset();
        this.remove();
      },
    },
  );

  return CanonicalDatasetHandlerView;
});