"use strict";

const chartExportOptions = {
  buttons: {
    contextButton: {
      menuItems: ["downloadPNG", "downloadJPEG", "downloadPDF", "downloadSVG"],
    },
  },
};

/**
 * Determines the timestamps that are missing from a smaller set of observations. Based on the comparison of two observation arrays, where one array is larger than the other
 * @param {Array} obsTimestampArrayOne An array of timestamps for the first set of observations
 * @param {Array} obsTimestampArrayTwo An array of timstamps for the second set of observations
 * @returns {Array} An array of timestamps missing from either set of observations
 */
const getSymmetricDifferenceBetweenArrays = function (
  obsTimestampArrayOne,
  obsTimestampArrayTwo
) {
  return obsTimestampArrayOne
    .filter((timestampOne) => !obsTimestampArrayTwo.includes(timestampOne))
    .concat(
      obsTimestampArrayTwo.filter(
        (timestampTwo) => !obsTimestampArrayOne.includes(timestampTwo)
      )
    );
};

/**
 * Determines the indexes of timestamps that are unique to the larger set of observatiuons. Based on the comparison of two observation arrays, where one array is larger than the other
 * @param {Array} uniqueTimestampsArr An array of timestamps unique to the larger set of observations
 * @param {Array} largerObsTimestampArr An array of timestamps for the larger set of observations
 * @returns {Array} An array of the indexes of the missing observations
 */
const getIndexesOfUniqueObservations = function (
  uniqueTimestampsArr,
  largerObsTimestampArr
) {
  return uniqueTimestampsArr.map((index) =>
    largerObsTimestampArr.indexOf(index)
  );
};

/**
 * Removes observations (by modifying array in place) that are unique to a larger set of observations. Based on the comparison of two observation arrays, where one array is larger than the other
 * @param {Array} uniqueIndexesArr An array of the indexes unique to the larger set of observations
 * @param {Array} largerObsArr The larger array of observations (timestamp + value)
 * @returns {Array} The larger array with the unique indexes removed
 */
const removeUniqueObservationsFromLargerArray = function (
  uniqueIndexesArr,
  largerObsArr
) {
  // Create a reversed copy of the indexes array, so that the larger index is removed first
  const uniqueIndexesReversedCopyArr = [...uniqueIndexesArr].reverse();

  // Create a copy of the larger observation array, will be modified in place
  const largerObsCopyArr = [...largerObsArr];

  uniqueIndexesReversedCopyArr.forEach((uniqueIndex) => {
    if (uniqueIndex > -1) {
      largerObsCopyArr.splice(uniqueIndex, 1);
    }
  });

  return largerObsCopyArr;
};

/**
 * Compares the length of two input arrays to determine the larger one
 * @param {Array} firstArr First input array
 * @param {Array} secondArr Second input array
 * @returns {Array} The larger array
 */
const getLargerArrayBetweenTwoInputArrays = function (firstArr, secondArr) {
  if (firstArr.length === secondArr.length) return;
  else if (firstArr.length > secondArr.length) {
    return firstArr;
  } else if (firstArr.length < secondArr.length) {
    return secondArr;
  }
};

/**
 * Compares the length of two input arrays to determine the smaller one
 * @param {Array} firstArr First input array
 * @param {Array} secondArr Second input array
 * @returns {Array} The smaller array
 */
const getSmallerArrayBetweenTwoInputArrays = function (firstArr, secondArr) {
  if (firstArr.length === secondArr.length) return;
  else if (firstArr.length < secondArr.length) {
    return firstArr;
  } else if (firstArr.length > secondArr.length) {
    return secondArr;
  }
};

/**
 * Utility function for deleting the unique observations from a larger array
 * @param {Array} obsArrayOne Array of observations (timestamp + value) that is response from SensorThings API
 * @param {Array} obsArrayTwo Array of observations (timestamp + value) that is response from SensorThings API
 * @returns {Array} Two arrays of observations (timestamp + value) with matching timestamps and equal lengths
 */
const deleteUniqueObservationsFromLargerArray = function (
  obsArrayOne,
  obsArrayTwo
) {
  // Create arrays with timestamps only
  const obsArrayOneTimestamp = obsArrayOne.map(
    (obsTimeValue) => obsTimeValue[0]
  );
  const obsArrayTwoTimestamp = obsArrayTwo.map(
    (obsTimeValue) => obsTimeValue[0]
  );

  const missingTimestamp = getSymmetricDifferenceBetweenArrays(
    obsArrayOneTimestamp,
    obsArrayTwoTimestamp
  );

  // Determine the larger observation timestamp array
  const biggerObsTimestampArr = getLargerArrayBetweenTwoInputArrays(
    obsArrayOneTimestamp,
    obsArrayTwoTimestamp
  );

  // Indexes of the missing observations
  const indexesMissingObsArr = getIndexesOfUniqueObservations(
    missingTimestamp,
    biggerObsTimestampArr
  );

  // Determine the larger observation array
  const biggerObsArr = getLargerArrayBetweenTwoInputArrays(
    obsArrayOne,
    obsArrayTwo
  );

  // Determine the smaller observation array
  const smallerObsArr = getSmallerArrayBetweenTwoInputArrays(
    obsArrayOne,
    obsArrayTwo
  );

  // Remove the missing observation from the larger array of observations
  const modifiedBiggerObsArr = removeUniqueObservationsFromLargerArray(
    indexesMissingObsArr,
    biggerObsArr
  );

  return [modifiedBiggerObsArr, smallerObsArr];
};

/**
 * Utility function for deleting the unique observations from a larger array AND ensuring the order of input arrays is maintained
 * @param {Array} obsArrayOne Array of observations (timestamp + value) that is response from SensorThings API
 * @param {Array} obsArrayTwo Array of observations (timestamp + value) that is response from SensorThings API
 * @returns {Array} Two arrays of observations (timestamp + value) with matching timestamps and equal lengths
 */
const checkForAndDeleteUniqueObservationsFromLargerArray = function (
  obsArrayOne,
  obsArrayTwo
) {
  if (obsArrayOne.length === obsArrayTwo.length) return;
  //   Case 1: obsArrayTwo larger than obsArrayOne
  else if (obsArrayOne.length < obsArrayTwo.length) {
    const [biggerObsArr, smallerObsArr] =
      deleteUniqueObservationsFromLargerArray(obsArrayOne, obsArrayTwo);

    return [smallerObsArr, biggerObsArr];
  }
  //   Case 2: obsArrayOne larger than obsArrayTwo
  else if (obsArrayOne.length > obsArrayTwo.length) {
    return deleteUniqueObservationsFromLargerArray(obsArrayOne, obsArrayTwo);
  }
};

/**
 * Format the response from SensorThings API to make it suitable for use in a line chart or column chart
 * @param {Array} obsArray Array of observations (timestamp + value) that is response from SensorThings API
 * @returns {Array} Array of formatted observations suitable for use in a line chart
 */
const formatSensorThingsApiResponseForLineOrColumnChart = function (obsArray) {
  if (!obsArray) return;

  return obsArray.map((result) => {
    const timestampObs = new Date(result[0].slice(0, -1)).getTime(); // slice() removes trailing "Z" character in timestamp
    const valueObs = result[1];
    return [timestampObs, valueObs];
  });
};

/**
 * Convert a hexadecimal color code obtained from the Highcharts object (`Highcharts.getOptions().colors`) to its equivalent RGB color code
 * @param {String} hexCode Input hex color code
 * @returns {String} Output RGB color code
 */
const convertHexColorToRGBColor = function (hexCode) {
  const hexToRGBMapping = {
    "#7cb5ec": "rgb(124, 181, 236)",
    "#434348": "rgb(67, 67, 72)",
    "#90ed7d": "rgb(144, 237, 125)",
    "#f7a35c": "rgb(247, 163, 92)",
    "#8085e9": "rgb(128, 133, 233)",
    "#f15c80": "rgb(241, 92, 128)",
    "#e4d354": "rgb(228, 211, 84)",
    "#2b908f": "rgb(228, 211, 84)",
    "#f45b5b": "rgb(244, 91, 91)",
    "#91e8e1": "rgb(145, 232, 225)",
  };

  if (hexToRGBMapping?.[hexCode] === undefined) {
    throw new Error(
      "The provided hex code is not valid or is not supported by this function"
    );
  }
  // Extract the RGB color elements as a single string
  // The individual color elements are separated by commas
  else {
    return (hexToRGBMapping?.[hexCode]).slice(4, -1);
  }
};

/**
 * Concatenates metadata properties into a single string with an ampersand as the delimiter
 * @param {Array} metadataPropertiesArr An array of metadata property strings
 * @returns {String} A string made up of combined metadata properties delimited by an ampersand
 */
const createCombinedTextDelimitedByAmpersand = function (
  metadataPropertiesArr
) {
  return metadataPropertiesArr.join(" & ");
};

/**
 * Concatenates metadata properties into a single string with a comma as the delimiter
 * @param {Array} metadataPropertiesArr An array of metadata property strings
 * @returns {String} A string made up of combined metadata properties delimited by a comma
 */
const createCombinedTextDelimitedByComma = function (metadataPropertiesArr) {
  return metadataPropertiesArr.join(", ");
};

/**
 * Extracts the sampling rate substring from a datastream name string
 * @param {Array} datastreamNamesArr An array of datastream name(s)
 * @returns {Array} An array containing the sampling rate substring(s)
 */
const extractSamplingRateFromDatastreamName = function (datastreamNamesArr) {
  // First split the Datastream name string based on a single space (" ").
  // The sampling rate string is the last word in the resulting string.
  // We then split the resulting string using the ':' character.
  // Our interest is also in the last word in the resulting string
  return datastreamNamesArr.map((datastreamName) =>
    datastreamName.split(" ").pop().split(":").pop()
  );
};

/**
 * Extract the building ID substring from a datastream name string
 *
 * @param {Array} datastreamNamesArr An array of datastream name(s)
 * @returns {Array} An array containing the building ID substring(s)
 */
const extractBuildingIdFromDatastreamName = function (datastreamNamesArr) {
  // The building ID string is the first word in the Datastream name string
  return datastreamNamesArr.map((datastreamName) =>
    datastreamName.split(" ").shift()
  );
};

/**
 * Create a partial string for a line chart or column chart title
 * @param {String} aggregationInterval The aggregation interval as a string, either "daily" or "monthly"
 * @param {String} aggregationType The aggregation type as a string, either "sum" or "average"
 * @returns {String} Partial string for chart title
 */
const createPartialTitleForLineOrColumnChart = function (
  aggregationInterval,
  aggregationType
) {
  // Case 1: No aggregation; return empty string
  if (!aggregationInterval && !aggregationType) return ``;
  // Case 2: Aggregation; capitalize the first characters
  else {
    return `${
      aggregationInterval.slice(0, 1).toUpperCase() +
      aggregationInterval.slice(1)
    } ${aggregationType.slice(0, 1).toUpperCase() + aggregationType.slice(1)}`;
  }
};

/**
 * Create a full string for a line chart or column chart title
 * @param {Array} phenomenonNamesArr An array of phenomenon names as strings
 * @param {String} aggregationInterval The aggregation interval as a string, either "daily" or "monthly"
 * @param {String} aggregationType The aggregation type as a string, either "sum" or "average"
 * @returns {String} Full string for chart title
 */
const createFullTitleForLineOrColumnChart = function (
  phenomenonNamesArr,
  aggregationInterval,
  aggregationType
) {
  // Case 1: No aggregation; create a comma separated string of phenomenon names
  if (!aggregationInterval && !aggregationType)
    return `${createPartialTitleForLineOrColumnChart(
      aggregationInterval,
      aggregationType
    )}${createCombinedTextDelimitedByComma(phenomenonNamesArr)}`;
  // Case 2: Aggregation
  else {
    return `${createPartialTitleForLineOrColumnChart(
      aggregationInterval,
      aggregationType
    )}: ${createCombinedTextDelimitedByComma(phenomenonNamesArr)}`;
  }
};

/**
 * Create a title for a heatmap
 *
 * @param {Array} phenomenonNamesArr An array of phenomenon names as strings
 * @returns {String} A string that represents the heatmap title
 */
const createTitleForHeatmap = function (phenomenonNamesArr) {
  return createCombinedTextDelimitedByComma(phenomenonNamesArr);
};

/**
 * Create a subtitle for the following charts: column chart, line chart and scatter plot
 *
 * @param {Array} datastreamNamesArr An array of datastream name(s)
 * @returns {String} A subtitle string
 */
const createSubtitleForChart = function (datastreamNamesArr) {
  // Case 1: We only have one sampling rate string
  if (datastreamNamesArr.length === 1) {
    return `Sampling rate: ${createCombinedTextDelimitedByComma(
      extractSamplingRateFromDatastreamName(datastreamNamesArr)
    )}`;
  }
  // Case 2: We have more than one sampling rate string
  else if (datastreamNamesArr.length > 1) {
    return `Sampling rate(s): ${createCombinedTextDelimitedByComma(
      extractSamplingRateFromDatastreamName(datastreamNamesArr)
    )} respectively`;
  }
};

/**
 * Create a subtitle for a heatmap which is different from the subtitles used for the other
 * types of charts, i.e. column charts, line charts and scatter plots
 *
 * @param {Array} datastreamNamesArr An array of datastream name(s)
 * @returns {String} A subtitle string
 */
const createSubtitleForHeatmap = function (datastreamNamesArr) {
  // Note: the `datastreamNamesArr` here contains only one element
  // We use the `createCombinedTextDelimitedByComma` function to "spread" the resulting arrays
  return `Building, Sampling rate: ${createCombinedTextDelimitedByComma(
    extractBuildingIdFromDatastreamName(datastreamNamesArr)
  )}, ${createCombinedTextDelimitedByComma(
    extractSamplingRateFromDatastreamName(datastreamNamesArr)
  )}`;
};

/**
 * Abbreviate temperature phenomenon names for use in chart y-axis title strings where space is limited
 *
 * @param {Array} phenomenonNamesArr An array of phenomenon name strings
 * @returns {Array} An array that contains abbreviated temperature phenomenon strings
 */
const abbreviateTemperaturePhenomenonNames = function (phenomenonNamesArr) {
  // We're interested in phenomenon names that contain the substrings
  // `temperature` or `Temperature`
  return phenomenonNamesArr.map((phenomenonName) => {
    // Case 1: Temperature phenomenon name string variant 1
    if (phenomenonName.includes("temperature")) {
      return phenomenonName.replace("temperature", "temp.");
    }
    // Case 2: Temperature phenomenon name string variant 2
    else if (phenomenonName.includes("Temperature")) {
      return phenomenonName.replace("Temperature", "Temp.");
    }
    // Case 3: The other phenomenon name strings
    else {
      return phenomenonName;
    }
  });
};

/**
 * Creates a date string that is used in a shared tooltip for a line or column chart
 * @param {Number} pointXAxisValue The x-axis value (Unix timestamp) which is common for a set of data points
 * @param {String} aggregationInterval The aggregation interval as a string, either "daily" or "monthly"
 * @returns {String} A calendar date or calendar month string that is common for a set of data points
 */
const createTooltipDateString = function (
  pointXAxisValue,
  aggregationInterval
) {
  if (aggregationInterval === undefined || aggregationInterval === "daily")
    // When `aggregationInterval === undefined`, assume that we are displaying raw observations
    return `${Highcharts.dateFormat("%A, %b %e, %Y", pointXAxisValue)}`;
  else if (aggregationInterval === "monthly")
    return `${Highcharts.dateFormat("%b %Y", pointXAxisValue)}`;
};

/**
 * Remove the transparency (alpha channel) from a color
 * @param {String} rgbaColor A color expressed in RGBA format
 * @returns {String} A color in RGB format
 */
const removeTransparencyFromColor = function (rgbaColor) {
  return `rgb(${rgbaColor.slice(5, -5)})`;
};

export {
  chartExportOptions,
  checkForAndDeleteUniqueObservationsFromLargerArray,
  formatSensorThingsApiResponseForLineOrColumnChart,
  createCombinedTextDelimitedByAmpersand,
  createCombinedTextDelimitedByComma,
  extractSamplingRateFromDatastreamName,
  extractBuildingIdFromDatastreamName,
  createFullTitleForLineOrColumnChart,
  createTitleForHeatmap,
  createSubtitleForChart,
  createSubtitleForHeatmap,
  abbreviateTemperaturePhenomenonNames,
  createTooltipDateString,
  convertHexColorToRGBColor,
  removeTransparencyFromColor,
};
