#' Get date-time in a different time zone
#'
#' with_tz returns a date-time as it would appear in a different time zone.
#' The actual moment of time measured does not change, just the time zone it is
#' measured in. with_tz defaults to the Universal Coordinated time zone (UTC)
#' when an unrecognized time zone is inputted. See [Sys.timezone()]
#' for more information on how R recognizes time zones.
#'
#' @param time a POSIXct, POSIXlt, Date, chron date-time object or a data.frame
#'   object. When a data.frame all POSIXt elements of a data.frame are processed
#'   with `with_tz()` and new data.frame is returned.
#' @param tzone a character string containing the time zone to convert to. R
#'   must recognize the name contained in the string as a time zone on your
#'   system.
#' @return a POSIXct object in the updated time zone
#' @keywords chron manip
#' @seealso [force_tz()]
#' @examples
#' x <- ymd_hms("2009-08-07 00:00:01", tz = "America/New_York")
#' with_tz(x, "GMT")
#' @export
with_tz <- function(time, tzone = "") {
  if (!C_valid_tz(tzone))
    warning(sprintf("Unrecognized time zone '%s'", tzone))
  if (is.data.frame(time)) {
    for (nm in names(time)) {
      if (is.POSIXt(time[[nm]])) {
        time[[nm]] <- .with_tz(time[[nm]], tzone = tzone)
      }
    }
    time
  } else {
    .with_tz(time, tzone)
  }
}

.with_tz <- function(time, tzone = "") {
  out <- if (!is.POSIXct(time)) as.POSIXct(time) else time
  attr(out, "tzone") <- tzone
  if (is.POSIXlt(time)) {
    out <- as.POSIXlt(out)
  }
  out
}

#' Replace time zone to create new date-time
#'
#' `force_tz` returns the date-time that has the same clock time as input time,
#'  but in the new time zone. `force_tzs` is the parallel version of `force_tz`,
#'  meaning that every element from `time` argument is matched with the
#'  corresponding time zone in `tzones` argument.
#'
#'  Although the new date-time has the same clock time (e.g. the same values in
#'  the year, month, days, etc. elements) it is a different moment of time than
#'  the input date-time.
#'
#'  As R date-time vectors cannot hold elements with non-uniform time zones,
#'  `force_tzs` returns a vector with time zone `tzone_out`, UTC by default.
#'
#' @param time a POSIXct, POSIXlt, Date, chron date-time object, or a data.frame
#'   object. When a data.frame all POSIXt elements of a data.frame are processed
#'   with `force_tz()` and new data.frame is returned.
#' @param tzone a character string containing the time zone to convert to. R
#'   must recognize the name contained in the string as a time zone on your
#'   system.
#' @param roll logical. If TRUE, and `time` falls into the DST-break, assume
#'   the next valid civil time, otherwise return NA. See examples.
#' @return a POSIXct object in the updated time zone
#' @keywords chron manip
#' @seealso [with_tz()], [local_time()]
#' @examples
#' x <- ymd_hms("2009-08-07 00:00:01", tz = "America/New_York")
#' force_tz(x, "UTC")
#' force_tz(x, "Europe/Amsterdam")
#'
#' ## DST skip:
#'
#' y <- ymd_hms("2010-03-14 02:05:05 UTC")
#' force_tz(y, "America/New_York", roll=FALSE)
#' force_tz(y, "America/New_York", roll=TRUE)
#' @export
force_tz <- function(time, tzone = "", roll = FALSE) {
  tzone <- as.character(tzone)
  if (is.data.frame(time)) {
    for (nm in names(time)) {
      if (is.POSIXt(time[[nm]])) {
        time[[nm]] <- force_tz(time[[nm]], tzone = tzone)
      }
    }
    time
  } else {
    if (is.POSIXct(time))
      C_force_tz(time, tz = tzone, roll)
    else if (is.Date(time))
      C_force_tz(date_to_posix(time), tz = tzone, roll)
    else {
      out <- C_force_tz(as.POSIXct(time, tz = tz(time)), tz = tzone, roll)
      reclass_date(out, time)
    }
  }
}

#' @param tzones character vector of timezones to be "enforced" on `time` time
#'   stamps. If `time` and `tzones` lengths differ, the smaller one is recycled
#'   in accordance with usual R conventions.
#' @param tzone_out timezone of the returned date-time vector (for `force_tzs`).
#' @rdname force_tz
#' @examples
#'
#' ## Heterogeneous time-zones:
#'
#' x <- ymd_hms(c("2009-08-07 00:00:01", "2009-08-07 01:02:03"))
#' force_tzs(x, tzones = c("America/New_York", "Europe/Amsterdam"))
#' force_tzs(x, tzones = c("America/New_York", "Europe/Amsterdam"), tzone_out = "America/New_York")
#'
#' x <- ymd_hms("2009-08-07 00:00:01")
#' force_tzs(x, tzones = c("America/New_York", "Europe/Amsterdam"))
#' @export
force_tzs <- function(time, tzones, tzone_out = "UTC", roll = FALSE) {
  if (length(tzones) < length(time))
    tzones <- rep_len(tzones, length(time))
  else if (length(tzones) > length(time)) {
    attr <- attributes(time)
    time <- rep_len(time, length(tzones))
    attributes(time) <- attr
  }
  out <- C_force_tzs(as.POSIXct(time), tzones, tzone_out, roll)
  reclass_date(out, time)
}

#' Get local time from a date-time vector.
#'
#' `local_time` retrieves day clock time in specified time zones. Computation is
#' vectorized over both `dt` and `tz` arguments, the shortest is recycled in
#' accordance with standard R rules.
#'
#' @param dt a date-time object.
#' @param tz a character vector of timezones for which to compute the local time.
#' @param units passed directly to [as.difftime()].
#' @examples
#'
#' x <- ymd_hms(c("2009-08-07 01:02:03", "2009-08-07 10:20:30"))
#' local_time(x, units = "secs")
#' local_time(x, units = "hours")
#' local_time(x, "Europe/Amsterdam")
#' local_time(x, "Europe/Amsterdam") == local_time(with_tz(x, "Europe/Amsterdam"))
#'
#' x <- ymd_hms("2009-08-07 01:02:03")
#' local_time(x, c("America/New_York", "Europe/Amsterdam", "Asia/Shanghai"), unit = "hours")
#' @export
local_time <- function(dt, tz = NULL, units = "secs") {
  if (is.null(tz))
    tz <- tz(dt)
  if (length(tz) < length(dt))
    tz <- rep_len(tz, length(dt))
  else if (length(tz) > length(dt)) {
    attr <- attributes(dt)
    dt <- rep_len(dt, length(tz))
    attributes(dt) <- attr
  }
  secs <- C_local_time(as.POSIXct(dt), tz)
  out <- structure(secs, units = "secs", class = "difftime")
  units(out) <- units
  out
}
