/**
 * @since 2.0.0
 */
import * as Array from "./Array.js";
import * as Either from "./Either.js";
import * as Equal from "./Equal.js";
import * as equivalence from "./Equivalence.js";
import { dual, pipe } from "./Function.js";
import * as Hash from "./Hash.js";
import { format, NodeInspectSymbol } from "./Inspectable.js";
import * as N from "./Number.js";
import { pipeArguments } from "./Pipeable.js";
import { hasProperty } from "./Predicate.js";
import * as String from "./String.js";
/**
 * @since 2.0.0
 * @category symbols
 */
export const TypeId = /*#__PURE__*/Symbol.for("effect/Cron");
const CronProto = {
  [TypeId]: TypeId,
  [Equal.symbol](that) {
    return isCron(that) && equals(this, that);
  },
  [Hash.symbol]() {
    return pipe(Hash.array(Array.fromIterable(this.minutes)), Hash.combine(Hash.array(Array.fromIterable(this.hours))), Hash.combine(Hash.array(Array.fromIterable(this.days))), Hash.combine(Hash.array(Array.fromIterable(this.months))), Hash.combine(Hash.array(Array.fromIterable(this.weekdays))), Hash.cached(this));
  },
  toString() {
    return format(this.toJSON());
  },
  toJSON() {
    return {
      _id: "Cron",
      minutes: Array.fromIterable(this.minutes),
      hours: Array.fromIterable(this.hours),
      days: Array.fromIterable(this.days),
      months: Array.fromIterable(this.months),
      weekdays: Array.fromIterable(this.weekdays)
    };
  },
  [NodeInspectSymbol]() {
    return this.toJSON();
  },
  pipe() {
    return pipeArguments(this, arguments);
  }
};
/**
 * Checks if a given value is a `Cron` instance.
 *
 * @param u - The value to check.
 *
 * @since 2.0.0
 * @category guards
 */
export const isCron = u => hasProperty(u, TypeId);
/**
 * Creates a `Cron` instance from.
 *
 * @param constraints - The cron constraints.
 *
 * @since 2.0.0
 * @category constructors
 */
export const make = ({
  days,
  hours,
  minutes,
  months,
  weekdays
}) => {
  const o = Object.create(CronProto);
  o.minutes = new Set(Array.sort(minutes, N.Order));
  o.hours = new Set(Array.sort(hours, N.Order));
  o.days = new Set(Array.sort(days, N.Order));
  o.months = new Set(Array.sort(months, N.Order));
  o.weekdays = new Set(Array.sort(weekdays, N.Order));
  return o;
};
/**
 * @since 2.0.0
 * @category symbol
 */
export const ParseErrorTypeId = /*#__PURE__*/Symbol.for("effect/Cron/errors/ParseError");
const ParseErrorProto = {
  _tag: "ParseError",
  [ParseErrorTypeId]: ParseErrorTypeId
};
const ParseError = (message, input) => {
  const o = Object.create(ParseErrorProto);
  o.message = message;
  if (input !== undefined) {
    o.input = input;
  }
  return o;
};
/**
 * Returns `true` if the specified value is an `ParseError`, `false` otherwise.
 *
 * @param u - The value to check.
 *
 * @since 2.0.0
 * @category guards
 */
export const isParseError = u => hasProperty(u, ParseErrorTypeId);
/**
 * Parses a cron expression into a `Cron` instance.
 *
 * @param cron - The cron expression to parse.
 *
 * @example
 * import * as Cron from "effect/Cron"
 * import * as Either from "effect/Either"
 *
 * // At 04:00 on every day-of-month from 8 through 14.
 * assert.deepStrictEqual(Cron.parse("0 4 8-14 * *"), Either.right(Cron.make({
 *   minutes: [0],
 *   hours: [4],
 *   days: [8, 9, 10, 11, 12, 13, 14],
 *   months: [],
 *   weekdays: []
 * })))
 *
 * @since 2.0.0
 * @category constructors
 */
export const parse = cron => {
  const segments = cron.split(" ").filter(String.isNonEmpty);
  if (segments.length !== 5) {
    return Either.left(ParseError(`Invalid number of segments in cron expression`, cron));
  }
  const [minutes, hours, days, months, weekdays] = segments;
  return Either.all({
    minutes: parseSegment(minutes, minuteOptions),
    hours: parseSegment(hours, hourOptions),
    days: parseSegment(days, dayOptions),
    months: parseSegment(months, monthOptions),
    weekdays: parseSegment(weekdays, weekdayOptions)
  }).pipe(Either.map(segments => make(segments)));
};
/**
 * Checks if a given `Date` falls within an active `Cron` time window.
 *
 * @param cron - The `Cron` instance.
 * @param date - The `Date` to check against.
 *
 * @example
 * import * as Cron from "effect/Cron"
 * import * as Either from "effect/Either"
 *
 * const cron = Either.getOrThrow(Cron.parse("0 4 8-14 * *"))
 * assert.deepStrictEqual(Cron.match(cron, new Date("2021-01-08 04:00:00")), true)
 * assert.deepStrictEqual(Cron.match(cron, new Date("2021-01-08 05:00:00")), false)
 *
 * @since 2.0.0
 */
export const match = (cron, date) => {
  const {
    days,
    hours,
    minutes,
    months,
    weekdays
  } = cron;
  const minute = date.getMinutes();
  if (minutes.size !== 0 && !minutes.has(minute)) {
    return false;
  }
  const hour = date.getHours();
  if (hours.size !== 0 && !hours.has(hour)) {
    return false;
  }
  const month = date.getMonth() + 1;
  if (months.size !== 0 && !months.has(month)) {
    return false;
  }
  if (days.size === 0 && weekdays.size === 0) {
    return true;
  }
  const day = date.getDate();
  if (weekdays.size === 0) {
    return days.has(day);
  }
  const weekday = date.getDay();
  if (days.size === 0) {
    return weekdays.has(weekday);
  }
  return days.has(day) || weekdays.has(weekday);
};
/**
 * Returns the next run `Date` for the given `Cron` instance.
 *
 * Uses the current time as a starting point if no value is provided for `now`.
 *
 * @example
 * import * as Cron from "effect/Cron"
 * import * as Either from "effect/Either"
 *
 * const after = new Date("2021-01-01 00:00:00")
 * const cron = Either.getOrThrow(Cron.parse("0 4 8-14 * *"))
 * assert.deepStrictEqual(Cron.next(cron, after), new Date("2021-01-08 04:00:00"))
 *
 * @param cron - The `Cron` instance.
 * @param now - The `Date` to start searching from.
 *
 * @since 2.0.0
 */
export const next = (cron, now) => {
  const {
    days,
    hours,
    minutes,
    months,
    weekdays
  } = cron;
  const restrictMinutes = minutes.size !== 0;
  const restrictHours = hours.size !== 0;
  const restrictDays = days.size !== 0;
  const restrictMonths = months.size !== 0;
  const restrictWeekdays = weekdays.size !== 0;
  const current = now ? new Date(now.getTime()) : new Date();
  // Increment by one minute to ensure we don't match the current date.
  current.setMinutes(current.getMinutes() + 1);
  current.setSeconds(0);
  current.setMilliseconds(0);
  // Only search 8 years into the future.
  const limit = new Date(current).setFullYear(current.getFullYear() + 8);
  while (current.getTime() <= limit) {
    if (restrictMonths && !months.has(current.getMonth() + 1)) {
      current.setMonth(current.getMonth() + 1);
      current.setDate(1);
      current.setHours(0);
      current.setMinutes(0);
      continue;
    }
    if (restrictDays && restrictWeekdays) {
      if (!days.has(current.getDate()) && !weekdays.has(current.getDay())) {
        current.setDate(current.getDate() + 1);
        current.setHours(0);
        current.setMinutes(0);
        continue;
      }
    } else if (restrictDays) {
      if (!days.has(current.getDate())) {
        current.setDate(current.getDate() + 1);
        current.setHours(0);
        current.setMinutes(0);
        continue;
      }
    } else if (restrictWeekdays) {
      if (!weekdays.has(current.getDay())) {
        current.setDate(current.getDate() + 1);
        current.setHours(0);
        current.setMinutes(0);
        continue;
      }
    }
    if (restrictHours && !hours.has(current.getHours())) {
      current.setHours(current.getHours() + 1);
      current.setMinutes(0);
      continue;
    }
    if (restrictMinutes && !minutes.has(current.getMinutes())) {
      current.setMinutes(current.getMinutes() + 1);
      continue;
    }
    return current;
  }
  throw new Error("Unable to find next cron date");
};
/**
 * Returns an `IterableIterator` which yields the sequence of `Date`s that match the `Cron` instance.
 *
 * @param cron - The `Cron` instance.
 * @param now - The `Date` to start searching from.
 *
 * @since 2.0.0
 */
export const sequence = function* (cron, now) {
  while (true) {
    yield now = next(cron, now);
  }
};
/**
 * @category instances
 * @since 2.0.0
 */
export const Equivalence = /*#__PURE__*/equivalence.make((self, that) => restrictionsEquals(self.minutes, that.minutes) && restrictionsEquals(self.hours, that.hours) && restrictionsEquals(self.days, that.days) && restrictionsEquals(self.months, that.months) && restrictionsEquals(self.weekdays, that.weekdays));
const restrictionsArrayEquals = /*#__PURE__*/equivalence.array(equivalence.number);
const restrictionsEquals = (self, that) => restrictionsArrayEquals(Array.fromIterable(self), Array.fromIterable(that));
/**
 * Checks if two `Cron`s are equal.
 *
 * @since 2.0.0
 * @category predicates
 */
export const equals = /*#__PURE__*/dual(2, (self, that) => Equivalence(self, that));
const minuteOptions = {
  segment: "minute",
  min: 0,
  max: 59
};
const hourOptions = {
  segment: "hour",
  min: 0,
  max: 23
};
const dayOptions = {
  segment: "day",
  min: 1,
  max: 31
};
const monthOptions = {
  segment: "month",
  min: 1,
  max: 12,
  aliases: {
    jan: 1,
    feb: 2,
    mar: 3,
    apr: 4,
    may: 5,
    jun: 6,
    jul: 7,
    aug: 8,
    sep: 9,
    oct: 10,
    nov: 11,
    dec: 12
  }
};
const weekdayOptions = {
  segment: "weekday",
  min: 0,
  max: 6,
  aliases: {
    sun: 0,
    mon: 1,
    tue: 2,
    wed: 3,
    thu: 4,
    fri: 5,
    sat: 6
  }
};
const parseSegment = (input, options) => {
  const capacity = options.max - options.min + 1;
  const values = new Set();
  const fields = input.split(",");
  for (const field of fields) {
    const [raw, step] = splitStep(field);
    if (raw === "*" && step === undefined) {
      return Either.right(new Set());
    }
    if (step !== undefined) {
      if (!Number.isInteger(step)) {
        return Either.left(ParseError(`Expected step value to be a positive integer`, input));
      }
      if (step < 1) {
        return Either.left(ParseError(`Expected step value to be greater than 0`, input));
      }
      if (step > options.max) {
        return Either.left(ParseError(`Expected step value to be less than ${options.max}`, input));
      }
    }
    if (raw === "*") {
      for (let i = options.min; i <= options.max; i += step ?? 1) {
        values.add(i);
      }
    } else {
      const [left, right] = splitRange(raw, options.aliases);
      if (!Number.isInteger(left)) {
        return Either.left(ParseError(`Expected a positive integer`, input));
      }
      if (left < options.min || left > options.max) {
        return Either.left(ParseError(`Expected a value between ${options.min} and ${options.max}`, input));
      }
      if (right === undefined) {
        values.add(left);
      } else {
        if (!Number.isInteger(right)) {
          return Either.left(ParseError(`Expected a positive integer`, input));
        }
        if (right < options.min || right > options.max) {
          return Either.left(ParseError(`Expected a value between ${options.min} and ${options.max}`, input));
        }
        if (left > right) {
          return Either.left(ParseError(`Invalid value range`, input));
        }
        for (let i = left; i <= right; i += step ?? 1) {
          values.add(i);
        }
      }
    }
    if (values.size >= capacity) {
      return Either.right(new Set());
    }
  }
  return Either.right(values);
};
const splitStep = input => {
  const seperator = input.indexOf("/");
  if (seperator !== -1) {
    return [input.slice(0, seperator), Number(input.slice(seperator + 1))];
  }
  return [input, undefined];
};
const splitRange = (input, aliases) => {
  const seperator = input.indexOf("-");
  if (seperator !== -1) {
    return [aliasOrValue(input.slice(0, seperator), aliases), aliasOrValue(input.slice(seperator + 1), aliases)];
  }
  return [aliasOrValue(input, aliases), undefined];
};
function aliasOrValue(field, aliases) {
  return aliases?.[field.toLocaleLowerCase()] ?? Number(field);
}
//# sourceMappingURL=Cron.js.map