#==========================================================
# Range Check
#==========================================================

#' Check Values Against a Valid Range
#'
#' Identifies values in a numeric variable that fall below
#' or above user-defined valid limits.
#'
#' @param data A data.frame.
#' @param variable Character string giving the name of the
#'   numeric variable to check.
#' @param min Optional numeric lower limit. Default is
#'   \code{NULL}.
#' @param max Optional numeric upper limit. Default is
#'   \code{NULL}.
#'
#' @return A data.frame of class \code{RangeCheck}
#'   containing row-level range diagnostics.
#'
#' @details
#' At least one of \code{min} or \code{max} must be
#' supplied.
#'
#' Missing values are not classified as out-of-range.
#' Infinite values are evaluated against the specified
#' limits and will normally be flagged when an appropriate
#' finite limit is supplied.
#'
#' @examples
#' dat <- data.frame(
#'   Temperature = c(38.5, 39.2, 44.0, 34.0, NA)
#' )
#'
#' range_check(
#'   dat,
#'   variable = "Temperature",
#'   min = 35,
#'   max = 43
#' )
#'
#' @export

range_check <- function(
    data,
    variable,
    min = NULL,
    max = NULL
) {

  #---------------------------------------------
  # Validate data
  #---------------------------------------------
  validate_data(data)

  #---------------------------------------------
  # Validate variable
  #---------------------------------------------
  if (missing(variable) ||
      !is.character(variable) ||
      length(variable) != 1L ||
      is.na(variable) ||
      !nzchar(variable)) {

    stop(
      "'variable' must be a single non-empty character string.",
      call. = FALSE
    )
  }

  if (!variable %in% names(data)) {

    stop(
      sprintf(
        "Variable '%s' was not found in 'data'.",
        variable
      ),
      call. = FALSE
    )
  }

  x <- data[[variable]]

  if (!is.numeric(x)) {

    stop(
      "'variable' must refer to a numeric variable.",
      call. = FALSE
    )
  }

  #---------------------------------------------
  # Validate limits
  #---------------------------------------------
  if (is.null(min) && is.null(max)) {

    stop(
      "At least one of 'min' or 'max' must be supplied.",
      call. = FALSE
    )
  }

  if (!is.null(min)) {

    if (!is.numeric(min) ||
        length(min) != 1L ||
        is.na(min) ||
        !is.finite(min)) {

      stop(
        "'min' must be NULL or a single finite numeric value.",
        call. = FALSE
      )
    }
  }

  if (!is.null(max)) {

    if (!is.numeric(max) ||
        length(max) != 1L ||
        is.na(max) ||
        !is.finite(max)) {

      stop(
        "'max' must be NULL or a single finite numeric value.",
        call. = FALSE
      )
    }
  }

  if (!is.null(min) &&
      !is.null(max) &&
      min > max) {

    stop(
      "'min' cannot be greater than 'max'.",
      call. = FALSE
    )
  }

  #---------------------------------------------
  # Detect violations
  #---------------------------------------------
  below <- rep(
    FALSE,
    length(x)
  )

  above <- rep(
    FALSE,
    length(x)
  )

  if (!is.null(min)) {

    below <-
      !is.na(x) &
      x < min
  }

  if (!is.null(max)) {

    above <-
      !is.na(x) &
      x > max
  }

  out_of_range <- below | above

  #---------------------------------------------
  # Construct output
  #---------------------------------------------
  result <- data.frame(
    Row = seq_along(x),
    Value = x,
    BelowMinimum = below,
    AboveMaximum = above,
    OutOfRange = out_of_range,
    stringsAsFactors = FALSE
  )

  class(result) <- c(
    "RangeCheck",
    "data.frame"
  )

  result
}
