"use strict";

import {
  chartExportOptions,
  convertHexColorToRGBColor,
  createCombinedTextDelimitedByAmpersand,
  createCombinedTextDelimitedByComma,
  extractSamplingRateFromDatastreamName,
} from "./chartHelpers.mjs";

/**
 * 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 reversedUniqueIndexesArr = uniqueIndexesArr.reverse();

  // Create a copy the larger observation array, will be modified in place
  const processedLargerObsArr = largerObsArr;

  reversedUniqueIndexesArr.forEach((index) => {
    if (index > -1) {
      processedLargerObsArr.splice(index, 1);
    }
  });

  return processedLargerObsArr;
};

/**
 * 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;

  if (firstArr.length > secondArr.length) return firstArr;

  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;

  if (firstArr.length < secondArr.length) return firstArr;

  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: obsArrayOne.length < obsArrayTwo.length
  if (obsArrayOne.length < obsArrayTwo.length) {
    const [biggerObsArr, smallerObsArr] =
      deleteUniqueObservationsFromLargerArray(obsArrayOne, obsArrayTwo);

    return [smallerObsArr, biggerObsArr];
  }

  //   Case 2: obsArrayOne.length > obsArrayTwo.length
  return deleteUniqueObservationsFromLargerArray(obsArrayOne, obsArrayTwo);
};

/**
 * Extracts and combines observation values from two input observation arrays of equal length
 * @param {Array} obsArrayOne First set of N observations (timestamp + value)
 * @param {Array} obsArrayTwo Second set of N observations (timestamp + value)
 * @returns {Array} A N*2 array of observation values from both input observation arrays
 */
const createCombinedObservationValues = function (obsArrayOne, obsArrayTwo) {
  // Extract the values from the two observation arrays
  const obsValuesOne = obsArrayOne.map((result) => result[1]);
  const obsValuesTwo = obsArrayTwo.map((result) => result[1]);

  //  Since the arrays are of equal length, we need only use one of the arrays for looping
  return obsValuesOne.map((obsValOne, i) => [obsValOne, obsValuesTwo[i]]);
};

/**
 * Format the response from SensorThings API to make it suitable for use in a scatter plot
 * @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} Array of formatted observations suitable for use in a scatter plot
 */
const formatSensorThingsApiResponseForScatterPlot = function (
  obsArrayOne,
  obsArrayTwo
) {
  // When our observation arrays have DIFFERENT lengths
  if (obsArrayOne.length !== obsArrayTwo.length) {
    const [obsArrayOneFinal, obsArrayTwoFinal] =
      checkForAndDeleteUniqueObservationsFromLargerArray(
        obsArrayOne,
        obsArrayTwo
      );

    return createCombinedObservationValues(obsArrayOneFinal, obsArrayTwoFinal);
  }

  // When our observation arrays already have SAME lengths
  return createCombinedObservationValues(obsArrayOne, obsArrayTwo);
};

/**
 * Concatenates metadata properties to create a string for either the title or subtitle of a scatter plot
 * @param {Array} phenomenonNamesArr An array of phenomenon name strings
 * @returns {String} A string made up of combined phenomenon names
 */
const createCombinedTextForScatterPlotTitles = function (phenomenonNamesArr) {
  // x-axis phenomenon name is the first element of array
  const phenomenonNameXAxis = phenomenonNamesArr[0];

  // y-axis phenomenon name(s) array is remaining elements of array
  const phenomenonNamesYAxisArr = phenomenonNamesArr.slice(1);
  // Use a set to remove duplicates
  const uniquePhenomenonNamesYAxis = new Set(phenomenonNamesYAxisArr);
  const uniquePhenomenonNamesYAxisArr = [...uniquePhenomenonNamesYAxis];

  return `${createCombinedTextDelimitedByAmpersand(
    uniquePhenomenonNamesYAxisArr
  )} versus ${phenomenonNameXAxis}`;
};

/**
 * Create string for the x-axis title of a scatter plot
 * @param {Array} phenomenonNamesArr Array of phenomenon name strings
 * @param {Array} unitOfMeasurementSymbolsArr rray of unit of measurement symbol strings
 * @returns {String} X-axis title string for scatter plot
 */
const createXAxisTitleTextScatterPlot = function (
  phenomenonNamesArr,
  unitOfMeasurementSymbolsArr
) {
  // x-axis phenomenon name string is first element of array
  const phenomenonNameXAxis = phenomenonNamesArr[0];

  // x-axis phenomenon symbol string is first element of array
  const unitOfMeasurementSymbolXAxis = unitOfMeasurementSymbolsArr[0];

  return `${phenomenonNameXAxis} [${unitOfMeasurementSymbolXAxis}]`;
};

/**
 * Create string for the y-axis title of a scatter plot
 * @param {Array} phenomenonNamesArr Array of phenomenon name strings
 * @param {Array} unitOfMeasurementSymbolsArr Array of unit of measurement symbol strings
 * @returns {String} Y-axis title string for scatter plot
 */
const createYAxisTitleTextScatterPlot = function (
  phenomenonNamesArr,
  unitOfMeasurementSymbolsArr
) {
  // y-axis phenomenon names start at array index 1
  const phenomenonNamesYAxisArr = phenomenonNamesArr.slice(1);

  // y-axis phenomenon symbols start at array index 1
  const unitOfMeasurementSymbolsYAxisArr = unitOfMeasurementSymbolsArr.slice(1);

  // The phenomenon names and unit of measurement arrays should have equal lengths
  // Use one of the arrays for looping
  if (
    phenomenonNamesYAxisArr.length !== unitOfMeasurementSymbolsYAxisArr.length
  )
    throw new Error(
      "The phenomenon names array and unit of measurement symbols array have different lengths"
    );

  const combinedNameSymbolArr = phenomenonNamesYAxisArr.map(
    (phenomenonNameYAxis, i) =>
      `${phenomenonNameYAxis} [${unitOfMeasurementSymbolsYAxisArr[i]}]`
  );

  return createCombinedTextDelimitedByComma(combinedNameSymbolArr);
};

/**
 * Create an options object for each series drawn in the scatter plot
 * @param {Array} formattedObsArraysForScatterPlot An array of formatted observation array(s) from one or more datastreams
 * @param {Array} phenomenonNamesArr An array of phenomenon name(s)
 * @returns {Array} An array made up of series options object(s)
 */
const createSeriesOptionsForScatterPlot = function (
  formattedObsArraysForScatterPlot,
  phenomenonNamesArr
) {
  // An array of colors, in hexadecimal format, provided by the global Highcharts object
  const highchartsColorsArr = Highcharts.getOptions().colors;

  // Create a reversed copy of the colors array
  const highchartsColorsReversedArr = [...highchartsColorsArr].reverse();

  // Opacity value for symbol
  const SERIES_SYMBOL_COLOR_OPACITY = ".3";

  // Create array of colors in RGBA format
  const seriesColors = highchartsColorsReversedArr.map(
    (hexColorCode) =>
      `rgba(${convertHexColorToRGBColor(
        hexColorCode
      )}, ${SERIES_SYMBOL_COLOR_OPACITY})`
  );

  // x-axis phenomenon name is the first element of array
  const phenomenonNameXAxis = phenomenonNamesArr[0];
  // y-axis phenomenon name(s) array is remaining elements of array
  const phenomenonNamesYAxisArr = phenomenonNamesArr.slice(1);

  // Create an array of seriesOptions objects
  // Assumes that the observation array of arrays and phenomenon names array are of equal length
  // Use one of the arrays for looping
  if (
    formattedObsArraysForScatterPlot.length !== phenomenonNamesYAxisArr.length
  )
    throw new Error(
      "The observations array and phenomenon names array have different lengths"
    );

  return formattedObsArraysForScatterPlot.map((formattedObsArray, i) => {
    return {
      name: `${phenomenonNamesYAxisArr[i]}, ${phenomenonNameXAxis}`,
      data: formattedObsArray,
      color: seriesColors[i],
    };
  });
};

/**
 * Draw a scatter plot using Highcharts library
 * @param {Array} formattedObsArrayForSeriesOnePlusSeriesTwo Response from SensorThings API formatted for use in a scatter plot
 * @param {Object} extractedFormattedDatastreamProperties An object that contains arrays of formatted Datastream properties
 * @returns {undefined} undefined
 */
const drawScatterPlotHighcharts = function (
  formattedObsArrayForSeriesOnePlusSeriesTwo,
  extractedFormattedDatastreamProperties
) {
  // Arrays of datastream properties
  const {
    datastreamDescriptionsArr,
    datastreamNamesArr,
    phenomenonNamesArr,
    unitOfMeasurementSymbolsArr,
  } = extractedFormattedDatastreamProperties;

  // Create the array of series options object(s)
  const seriesOptionsArr = createSeriesOptionsForScatterPlot(
    formattedObsArrayForSeriesOnePlusSeriesTwo,
    phenomenonNamesArr
  );

  const CHART_TITLE =
    createCombinedTextForScatterPlotTitles(phenomenonNamesArr);

  const CHART_SUBTITLE = `Sampling rate(s): ${createCombinedTextDelimitedByComma(
    extractSamplingRateFromDatastreamName(datastreamNamesArr)
  )}`;

  const X_AXIS_TITLE = createXAxisTitleTextScatterPlot(
    phenomenonNamesArr,
    unitOfMeasurementSymbolsArr
  );

  const Y_AXIS_TITLE = createYAxisTitleTextScatterPlot(
    phenomenonNamesArr,
    unitOfMeasurementSymbolsArr
  );

  // The unit of measurement symbols for the x-axis is the first element of the array
  // Assume that we will be comparing similar phenomena, so we can reuse this symbol
  const UNIT_OF_MEASUREMENT_SYMBOL = unitOfMeasurementSymbolsArr[0];

  const MARKER_RADIUS = 2;

  Highcharts.chart("chart-scatter-plot", {
    chart: {
      type: "scatter",
      zoomType: "xy",
    },

    boost: {
      useGPUTranslations: true,
      usePreAllocated: true,
    },

    title: {
      text: CHART_TITLE,
      "align": "left",
    },

    subtitle: {
      text: CHART_SUBTITLE,
      "align": "left",
    },

    xAxis: {
      labels: {
        format: `{value}`,
      },
      title: {
        enabled: true,
        text: X_AXIS_TITLE,
      },
      startOnTick: true,
      endOnTick: true,
      showLastLabel: true,
    },

    yAxis: [
      {
        labels: {
          format: `{value}`,
        },
        title: {
          text: Y_AXIS_TITLE,
        },
      },
    ],

    legend: {
      enabled: false,
    },

    plotOptions: {
      scatter: {
        marker: {
          radius: MARKER_RADIUS,
          states: {
            hover: {
              enabled: true,
              lineColor: "rgb(100,100,100)",
            },
          },
        },
        states: {
          hover: {
            marker: {
              enabled: false,
            },
          },
        },
      },
    },

    tooltip: {
      formatter() {
        const headerString = `${this.series.name}<br>`;
        const pointString = `<b>${this.point.y.toFixed(
          2
        )} ${UNIT_OF_MEASUREMENT_SYMBOL}, ${this.point.x.toFixed(
          2
        )} ${UNIT_OF_MEASUREMENT_SYMBOL}</b>`;
        return headerString + pointString;
      },
    },

    exporting: chartExportOptions,

    series: seriesOptionsArr,
  });
};

export {
  formatSensorThingsApiResponseForScatterPlot,
  drawScatterPlotHighcharts,
};
