# small wrapper around this commonly used try-catch
.safe <- function(code, on_error = NULL) {
  tryCatch(code, error = function(e) on_error)
}


# select rows where values in "variable" match "value"
#' @keywords internal
.select_rows <- function(data, variable, value) {
  data[which(data[[variable]] == value), ]
}


#' select numerics columns
#' @keywords internal
.select_nums <- function(x) {
  x[unlist(lapply(x, is.numeric))]
}


#' @keywords internal
.retrieve_model <- function(x) {
  # retrieve model
  obj_name <- attr(x, "object_name", exact = TRUE)
  model <- NULL

  if (!is.null(obj_name)) {
    # first try, parent frame
    model <- .safe(get(obj_name, envir = parent.frame()))

    if (is.null(model)) {
      # second try, global env
      model <- .safe(get(obj_name, envir = globalenv()))
    }

    if (is.null(model)) {
      # last try
      model <- .dynGet(obj_name, ifnotfound = NULL)
    }
  }
  model
}


#' @keywords internal
.dynGet <- function(x,
                    ifnotfound = stop(gettextf("%s not found", sQuote(x)), domain = NA, call. = FALSE),
                    minframe = 1L,
                    inherits = FALSE) {
  x <- insight::safe_deparse(x)
  n <- sys.nframe()
  myObj <- structure(list(.b = as.raw(7)), foo = 47L)
  while (n > minframe) {
    n <- n - 1L
    env <- sys.frame(n)
    r <- get0(x, envir = env, inherits = inherits, ifnotfound = myObj)
    if (!identical(r, myObj)) {
      return(r)
    }
  }
  ifnotfound
}


#' @keywords internal
.get_direction <- function(direction) {
  if (length(direction) > 1) {
    insight::format_warning("Using first 'direction' value.")
  }

  if (is.numeric(direction[1])) {
    return(sign(direction[1]))
  }

  Value <- c(
    left = -1,
    right = 1,
    "two-sided" = 0,
    twosided = 0,
    "one-sided" = 1,
    onesided = 1,
    "<" = -1,
    ">" = 1,
    "=" = 0,
    "==" = 0,
    "-1" = -1,
    "0" = 0,
    "1" = 1,
    "+1" = 1
  )

  direction <- Value[tolower(direction[1])]

  if (is.na(direction)) {
    insight::format_error("Unrecognized 'direction' argument.")
  }
  direction
}


#' Prepare output data frame for printing
#'
#' @description
#' This is an internal helper function to standardize and enrich the output of
#' various `bayestestR` functions (like `ci()`, `hdi()`, `rope()`, etc.).
#' Its main purpose is to merge a data frame containing analysis results
#' (e.g., credible intervals) with a data frame of "cleaned" parameter
#' information (from `insight::clean_parameters()`).
#'
#' This process adds human-readable parameter names and model component
#' information (like fixed or random effects) to the output. It also includes
#' special handling for complex models, such as multivariate models from
#' `rstanarm` or `brms`, where response variables need to be parsed from
#' parameter names.
#'
#' @param temp A data frame with estimation results, like CIs or point estimates.
#' @param cleaned_parameters A data frame as returned by
#'   `insight::clean_parameters()`.
#' @param is_stan_mv Logical, indicates if the model is a `stanmvreg` object.
#' @param is_brms_mv Logical, indicates if the model is a `brms` multivariate
#'   model.
#'
#' @keywords internal
#' @noRd
.prepare_output <- function(temp,
                            cleaned_parameters,
                            is_stan_mv = FALSE,
                            is_brms_mv = FALSE) {
  if (is.null(cleaned_parameters)) {
    return(temp)
  }
  if (isTRUE(is_stan_mv)) {
    # for models with multiple responses, we create a separate response column
    temp$Response <- gsub("(b\\[)*(.*)\\|(.*)", "\\2", temp$Parameter)
    # from the parameter names, we can now remove the name of the respone variables
    for (i in unique(temp$Response)) {
      temp$Parameter <- gsub(sprintf("%s|", i), "", temp$Parameter, fixed = TRUE)
    }
    merge_by <- c("Parameter", "Effects", "Component", "Response")
    remove_cols <- c("Group", "Cleaned_Parameter", "Function", ".roworder")
  } else if (isTRUE(is_brms_mv)) {
    # for models with multiple responses, we create a separate response column
    temp$Response <- gsub("(.*)_(.*)_(.*)", "\\2", temp$Parameter)
    merge_by <- c("Parameter", "Effects", "Component", "Response")
    remove_cols <- c("Group", "Cleaned_Parameter", "Function", ".roworder")
  } else {
    # By default, we only merge by these three columns
    merge_by <- c("Parameter", "Effects", "Component")
    remove_cols <- c("Group", "Cleaned_Parameter", "Response", "Function", ".roworder")
  }

  # in "temp", we have the data frame from the related functions (like
  # `point_estimate()`, `ci()`  etc.). "cleaned_parameters" is a data frame
  # only with original parameter names, model components and "cleaned"
  # parameter names (retrieved from `insight::clean_parameters()`).

  merge_by <- intersect(merge_by, colnames(temp))
  temp$.roworder <- seq_len(nrow(temp))
  out <- merge(x = temp, y = cleaned_parameters, by = merge_by, all.x = TRUE)

  # hope this works for stanmvreg...
  if ((isTRUE(is_stan_mv) || isTRUE(is_brms_mv)) && all(is.na(out$Effects)) && all(is.na(out$Component))) {
    out$Effects <- cleaned_parameters$Effects[seq_len(nrow(out))]
    out$Component <- cleaned_parameters$Component[seq_len(nrow(out))]
  }
  # this here is required for multiple response models...
  if (all(is.na(out$Effects)) || all(is.na(out$Component))) {
    out <- out[!duplicated(out$.roworder), ]
  } else {
    out <- out[!is.na(out$Effects) & !is.na(out$Component) & !duplicated(out$.roworder), ]
  }
  attr(out, "Cleaned_Parameter") <- out$Cleaned_Parameter[order(out$.roworder)]
  datawizard::data_remove(out[order(out$.roworder), ], remove_cols, verbose = FALSE)
}


#' @keywords internal
.merge_and_sort <- function(x, y, by, all) {
  if (is.null(ncol(y))) {
    return(x)
  }
  x$.rowid <- seq_len(nrow(x))
  x <- merge(x, y, by = by, all = all)
  datawizard::data_remove(x[order(x$.rowid), ], ".rowid", verbose = FALSE)
}


# returns the variables that were used for grouping data frames (dplyr::group_var())
#' @keywords internal
.group_vars <- function(x) {
  # dplyr < 0.8.0 returns attribute "indices"
  grps <- attr(x, "groups", exact = TRUE)

  # dplyr < 0.8.0?
  if (is.null(grps)) {
    ## TODO fix for dplyr < 0.8
    attr(x, "vars", exact = TRUE)
  } else {
    setdiff(colnames(grps), ".rows")
  }
}


# safe add cleaned parameter names to a model object
.add_clean_parameters_attribute <- function(params, model, ...) {
  cp <- tryCatch(
    {
      .get_cleaned_parameters(model, ...)
    },
    error = function(e) {
      NULL
    }
  )
  attr(params, "clean_parameters") <- cp
  params
}


#' @keywords internal
.get_marginaleffects_draws <- function(object) {
  # errors and checks are handled by marginaleffects
  insight::check_if_installed("marginaleffects", minimum_version = "0.29.0")
  as.data.frame(marginaleffects::get_draws(object, shape = "DxP"))
}


#' @keywords internal
.possibly_extract_rvar_col <- function(df, rvar_col) {
  if (missing(rvar_col) || is.null(rvar_col)) {
    return(NULL)
  }

  if (is.character(rvar_col) &&
    length(rvar_col) == 1L &&
    rvar_col %in% colnames(df) &&
    inherits(df[[rvar_col]], "rvar")) {
    return(df[[rvar_col]])
  }

  insight::format_error("The `rvar_col` argument must be a single, valid column name.")
}
