///|
/// A named IANA time zone, backed by parsed TZif (RFC 8536) data plus,
/// when the file provides one, the POSIX TZ rule used to extrapolate
/// offsets past the file's last recorded transition.
///
/// Equality and hashing cover the parsed TZif data, the POSIX rule and
/// `name()`, so a zone reached through an alias, or built without a name,
/// is a different `Location` from the canonical one even when it resolves
/// identically.
pub struct Location {
  priv tzif : TzifData
  priv posix_tz : PosixTz?
  priv name : String?
} derive(Eq, Hash, @debug.Debug)

///|
pub extend Location with Eq::{equal}

///|
pub extend Location with Eq::{not_equal}

///|
pub extend Location with Hash::{hash, hash_combine}

///|
/// Expands the whole transition table, so the output of a real zone is long.
pub extend Location with @debug.Debug::{to_repr}

///|
/// Renders as `name()`, or as an empty string for a `Location` without one.
pub impl Show for Location with fn output(self, logger) {
  logger.write_string(self.name.unwrap_or(""))
}

///|
pub extend Location with Show::{to_string, output}

///|
/// Constructs a `Location` from raw TZif bytes, or `None` if `parse_tzif`
/// rejects them (including a malformed POSIX TZ footer).
/// `name()` reports `None`, since raw bytes carry no IANA identifier; use
/// `Location::from_tzif_bytes_named` or `Location::load` to construct a
/// `Location` whose `name()` is known.
pub fn Location::from_tzif_bytes(data : Bytes) -> Location? {
  parse_tzif(data).map(tzif => Location::{
    tzif,
    posix_tz: tzif.posix_tz(),
    name: None,
  })
}

///|
/// Like `Location::from_tzif_bytes`, but `name()` reports `name`. Any text
/// is accepted as given, without validation.
pub fn Location::from_tzif_bytes_named(
  name : String,
  data : Bytes,
) -> Location? {
  Location::from_tzif_bytes(data).map(loc => Location::{
    ..loc,
    name: Some(name),
  })
}

///|
/// The IANA zone identifier this `Location` was loaded with (e.g.
/// `"Asia/Tokyo"`), or `None` if it was constructed via
/// `Location::from_tzif_bytes` instead. Distinct from `zone_name`, which
/// reports the instant-specific abbreviation (e.g. `"JST"`). Reports the
/// name exactly as given to `Location::load`, not the canonical name an
/// alias resolves to.
pub fn Location::name(self : Location) -> String? {
  self.name
}

///|
/// The bytes for `name` in `zones`, following `name` through `aliases`
/// (to its canonical name) when it is not itself a key of `zones`. `None`
/// if `name` is not a key of either map.
fn resolve_tzdata_bytes(
  zones : Map[String, Bytes],
  aliases : Map[String, String],
  name : String,
) -> Bytes? {
  match zones.get(name) {
    Some(bytes) => Some(bytes)
    None =>
      match aliases.get(name) {
        None => None
        Some(canonical) => zones.get(canonical)
      }
  }
}

///|
/// Loads the `Location` for the given embedded IANA zone name (e.g.
/// `"America/New_York"`), or `None` if `name` is not a known zone or its
/// embedded TZif bytes are malformed.
pub fn Location::load(name : String) -> Location? {
  match resolve_tzdata_bytes(tzdata_zones, tzdata_aliases, name) {
    None => None
    Some(bytes) => Location::from_tzif_bytes_named(name, bytes)
  }
}

///|
/// The UTC zone as a `Location`, for APIs that take a `Location` rather than
/// the separate `Utc` type: the embedded `"UTC"` zone, so `name()` is
/// `Some("UTC")`. Equal to `Location::load("UTC")`.
///
/// `Location::load("")` is not an alias for UTC
/// and returns `None`, so an empty name cannot silently become UTC.
pub fn Location::utc() -> Location {
  Location::load("UTC").unwrap()
}

///|
/// The local time type in effect before this zone's first recorded
/// transition (or for its entire timeline, if it has none): the first
/// non-DST type, or the first type if every type observes DST (RFC
/// 8536's rule for pre-first-transition instants).
fn Location::initial_type(self : Location) -> LocalTimeType {
  let types = self.tzif.local_time_types
  for i = 0 {
    if i >= types.length() {
      break types[0]
    }
    if !types[i].is_dst() {
      break types[i]
    }
    continue i + 1
  }
}

///|
/// The index into `local_time_types` (via `transition_types`) in effect
/// at `target`, or `None` if `target` is before this zone's first
/// recorded transition, or it has none (use `initial_type` instead).
fn Location::segment_at(self : Location, target : Int64) -> Int? {
  let transitions = self.tzif.transitions
  if transitions.is_empty() {
    None
  } else {
    let segment = match transitions.binary_search(target) {
      Ok(i) => i
      Err(i) => i - 1
    }
    if segment < 0 {
      None
    } else {
      Some(segment)
    }
  }
}

///|
/// The UTC offset a zone abbreviation (e.g. `"EST"`) denotes in this zone:
/// the offset of the local time type in effect at `near` if its
/// abbreviation matches, otherwise that of the first local time type in the
/// zone's table with that abbreviation, or `None` if no type has it. The
/// fallback is what resolves an abbreviation the zone is not observing at
/// `near`, such as `"EST"` in summer.
pub fn Location::offset_from_abbreviation(
  self : Location,
  abbreviation : String,
  near : @core.NaiveDateTime,
) -> FixedOffset? {
  let in_effect = self.type_at(near)
  if in_effect.abbreviation() == abbreviation {
    FixedOffset::east(in_effect.utc_offset())
  } else {
    match
      self.tzif.local_time_types
      .iter()
      .find_first(t => t.abbreviation() == abbreviation) {
      Some(t) => FixedOffset::east(t.utc_offset())
      None => None
    }
  }
}

///|
/// Whether `target` is strictly past this zone's last recorded
/// transition (and so should be resolved via `posix_tz` rather than the
/// transition table).
fn Location::past_last_transition(self : Location, target : Int64) -> Bool {
  let transitions = self.tzif.transitions
  !transitions.is_empty() && target > transitions[transitions.length() - 1]
}

///|
fn Location::type_at_segment(self : Location, segment : Int) -> LocalTimeType {
  self.tzif.local_time_types[self.tzif.transition_types[segment]]
}

///|
/// The local time type (offset, DST flag, and abbreviation) in effect at
/// the given UTC instant.
pub fn Location::type_at(
  self : Location,
  utc : @core.NaiveDateTime,
) -> LocalTimeType {
  let target = utc.timestamp()
  if self.past_last_transition(target) {
    match self.posix_tz {
      Some(p) => p.type_at_secs(target)
      None => self.type_at_segment(self.tzif.transitions.length() - 1)
    }
  } else {
    match self.segment_at(target) {
      None => self.initial_type()
      Some(segment) => self.type_at_segment(segment)
    }
  }
}

///|
/// The validity window of the time-zone segment covering a given instant:
/// the local time type `type_at` reports is in effect from `start`
/// (inclusive) until `end` (exclusive). `None` on either side means
/// unbounded in that direction.
pub struct TransitionBounds {
  priv start : @core.NaiveDateTime?
  priv end : @core.NaiveDateTime?
} derive(Eq, Hash, @debug.Debug)

///|
pub extend TransitionBounds with Eq::{equal}

///|
pub extend TransitionBounds with Eq::{not_equal}

///|
pub extend TransitionBounds with Hash::{hash, hash_combine}

///|
pub extend TransitionBounds with @debug.Debug::{to_repr}

///|
/// The instant this segment began, or `None` if it extends back
/// indefinitely (before the zone's first recorded transition, or for a
/// zone with no transitions at all).
pub fn TransitionBounds::start(self : TransitionBounds) -> @core.NaiveDateTime? {
  self.start
}

///|
/// The instant the next segment begins, or `None` if this segment
/// extends forward indefinitely (past the zone's last recorded
/// transition, when no POSIX rule extrapolates further).
pub fn TransitionBounds::end(self : TransitionBounds) -> @core.NaiveDateTime? {
  self.end
}

///|
/// The validity window of the segment covering `utc`. See
/// `TransitionBounds`.
pub fn Location::transition_bounds(
  self : Location,
  utc : @core.NaiveDateTime,
) -> TransitionBounds {
  let target = utc.timestamp()
  let transitions = self.tzif.transitions
  fn at(secs : Int64) -> @core.NaiveDateTime? {
    @core.NaiveDateTime::from_timestamp(secs, 0)
  }

  if self.past_last_transition(target) {
    let unbounded_forward = TransitionBounds::{
      start: at(transitions[transitions.length() - 1]),
      end: None,
    }
    match self.posix_tz {
      None => unbounded_forward
      Some(p) =>
        match p.bounds_at_secs(target) {
          None => unbounded_forward
          Some((start, end)) =>
            TransitionBounds::{ start: at(start), end: at(end), }
        }
    }
  } else {
    match self.segment_at(target) {
      None =>
        TransitionBounds::{
          start: None,
          end: if transitions.is_empty() {
            None
          } else {
            at(transitions[0])
          },
        }
      Some(segment) => {
        let n = transitions.length()
        TransitionBounds::{
          start: at(transitions[segment]),
          end: if segment + 1 < n {
            at(transitions[segment + 1])
          } else {
            None
          },
        }
      }
    }
  }
}

///|
pub impl TimeZone for Location with fn offset_from_utc(self, utc) {
  FixedOffset::east(self.type_at(utc).utc_offset()).unwrap()
}

///|
pub impl TimeZone for Location with fn zone_name(self, utc) {
  self.type_at(utc).abbreviation()
}

///|
/// Whether `segment` is a valid candidate resolution for the local
/// (wall-clock) reading `local_ts`: converting `local_ts` back to UTC via
/// `segment`'s own offset lands within `segment`'s own true range. This
/// (rather than trusting whichever segment a first-guess lookup landed
/// on) is what correctly identifies gaps and folds: near a transition,
/// the segment that a naive lookup finds is not necessarily the one
/// `local_ts` actually resolves through.
fn Location::candidate_offset(
  self : Location,
  segment : Int,
  local_ts : Int64,
) -> Int? {
  let transitions = self.tzif.transitions
  let n = transitions.length()
  if segment < 0 || segment >= n {
    None
  } else {
    let offset = self.type_at_segment(segment).utc_offset()
    let candidate_utc = local_ts - offset.to_int64()
    let lower_ok = segment == 0 || candidate_utc >= transitions[segment]
    let upper_ok = segment == n - 1 || candidate_utc < transitions[segment + 1]
    if lower_ok && upper_ok {
      Some(offset)
    } else {
      None
    }
  }
}

///|
/// A gap or fold only ever straddles a single transition, so of the three
/// neighboring segments checked below, only an adjacent pair (`prev` with
/// `cur`, or `cur` with `next`) can ever be valid together; the remaining
/// combinations are unreachable with well-formed TZif data and exist only
/// to make the match exhaustive.
pub impl TimeZone for Location with fn offset_from_local(self, naive_local) {
  let local_ts = naive_local.timestamp()
  if self.past_last_transition(local_ts) {
    match self.posix_tz {
      Some(p) => p.offset_from_local(naive_local)
      None =>
        Single(
          FixedOffset::east(
            self
            .type_at_segment(self.tzif.transitions.length() - 1)
            .utc_offset(),
          ).unwrap(),
        )
    }
  } else {
    match self.segment_at(local_ts) {
      None =>
        Single(FixedOffset::east(self.initial_type().utc_offset()).unwrap())
      Some(segment) => {
        let prev = self.candidate_offset(segment - 1, local_ts)
        let cur = self.candidate_offset(segment, local_ts)
        let next = self.candidate_offset(segment + 1, local_ts)
        match (prev, cur, next) {
          (None, None, None) => Absent
          (Some(a), None, None) => Single(FixedOffset::east(a).unwrap())
          (None, Some(a), None) => Single(FixedOffset::east(a).unwrap())
          (None, None, Some(a)) => Single(FixedOffset::east(a).unwrap())
          (Some(a), Some(b), None) =>
            Ambiguous(
              FixedOffset::east(a).unwrap(),
              FixedOffset::east(b).unwrap(),
            )
          (None, Some(a), Some(b)) =>
            Ambiguous(
              FixedOffset::east(a).unwrap(),
              FixedOffset::east(b).unwrap(),
            )
          _ => Absent
        }
      }
    }
  }
}

///|
pub impl TimeZone for Location with fn is_dst(self, utc) {
  self.type_at(utc).is_dst()
}

///|
pub impl TimeZone for Location with fn transition_bounds(self, utc) {
  Location::transition_bounds(self, utc)
}

///|
pub impl TimeZone for Location with fn offset_from_abbreviation(
  self,
  abbreviation,
  near,
) {
  Location::offset_from_abbreviation(self, abbreviation, near)
}

///|
pub extend Location with TimeZone::{
  offset_from_utc,
  offset_from_local,
  zone_name,
  is_dst,
}