"use strict";

/**
 * Create 24-hour time strings for a time interval delimited by a start time and an end time. It is assumed that the start time is at "00:00:00" and the end time is at "23:45:00" (when the sampling rate of observations is 15 min) or "23:00:00" (when the sampling rate of observations is 60 min)
 * @param {String} phenomenonSamplingRate The sampling rate of the phenomenon of interest represented as a string, e.g. "15min", "60min"
 * @returns {Array} An array of two 24-hour strings representing the start time and end time
 */
const createTimeStringsForInterval = function (phenomenonSamplingRate) {
  const fifteenMinutes = "15min";
  const sixtyMinutes = "60min";

  const startTime = "00:00:00";
  const endTimeFifteenMinutes = "23:45:00";
  const endTimeSixtyMinutes = "23:00:00";

  if (
    phenomenonSamplingRate !== fifteenMinutes &&
    phenomenonSamplingRate !== sixtyMinutes
  ) {
    throw new Error(
      `Check that the provided phenomenon sampling rate string is in this format: "15min" or "60min"`
    );
  }
  // 15 min sampling rate
  else if (phenomenonSamplingRate === fifteenMinutes) {
    return [startTime, endTimeFifteenMinutes];
  }
  // 60 min sampling rate
  else if (phenomenonSamplingRate === sixtyMinutes) {
    return [startTime, endTimeSixtyMinutes];
  }
};

/**
 * Create an ISO 8601 date and time string
 * @param {String} inputCalendarDate Calendar date string in "YYYY-MM-DD" format
 * @param {String} inputTwentyFourHourTime 24-hour time string in "hh:mm:ss" format
 * @returns {String} An ISO 8601 date and time string
 */
const createIso8601DateTimeString = function (
  inputCalendarDate,
  inputTwentyFourHourTime
) {
  return `${inputCalendarDate}T${inputTwentyFourHourTime}.000Z`;
};

/**
 * Check whether a year is a leap year or not
 * @param {Number} year Integer representing the year
 * @returns {Boolean} true if leap year, false if not
 */
const checkIfLeapYear = function (year) {
  return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
};

/**
 * Calculate the index of a timestamp in an array of timestamps
 * @param {Array} inputTimestampArr An array of timestamps, extracted from an array of observations
 * @param {String} timestampOfInterest A string representing the timestamp of interest in ISO 8601 format
 * @returns {Number} An integer representing the index of the timestamp of interest in the array of timestamps
 */
const getIndexOfTimestamp = function (inputTimestampArr, timestampOfInterest) {
  return inputTimestampArr.findIndex(
    (timestamp) => timestamp === timestampOfInterest
  );
};

/**
 * Calculate the index of a start timestamp in an array of timestamps
 *
 * @param {Array} inputTimestampArr An array of timestamps, extracted from an array of observations
 * @param {String} timestampOfInterest A string representing the timestamp of interest in ISO 8601 format
 * @returns {Number} An integer representing the index of the timestamp of interest in the array of timestamps
 */
const getIndexOfStartTimestamp = function (
  inputTimestampArr,
  timestampOfInterest
) {
  const timestampStartIndex = getIndexOfTimestamp(
    inputTimestampArr,
    timestampOfInterest
  );

  // If the timestamp does not exist in the timestamp array
  if (timestampStartIndex === -1) {
    throw new Error(
      "A start timestamp could not be found in the timestamp array"
    );
  }
  // If the timestamp exists in the timestamp array
  else {
    return timestampStartIndex;
  }
};

/**
 * Calculate the index of an end timestamp in an array of timestamps
 *
 * @param {Array} inputTimestampArr An array of timestamps, extracted from an array of observations
 * @param {String} timestampOfInterest A string representing the timestamp of interest in ISO 8601 format
 * @returns {Number} An integer representing the index of the timestamp of interest in the array of timestamps
 */
const getIndexOfEndTimestamp = function (
  inputTimestampArr,
  timestampOfInterest
) {
  const timestampEndIndex = getIndexOfTimestamp(
    inputTimestampArr,
    timestampOfInterest
  );

  // If the timestamp does not exist in the timestamp array
  if (timestampEndIndex === -1) {
    throw new Error(
      "An end timestamp could not be found in the timestamp array"
    );
  }
  // If the timestamp exists in the timestamp array
  else {
    return timestampEndIndex;
  }
};

/**
 * Calculate the indexes of the start and end timestamps
 * @param {Array} obsTimestampArr An array of observations timestamps
 * @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min"
 * @param {String} startDate A calendar date string in "YYYY-MM-DD" format representing the start date
 * @param {String} endDate A calendar date string in "YYYY-MM-DD" format representing the end date
 * @returns {Array} A 1*2 array tht contains integers representing the start index and end index respectively
 */
const calculateIndexStartEndTimestamp = function (
  obsTimestampArr,
  samplingRate,
  startDate,
  endDate
) {
  // Create and extract 24-hour strings for the start and end of interval
  const [startTimeString, endTimeString] =
    createTimeStringsForInterval(samplingRate);

  // Create ISO 8601 strings for the start and end of interval
  const startIso8601DateTimeString = createIso8601DateTimeString(
    startDate,
    startTimeString
  );
  const endIso8601DateTimeString = createIso8601DateTimeString(
    endDate,
    endTimeString
  );

  // Calculate the indexes of the timestamps for the start and end of interval
  const indexStartTimestamp = getIndexOfStartTimestamp(
    obsTimestampArr,
    startIso8601DateTimeString
  );
  const indexEndTimestamp = getIndexOfEndTimestamp(
    obsTimestampArr,
    endIso8601DateTimeString
  );

  return [indexStartTimestamp, indexEndTimestamp];
};

/**
 * Extract the set of observations that fall within a time interval delimited by a start date and end date. The start date may be the same as the end date.
 * @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API
 * @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min"
 * @param {String} startDate A calendar date string in "YYYY-MM-DD" format representing the start date
 * @param {String} endDate A calendar date string in "YYYY-MM-DD" format representing the end date
 * @returns {Array} An array of observations (timestamp + value) that fall within our time interval
 */
const extractObservationsWithinDatesInterval = function (
  obsArray,
  samplingRate,
  startDate,
  endDate
) {
  // Extract the timestamps from the observations
  const obsTimestampArr = obsArray.map((obs) => obs[0]);

  // Calculate the indexes of the timestamps for the start and end of interval
  const [indexStartTimestamp, indexEndTimestamp] =
    calculateIndexStartEndTimestamp(
      obsTimestampArr,
      samplingRate,
      startDate,
      endDate
    );

  // Extract the observations that fall within our time interval
  return obsArray.slice(indexStartTimestamp, indexEndTimestamp + 1);
};

/**
 * Extract the set of observation values that fall within a time interval delimited by a start date and end date. The start date may be the same as the end date.
 * @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API
 * @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min"
 * @param {String} startDate A calendar date string in "YYYY-MM-DD" format representing the start date
 * @param {String} endDate A calendar date string in "YYYY-MM-DD" format representing the end date
 * @returns {Array} An array of observation values that fall within our time interval
 */
const extractObservationValuesWithinDatesInterval = function (
  obsArray,
  samplingRate,
  startDate,
  endDate
) {
  // Extract the timestamps and values from the observations
  const obsTimestampArr = obsArray.map((obs) => obs[0]);
  const obsValuesArr = obsArray.map((obs) => obs[1]);

  // Calculate the indexes of the timestamps for the start and end of interval
  const [indexStartTimestamp, indexEndTimestamp] =
    calculateIndexStartEndTimestamp(
      obsTimestampArr,
      samplingRate,
      startDate,
      endDate
    );

  // Extract the observation values that fall within our time interval
  return obsValuesArr.slice(indexStartTimestamp, indexEndTimestamp + 1);
};

/**
 * Extract the year and month digits from a calendar month string
 * @param {String} calendarMonthStr Calendar month string in "YYYY-MM" format
 * @returns {Array} A 1*2 array tht contains integers representing the year and month respectively
 */
const extractMonthYearDigitsFromCalendarMonthString = function (
  calendarMonthStr
) {
  // Extract year as integer
  const yearNum = parseInt(calendarMonthStr.slice(0, 4), 10);

  // Extract month as integer
  const monthNum = parseInt(calendarMonthStr.slice(-2), 10);

  return [yearNum, monthNum];
};

/**
 * Extract the set of observation values that fall within a time interval delimited by the first day and last day of a calendar month
 * @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API
 * @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min"
 * @param {String} calendarMonthStr Calendar month string in "YYYY-MM" format
 * @returns {Array} An array of observation values that fall within one calendar month
 */
const extractObservationValuesWithinMonthInterval = function (
  obsArray,
  samplingRate,
  calendarMonthStr
) {
  try {
    // Extract the year and month digits from the calendar month string
    const [yearNum, monthNum] =
      extractMonthYearDigitsFromCalendarMonthString(calendarMonthStr);

    // All the months start on the first
    const startDateStr = `${calendarMonthStr}-01`;

    if (monthNum < 1 || monthNum > 12) {
      throw new Error(
        "The specified digit for the month of the year is invalid"
      );
    }
    // February
    else if (monthNum === 2) {
      // Leap year
      if (checkIfLeapYear(yearNum)) {
        return extractObservationValuesWithinDatesInterval(
          obsArray,
          samplingRate,
          startDateStr,
          `${calendarMonthStr}-29`
        );
      }
      // Non-leap year
      else {
        return extractObservationValuesWithinDatesInterval(
          obsArray,
          samplingRate,
          startDateStr,
          `${calendarMonthStr}-28`
        );
      }
    }
    // Months with 30 days
    else if (
      monthNum === 4 ||
      monthNum === 6 ||
      monthNum === 9 ||
      monthNum === 11
    ) {
      return extractObservationValuesWithinDatesInterval(
        obsArray,
        samplingRate,
        startDateStr,
        `${calendarMonthStr}-30`
      );
    }
    // Months with 31 days
    else {
      return extractObservationValuesWithinDatesInterval(
        obsArray,
        samplingRate,
        startDateStr,
        `${calendarMonthStr}-31`
      );
    }
  } catch (err) {
    // Rethrow errors from `getIndexOfStartTimestamp` or `getIndexOfEndTimestamp` functions
    throw new Error(
      `${err.message} \nCurrently, the monthly aggregation does not support partial calendar months`
    );
  }
};

/**
 * Extract unique calendar dates from date/time strings in ISO 8601 format
 * @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API
 * @returns {Array} An array of unique calendar date strings in "YYYY-MM-DD" format
 */
const extractUniqueCalendarDatesFromTimestamp = function (obsArray) {
  // The timestamp is the first element of the observation array
  const timestampArr = obsArray.map((obs) => obs[0]);

  // Extract the calendar date string from the timestamp string
  const calendarDateArr = timestampArr.map((timestamp) =>
    timestamp.slice(0, 10)
  );

  // Use a set to remove duplicates
  const uniqueCalendarDates = new Set(calendarDateArr);

  return [...uniqueCalendarDates];
};

/**
 * Extract unique calendar months from calendar date strings
 * @param {Array} calendarDatesArr An array of unique calendar date strings in "YYYY-MM-DD" format
 * @returns {Array} An array of unique calendar month strings in "YYYY-MM" format
 */
const extractUniqueCalendarMonthsFromCalendarDates = function (
  calendarDatesArr
) {
  // Extract the calendar month strings
  const calendarMonthsArr = calendarDatesArr.map((date) => date.slice(0, 7));

  // Use a set to remove duplicates
  const uniqueCalendarMonths = new Set(calendarMonthsArr);

  return [...uniqueCalendarMonths];
};

/**
 * Format a computed aggregation result to make it suitable for a chart. Currently, only line and column charts are supported
 * @param {Array} calendarDatesMonthsStrArr An array of unique calendar dates strings (in "YYYY-MM-DD" fromat) or unique calendar months strings (in "YYYY-MM" format)
 * @param {Array} aggregatedValuesArr An array of aggregated values
 * @returns {Array} An array of formatted aggregation values suitable for use in a column chart
 */
const formatAggregationResultForChart = function (
  calendarDatesMonthsStrArr,
  aggregatedValuesArr
) {
  if (!calendarDatesMonthsStrArr || !aggregatedValuesArr) return;

  // Create an array of Unix timestamp strings
  const timestampsArr = calendarDatesMonthsStrArr.map((calendarStr) =>
    new Date(calendarStr).getTime()
  );

  // Combine timestamp and value pairs
  // The timestamps array and values array have same lengths, use one for looping
  if (timestampsArr.length !== aggregatedValuesArr.length) {
    throw new Error(
      "The timestamps array and aggregated values array have different lengths"
    );
  } else {
    return timestampsArr.map((timestamp, i) => [
      timestamp,
      aggregatedValuesArr[i],
    ]);
  }
};

export {
  extractObservationsWithinDatesInterval,
  extractObservationValuesWithinDatesInterval,
  extractObservationValuesWithinMonthInterval,
  extractUniqueCalendarDatesFromTimestamp,
  extractUniqueCalendarMonthsFromCalendarDates,
  formatAggregationResultForChart,
};
