#' Create a G6 Graph Visualization
#'
#' @description
#' Creates an interactive graph visualization using the G6 graph visualization library.
#' This function is the main entry point for creating G6 graph visualizations in R.
#'
#' @details
#' The `g6` function creates a G6 graph as an htmlwidget that can be used in R Markdown,
#' Shiny applications, or rendered to HTML. It takes graph data in the form of nodes, edges,
#' and optional combo groupings, along with various configuration options for customizing
#' the appearance and behavior of the graph.
#'
#' \subsection{Nodes}{
#' The `nodes` parameter should be a data frame or list of nodes with at least an `id` field
#' for each node. Additional fields can include:
#' \itemize{
#'   \item \code{id} (required): Unique identifier for the node. Must be a character.
#'   \item \code{type}: Node type (e.g., "circle", "rect", "diamond").
#'   \item \code{data}: Custom data associated with the node.
#'   \item \code{style}: List of style attributes (color, size, etc.).
#'   \item \code{states}: String. Initial states for the node, such as selected, active, hover, etc.
#'   \item \code{combo}: ID of the combo this node belongs to.
#'   \item \code{ports}: Node ports. Can also be passed inside the style field.
#' }}
#'
#' \subsection{Edges}{
#' The `edges` parameter should be a data frame or list of edges with at least `source` and
#' `target` fields identifying the connected nodes. Additional fields can include:
#' \itemize{
#'   \item \code{source} (required): ID of the source node. Must be a character.
#'   \item \code{target} (required): ID of the target node. Must be a character.
#'   \item \code{id}: Unique identifier for the edge.
#'   \item \code{type}: Edge type (e.g., "line", "cubic", "arc").
#'   \item \code{data}: Custom data associated with the edge.
#'   \item \code{style}: List of style attributes (color, width, etc.).
#'   \item \code{states}: String. Initial states for the edge.
#' }}
#'
#'
#' \subsection{Combos}{
#' The `combos` parameter is used for grouping nodes and can be a data frame or list with
#' combo definitions. Fields include:
#' \itemize{
#'   \item \code{id} (required): Unique identifier for the combo. Must be a character.
#'   \item \code{type}: String: Combo type. It can be the type of built-in Combo, or the custom Combo.
#'   \item \code{data}: Custom data associated with the combo.
#'   \item \code{style}: List of style attributes.
#'   \item \code{states}: String. Initial states for the combo.
#'   \item \code{combo}: String. Parent combo ID. If there is no parent combo, it is null.
#' }}
#'
#' Nodes are assigned to combos by setting their `combo` field to the ID of the combo.
#'
#' @param nodes A data frame or list of nodes in the graph. Each node should have at least
#'   an "id" field. See 'Data Structure' section for more details.
#'   Default: NULL.
#'
#' @param edges A data frame or list of edges in the graph. Each edge should have "source"
#'   and "target" fields identifying the connected nodes. See 'Data Structure' section
#'   for more details.
#'   Default: NULL.
#'
#' @param combos A data frame or list of combo groups in the graph. Each combo should have
#'   at least an "id" field. Nodes can be assigned to combos using their "combo" field.
#'   See 'Data Structure' section for more details.
#'   Default: NULL.
#'
#' @param jsonUrl An url pointing to a valid JSON containing the graph data in G6 format.
#' See \url{https://assets.antv.antgroup.com/g6/20000.json} for
#' an example. Can't be used at the same time as `nodes`, `edges`, and `combos`.
#' @param iconsUrl A URL pointing to a JavaScript file containing custom icons. Default provides
#' reasonable set of icons from
#' \url{https://at.alicdn.com/t/project/2678727/caef142c-804a-4a2f-a914-ae82666a31ee.html?spm=a313x.7781069.1998910419.35}.
#'
#' @param width Width of the graph container in pixels or as a valid CSS unit.
#'   Default: NULL (automatic sizing).
#'
#' @param height Height of the graph container in pixels or as a valid CSS unit.
#'   Default: NULL (automatic sizing).
#'
#' @param elementId A unique ID for the graph HTML element.
#'   Default: NULL (automatically generated).
#'
#' @return An htmlwidget object that can be printed, included in R Markdown documents,
#' or used in Shiny applications. This widget contains the graph data and configuration
#' necessary to render the G6 graph visualization.
#'
#' @examples
#' # Create a simple graph with two nodes and one edge
#' nodes <- data.frame(
#'   id = c("node1", "node2")
#' )
#'
#' edges <- data.frame(
#'   source = "node1",
#'   target = "node2"
#' )
#'
#' g6(nodes = nodes, edges = edges)
#' @export
g6 <- function(
  nodes = NULL,
  edges = NULL,
  combos = NULL,
  jsonUrl = NULL,
  iconsUrl = "//at.alicdn.com/t/font_2678727_za4qjydwkkh.js",
  width = "100%",
  height = NULL,
  elementId = NULL
) {
  stopifnot(!is.null(iconsUrl))
  if (
    !is.null(jsonUrl) &&
      (!is.null(nodes) || !is.null(edges) || !is.null(combos))
  ) {
    stop(
      "Can't use jsonUrl` argument at the same time as `nodes`, `edges`, and `combos`."
    )
  }

  dat <- NULL
  if (is.null(jsonUrl)) {
    # Convert data frames to lists of records
    dat <- g6_data(nodes, edges, combos)
    validate_edges_ports(dat[["edges"]], dat[["nodes"]])
  }

  # Build properly named list of parameters to pass to widget
  x <- list(
    data = dat,
    jsonUrl = jsonUrl,
    iconsUrl = iconsUrl,
    mode = get_g6_mode(),
    preservePosition = get_g6_preserve_position(),
    directed = get_g6_directed_graph(),
    maxCollapseDepth = get_g6_max_collapse_depth()
  )

  # In case we need it ...
  hookFunc <- function(widget) {
    # TBD
  }

  # create widget
  htmlwidgets::createWidget(
    name = "g6",
    x,
    width = width,
    height = height,
    package = "g6R",
    elementId = elementId,
    preRenderHook = NULL
  )
}

#' @keywords internal
validate_g6_mode <- function(mode) {
  if (!(mode %in% c("dev", "prod"))) {
    stop(sprintf(
      "`g6R.mode` option must be one of 'dev' or 'prod'. Current value: '%s'",
      mode
    ))
  }
  invisible(mode)
}

#' @keywords internal
get_g6_mode <- function() {
  mode <- getOption("g6R.mode", "prod")
  validate_g6_mode(mode)
}

#' @keywords internal
validate_g6_preserve_position <- function(val) {
  if (!is.logical(val) || length(val) != 1) {
    stop(
      "`g6R.preserve_elements_position` option must be a single logical value (TRUE or FALSE)."
    )
  }
  invisible(val)
}

#' @keywords internal
get_g6_preserve_position <- function() {
  val <- getOption("g6R.preserve_elements_position", FALSE)
  validate_g6_preserve_position(val)
}

#' @keywords internal
validate_g6_directed_graph <- function(val) {
  if (!is.logical(val) || length(val) != 1) {
    stop(
      "`g6R.directed_graph` option must be a single logical value (TRUE or FALSE)."
    )
  }
  invisible(val)
}

#' @keywords internal
get_g6_directed_graph <- function() {
  val <- getOption("g6R.directed_graph", FALSE)
  validate_g6_directed_graph(val)
}

#' @keywords internal
set_g6_directed_graph <- function(val) {
  validate_g6_directed_graph(val)
  options("g6R.directed_graph" = val)
  invisible(val)
}

#' @keywords internal
validate_g6_max_collapse_depth <- function(val) {
  if (!is.numeric(val) || length(val) != 1 || val < -1) {
    stop(
      "`g6R.max_collapse_depth` option must be a single number >= -1."
    )
  }
  invisible(val)
}

#' @keywords internal
get_g6_max_collapse_depth <- function() {
  val <- getOption("g6R.max_collapse_depth", Inf)
  validate_g6_max_collapse_depth(val)
}

#' Set max collapse depth
#'
#' Controls which nodes display a collapse button based on their depth in
#' the graph. Only nodes at depth \code{<= maxCollapseDepth} will show
#' collapse buttons. Set to \code{Inf} (the default) to allow all nodes
#' with children to be collapsible. Set to \code{0} to only allow root
#' nodes to collapse. Set to \code{-1} to disable collapsing entirely.
#'
#' @param val A single number >= -1. Use \code{Inf} for no limit,
#' \code{-1} to disable all collapsing.
#'
#' @return Invisibly returns \code{val}.
#' @export
set_g6_max_collapse_depth <- function(val) {
  validate_g6_max_collapse_depth(val)
  options("g6R.max_collapse_depth" = val)
  invisible(val)
}

#' Shiny bindings for g6
#'
#' Output and render functions for using g6 within Shiny
#' applications and interactive Rmd documents.
#'
#' @param outputId output variable to read from
#' @param width,height Must be a valid CSS unit (like \code{'100\%'},
#'   \code{'400px'}, \code{'auto'}) or a number, which will be coerced to a
#'   string and have \code{'px'} appended.
#' @param expr An expression that generates a g6
#' @param env The environment in which to evaluate \code{expr}.
#' @param quoted Is \code{expr} a quoted expression (with \code{quote()})? This
#'   is useful if you want to save an expression in a variable.
#'
#' @name g6-shiny
#'
#' @return `g6Output` and `g6_output`
#' return a Shiny output function that can be used in the UI part of a Shiny app.
#' `renderG6` and `render_g6` return a
#' Shiny render function that can be used in the server part of a Shiny app to
#' render a `g6` element.
#'
#' @export
g6Output <- function(outputId, width = "100%", height = "400px") {
  #nocov start
  htmlwidgets::shinyWidgetOutput(
    outputId,
    "g6",
    width,
    height,
    package = "g6R"
  ) #nocov end
}

#' Alias to \link{g6Output}
#' @export
#' @rdname g6-shiny
g6_output <- g6Output

#' @rdname g6-shiny
#' @export
renderG6 <- function(expr, env = parent.frame(), quoted = FALSE) {
  #nocov start
  if (!quoted) {
    expr <- substitute(expr)
  } # force quoted
  htmlwidgets::shinyRenderWidget(expr, g6Output, env, quoted = TRUE)
} #nocov end

#' Alias to \link{renderG6}
#' @export
#' @rdname g6-shiny
render_g6 <- renderG6
