#' Boxplot type
#' 
#' @description Type function for producing box-and-whisker plots.
#' Arguments are passed to \code{\link[graphics]{boxplot}}, although `tinyplot`
#' scaffolding allows added functionality such as grouping and faceting.
#' Box-and-whisker plots are the default plot type if `x` is a factor and `y` is
#' numeric.
#'
#' @inheritParams graphics::boxplot
#' @param lighten logical. Should the fills use a lighter, opaque tint of the
#'   series colour(s)? Default is `TRUE`, which keeps single- and multi-group
#'   displays consistent and lets the fill read cleanly over grid lines. Set to
#'   `FALSE` to use the fully-saturated palette colour(s) instead.
#' @examples
#' # "boxplot" type convenience string
#' tinyplot(weight ~ feed, data = chickwts, type = "boxplot")
#' 
#' # Note: Specifying the type here is redundant. Like base plot, tinyplot
#' # automatically produces a boxplot if x is a factor and y is numeric
#' tinyplot(weight ~ feed, data = chickwts)
#' 
#' # For flipped boxplots, it's usually better to use a dynamic theme to
#' # accommodate (horizontal) y-axis labels
#' tinyplot(weight ~ feed, data = chickwts, flip = TRUE, theme = "dynamic")
#' 
#' # Grouped boxplot example using a different dataset
#' # (C.f., the final example in `?boxplot`)
#' tinyplot(
#'   len ~ dose | supp, data = ToothGrowth, type = "boxplot",
#'   main = "Guinea Pigs' Tooth Growth",
#'   legend = list(title = "Supplement"),
#'   xlab = "Vitamin C dose mg", ylab = "tooth length"
#' )
#' 
#' # Use `type_boxplot()` to pass extra arguments for customization
#' tinyplot(
#'   len ~ dose | supp, data = ToothGrowth,
#'   type = type_boxplot(boxwex = 0.3, staplewex = 0, outline = FALSE), lty = 1,
#'   legend = list(title = "Supplement"),
#'   main = "Guinea Pigs' Tooth Growth",
#'   xlab = "Vitamin C dose mg", ylab = "tooth length"
#' )
#' @export
type_boxplot = function(
    range = 1.5,
    width = NULL,
    varwidth = FALSE,
    notch = FALSE,
    outline = TRUE,
    boxwex = 0.8,
    staplewex = 0.5,
    outwex = 0.5,
    lighten = TRUE) {
  out = list(
    draw = draw_boxplot(
      range = range,
      width = width,
      varwidth = varwidth,
      notch = notch,
      outline = outline,
      boxwex = boxwex,
      staplewex = staplewex,
      outwex = outwex),
    data = data_boxplot(boxwex = boxwex, lighten = lighten),
    name = "boxplot"
  )
  class(out) = "tinyplot_type"
  return(out)
}



draw_boxplot = function(range, width, varwidth, notch, outline, boxwex, staplewex, outwex) {
    fun = function(iby, ix, iy, ipch, ilty, icol, ibg, x_by = FALSE, facet_by = FALSE, ngrps = 1, flip, ...) {

        at_ix = unique(ix)
        if (isTRUE(x_by)) boxwex = boxwex * 2

        # Handle multiple groups
        if (ngrps > 1 && isFALSE(x_by) && isFALSE(facet_by)) {
            group_offsets = get_environment_variable(".group_offsets")
            boxwex = boxwex / ngrps - 0.01
            at_ix = at_ix + group_offsets[iby]
        }

        boxplot(
            formula = iy ~ ix,
            pch = ipch,
            lty = ilty,
            border = icol,
            col = ibg,
            horizontal = flip,
            add = TRUE, axes = FALSE,
            at = at_ix,
            range = range,
            width = width,
            varwidth = varwidth,
            notch = notch,
            outline = outline,
            boxwex = boxwex,
            staplewex = staplewex,
            outwex = outwex
        )
    }
    return(fun)
}



data_boxplot = function(boxwex = 0.8, lighten = TRUE) {
    fun = function(settings, ...) {
        env2env(settings, environment(), c("datapoints", "by", "facet", "null_facet", "null_palette", "x", "col", "bg", "null_by"))
        settings[["lighten"]] = lighten
        # Convert x to factor if it's not already
        datapoints$x = as.factor(datapoints$x)

        # Handle factor levels and maintain order
        xlvls = levels(datapoints$x)
        xlabs = seq_along(xlvls)
        names(xlabs) = xlvls
        datapoints$x = as.integer(datapoints$x)

        if (null_by && null_facet) {
            xord = order(datapoints$x)
        } else if (null_facet) {
            xord = order(datapoints$by, datapoints$x)
        } else if (null_by) {
            xord = order(datapoints$facet, datapoints$x)
        } else {
            xord = order(datapoints$by, datapoints$facet, datapoints$x)
        }

        # Check if user provided palette before substitute)
        if (length(unique(datapoints[["by"]])) == 1 && null_palette) {
            # With a theme palette active, leave bg = NULL so the fill tracks
            # the resolved border colour (see by_bg). Otherwise use neutral grey.
            if (is.null(bg) && is.null(get_tpar("palette.qualitative", default = NULL))) bg = "lightgray"
        } else {
            if (is.null(bg)) bg = "by"
        }

        # Reorder x, y, ymin, and ymax based on the order determined
        datapoints = datapoints[xord,]

        # Return the result as a list called 'out'
        x = datapoints$x
        y = datapoints$y
        ymin = datapoints$ymin
        ymax = datapoints$ymax
        by = if (length(unique(datapoints$by)) > 1) datapoints$by else by
        facet = if (length(unique(datapoints$facet)) > 1) datapoints$facet else facet
        
        # Compute group offsets for multi-group boxplots
        ngrps = length(unique(datapoints$by))
        if (ngrps > 1 && !settings$x_by) {
            boxwex_grp = boxwex / ngrps - 0.01
            group_offsets = seq(
                -((boxwex - boxwex_grp) / 2),
                ((boxwex - boxwex_grp) / 2),
                length.out = ngrps
            )
        } else {
            group_offsets = rep(0, max(ngrps, 1))
        }
        offsets_axis = "x"

        # legend customizations
        settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22
        settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5
        settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25
        settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25
        
        env2env(environment(), settings, c(
            "x",
            "y",
            "ymin",
            "ymax",
            "xlabs",
            "datapoints",
            "col",
            "bg",
            "by",
            "facet",
            "group_offsets",
            "offsets_axis"
        ))
    }
    return(fun)
}
