"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) {
  const timestampIndex = inputTimestampArr.findIndex(
    (timestamp) => timestamp === timestampOfInterest
  );

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

/**
 * 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 = getIndexOfTimestamp(
    obsTimestampArr,
    startIso8601DateTimeString
  );
  const indexEndTimestamp = getIndexOfTimestamp(
    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
) {
  // 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`
    );
  }
};

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

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