// DOS date/time conversion for ZIP entries.
//
// ZIP stores modification time as two packed 16-bit fields (APPNOTE 4.4.6):
//   time  bits 15-11 hours (0-23), 10-5 minutes (0-59), 4-0 seconds/2 (0-29)
//   date  bits 15-9  year-1980 (0-127), 8-5 month (1-12), 4-0 day (1-31)
// These helpers convert to and from Unix seconds (UTC). Only the two conversions
// the encoder/decoder needs are defined here, and both are private to the package.

///|
/// Days per month, indexed 0 (January) .. 11 (December), for a common year.
let days_in_month_common : FixedArray[Int] = [
  31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
]

///|
/// Days per month for a leap year (February has 29).
let days_in_month_leap : FixedArray[Int] = [
  31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
]

///|
fn days_in_months(is_leap : Bool) -> FixedArray[Int] {
  if is_leap {
    days_in_month_leap
  } else {
    days_in_month_common
  }
}

///|
fn is_leap_year(year : Int) -> Bool {
  (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}

///|
/// Convert a Unix timestamp (UTC seconds) to packed DOS (time, date) fields.
/// Timestamps at or before the DOS epoch clamp to 1980-01-01 00:00:00.
fn unix_to_dos_datetime(unix_timestamp : Int) -> (Int, Int) {
  if unix_timestamp <= dos_epoch {
    return (0, 0x0021) // 1980-01-01 00:00:00
  }
  let seconds_since_dos_epoch = unix_timestamp - dos_epoch
  let days_since_dos_epoch = seconds_since_dos_epoch / 86400
  let seconds_in_day = seconds_since_dos_epoch % 86400
  let hours = seconds_in_day / 3600
  let minutes = seconds_in_day % 3600 / 60
  let seconds = seconds_in_day % 60
  let dos_time = (hours << 11) | (minutes << 5) | (seconds / 2)

  // Walk whole years off the day count, then resolve month/day within the year.
  let (year, remaining_days) = for year = 1980, remaining_days = days_since_dos_epoch; remaining_days >=
                                  365; {
    let days_in_year = if is_leap_year(year) { 366 } else { 365 }
    if remaining_days >= days_in_year {
      continue year + 1, remaining_days - days_in_year
    } else {
      break (year, remaining_days)
    }
  } nobreak {
    (year, remaining_days)
  }
  let (month, day) = day_of_year_to_month_day(
    remaining_days + 1,
    is_leap_year(year),
  )
  let dos_date = ((year - 1980) << 9) | (month << 5) | day
  (dos_time, dos_date)
}

///|
/// Convert packed DOS (time, date) fields back to a Unix timestamp (UTC
/// seconds). Out-of-range fields fall back to the DOS epoch.
fn dos_datetime_to_unix(dos_time : Int, dos_date : Int) -> Int {
  let hours = (dos_time >> 11) & 0x1f
  let minutes = (dos_time >> 5) & 0x3f
  let seconds = (dos_time & 0x1f) * 2
  let year_offset = (dos_date >> 9) & 0x7f
  let month = (dos_date >> 5) & 0x0f
  let day = dos_date & 0x1f
  let year = 1980 + year_offset
  if month < 1 ||
    month > 12 ||
    day < 1 ||
    day > 31 ||
    hours > 23 ||
    minutes > 59 ||
    seconds > 59 {
    return dos_epoch
  }
  let year_days = for y = 1980, acc = 0; y < year; {
    continue y + 1, acc + (if is_leap_year(y) { 366 } else { 365 })
  } nobreak {
    acc
  }
  let days = year_days +
    month_day_to_day_of_year(month, day, is_leap_year(year)) -
    1
  dos_epoch + days * 86400 + hours * 3600 + minutes * 60 + seconds
}

///|
/// Day-of-year (1-based) to (month, day).
fn day_of_year_to_month_day(day_of_year : Int, is_leap : Bool) -> (Int, Int) {
  let months = days_in_months(is_leap)
  for i = 0, remaining_days = day_of_year; i < months.length(); {
    if remaining_days <= months[i] {
      break (i + 1, remaining_days)
    }
    continue i + 1, remaining_days - months[i]
  } nobreak {
    (12, 31) // fallback for an out-of-range day-of-year
  }
}

///|
/// (month, day) to day-of-year (1-based).
fn month_day_to_day_of_year(month : Int, day : Int, is_leap : Bool) -> Int {
  let months = days_in_months(is_leap)
  for i = 0, day_of_year = day; i < month - 1 && i < months.length(); {
    continue i + 1, day_of_year + months[i]
  } nobreak {
    day_of_year
  }
}