import { computeValue } from "../../../core";
import { TIME_UNITS } from "../../../types";
import { assert, isDate, isNil } from "../../../util";
import {
  adjustDate,
  dateAdd,
  dateDiffDay,
  dateDiffMonth,
  dateDiffQuarter,
  dateDiffWeek,
  dateDiffYear,
  DAYS_OF_WEEK_SET,
  DAYS_PER_WEEK,
  isoWeekday,
  parseTimezone,
  TIMEUNIT_IN_MILLIS
} from "./_internal";
const REF_DATE_MILLIS = 9466848e5;
const distanceToBinLowerBound = (value, binSize) => {
  let remainder = value % binSize;
  if (remainder < 0) {
    remainder += binSize;
  }
  return remainder;
};
const DATE_DIFF_FN = {
  day: dateDiffDay,
  month: dateDiffMonth,
  quarter: dateDiffQuarter,
  year: dateDiffYear
};
const $dateTrunc = (obj, expr, options) => {
  const {
    date,
    unit,
    binSize: optBinSize,
    timezone,
    startOfWeek: optStartOfWeek
  } = computeValue(obj, expr, null, options);
  if (isNil(date) || isNil(unit)) return null;
  const startOfWeek = (optStartOfWeek ?? "sun").toLowerCase().substring(0, 3);
  assert(
    isDate(date),
    "$dateTrunc: 'date' must resolve to a valid Date object."
  );
  assert(TIME_UNITS.includes(unit), "$dateTrunc: unit is invalid.");
  assert(
    unit != "week" || DAYS_OF_WEEK_SET.has(startOfWeek),
    `$dateTrunc: startOfWeek '${startOfWeek}' is not a valid.`
  );
  assert(
    isNil(optBinSize) || optBinSize > 0,
    "$dateTrunc requires 'binSize' to be greater than 0, but got value 0."
  );
  const binSize = optBinSize ?? 1;
  switch (unit) {
    case "millisecond":
    case "second":
    case "minute":
    case "hour": {
      const binSizeMillis = binSize * TIMEUNIT_IN_MILLIS[unit];
      const shiftedDate = date.getTime() - REF_DATE_MILLIS;
      return new Date(
        date.getTime() - distanceToBinLowerBound(shiftedDate, binSizeMillis)
      );
    }
    default: {
      assert(binSize <= 1e11, "dateTrunc unsupported binSize value");
      const d = new Date(date);
      const refPointDate = new Date(REF_DATE_MILLIS);
      let distanceFromRefPoint = 0;
      if (unit == "week") {
        const refPointDayOfWeek = isoWeekday(refPointDate, startOfWeek);
        const daysToAdjustBy = (DAYS_PER_WEEK - refPointDayOfWeek) % DAYS_PER_WEEK;
        refPointDate.setTime(
          refPointDate.getTime() + daysToAdjustBy * TIMEUNIT_IN_MILLIS.day
        );
        distanceFromRefPoint = dateDiffWeek(refPointDate, d, startOfWeek);
      } else {
        distanceFromRefPoint = DATE_DIFF_FN[unit](refPointDate, d);
      }
      const binLowerBoundFromRefPoint = distanceFromRefPoint - distanceToBinLowerBound(distanceFromRefPoint, binSize);
      const newDate = dateAdd(
        refPointDate,
        unit,
        binLowerBoundFromRefPoint,
        timezone
      );
      const minuteOffset = parseTimezone(timezone);
      adjustDate(newDate, -minuteOffset);
      return newDate;
    }
  }
};
export {
  $dateTrunc
};
