piechart.plotly <- function(
  x, x_name, y_name, by_name = NULL, main = NULL,
  fill, border, opacity,
  hole,
  ncols = NULL,
  labels = c("%", "input", "prop", "off"),
  labels_position, labels_color,
  labels_size,
  labels_decimals,
  digits_d = 2,
  group_labels = TRUE,
  group_label_pos = c("below","above"),
  group_label_size = 14,
  group_label_gap = 0.05,
  theme = getOption("theme"),
  style_opts = .plotly_style()
) {

  group_label_pos <- match.arg(group_label_pos)
  labels <- match.arg(labels)

  # decimals for the numeric part of a slice label. NULL falls back
  # to digits_d, except for "prop", whose proportions digits_d
  # (0 for counts) would otherwise flatten to "0".
  lbl.d <- if (!missing(labels_decimals) && length(labels_decimals) &&
               !is.null(labels_decimals))
             as.integer(labels_decimals)
           else if (identical(labels, "prop")) 2L
           else as.integer(digits_d)

  # --- helpers ------------------------------------------------------------
  .pick_by_names_or_recycle <- function(v, needed_names, needed_len) {
    v <- as.character(v)
    nm <- names(v)
    if (!is.null(nm) && length(nm)) {
      out <- v[match(needed_names, nm)]
      if (anyNA(out)) {
        base <- if (length(v)) v else "black"
        out[is.na(out)] <- base[((which(is.na(out)) - 1L) %% length(base)) + 1L]
      }
      return(out)
    }
    if (!length(v)) return(rep("black", needed_len))
    rep_len(v, needed_len)
  }

  .pick_by_names <- function(v, needed_names, fallback = "gray80") {
    v <- as.character(v); nm <- names(v)
    if (!is.null(nm) && length(nm)) {
      out <- v[match(needed_names, nm)]
      out[is.na(out)] <- fallback
      return(out)
    }
    if (!length(v)) return(rep(fallback, length(needed_names)))
    rep_len(v, length(needed_names))
  }

  # Simulated panel bg for pie grid (helps in some Viewers)
  .add_panel_bg <- function(plt) {
    bg.plot  <- toupper(.to_hex(style_opts$panel_fill))
    bg.paper <- toupper(.to_hex(style_opts$window_fill))
    plt <- plotly::layout(plt, paper_bgcolor = bg.paper)
    shapes <- plt$x$layout$shapes %||% list()
    shapes <- Filter(function(s) {
      !(identical(s$type,"rect") && identical(s$xref,"paper") &&
        identical(s$yref,"paper") && identical(s$x0,0) && identical(s$y0,0) &&
        identical(s$x1,1) && identical(s$y1,1))
    }, shapes)
    bg.shape <- list(
      type="rect", xref="paper", yref="paper",
      x0=0, y0=0, x1=1, y1=1,
      fillcolor=bg.plot, line=list(width=0), layer="below"
    )
    plt$x$layout$shapes <- c(shapes, list(bg.shape))
    plt
  }

  title.size <- round(16 * getOption("main_size", 1))

  val.spec <- paste0(":.", max(0, as.integer(digits_d)), "f")

  # --- SINGLE PIE: 1D / named vector -------------------------------------
  if (is.null(dim(x)) || length(dim(x)) == 0L || length(dim(x)) == 1L) {

    slices <- names(x)
    values <- suppressWarnings(as.numeric(x))
    stopifnot(length(slices) == length(values))

    # fill/border
    fill.vec   <- .pick_by_names_or_recycle(fill, slices, length(slices))
    border.vec <- if (length(border) <= 1L) rep_len(as.character(border), length(slices))
                  else .pick_by_names_or_recycle(border, slices, length(slices))

    # label text
    label.mode <- labels
    fmt.num <- function(z) formatC(z, format = "f", digits = lbl.d)

    tot <- sum(values, na.rm = TRUE)
    num.str <- switch(label.mode,
      "%"     = paste0(fmt.num(100 * values / max(tot, 1e-12)), "%"),
      "prop"  = fmt.num(values / max(tot, 1e-12)),
      "input" = fmt.num(values),
      "off"   = rep("", length(values)),
      paste0(fmt.num(100 * values / max(tot, 1e-12)), "%")
    )
    text.vec <- if (identical(label.mode, "off")) slices else paste0(slices, "<br>", num.str)

    # inside/outside
    pos.in  <- if (!missing(labels_position) && length(labels_position))
                 tolower(as.character(labels_position[1])) == "in" else TRUE
    txt.pos <- if (pos.in) "inside" else "outside"

    base.size <- 12
    txt.size  <- round(base.size * 1.38 *
                   (if (!missing(labels_size) && length(labels_size))
                     labels_size else 1) *
                   getOption("axis_size", 0.9), 0)

    # --- opacity for fill -------------------------------------------------
    alpha.fill <- as.numeric(opacity)
    if (!is.finite(alpha.fill)) alpha.fill <- 1
    alpha.fill <- max(0, min(1, alpha.fill))
    fills.rgba <- .maketrans_plotly(fill.vec, alpha = alpha.fill)

    # auto text color if requested
    want.adjust <- is.null(labels_color) || identical(labels_color[1], "adjust")
    txt.colors  <- if (want.adjust) .auto_text_color(fills.rgba, bg = style_opts$panel_fill)
                   else rep(labels_color[1], length(fills.rgba))

    dom.y   <- c(0.03, 0.93)
    margins <- list(t = round(title.size * 2.2),
                    r = 20, b = 30, l = 20)

    grand.total <- tot
    overall.pct <- values / if (grand.total > 0) grand.total else 1

    plt <- plotly::plot_ly(
      type   = "pie",
      labels = slices,
      values = values,
      sort   = FALSE,
      direction = "clockwise",
      hole   = hole,
      domain = list(x = c(0, 1), y = dom.y),
      text         = text.vec,
      textinfo     = "text",
      textposition = txt.pos,
      insidetextorientation = "radial",
      textfont       = list(color = txt.colors, size = txt.size),
      insidetextfont = list(color = txt.colors, size = txt.size),
      outsidetextfont= list(color = txt.colors, size = txt.size),
      automargin     = !pos.in,
      marker = list(
        colors = fills.rgba,
        line   = list(color = "rgba(0,0,0,0)", width = 1)
      ),
      customdata = overall.pct,
      hovertemplate = paste0(
        x_name, ": %{label}",
        "<br>", y_name, ": %{value", val.spec, "}",
        "<br>% of total: %{customdata:.2%}",
        "<extra></extra>"
      ),
      showlegend = FALSE
    )

    # Defensive post-build tweaks
    built <- tryCatch(plotly::plotly_build(plt), error = function(e) {
      warning("piechart.plotly: plotly_build() failed. ",
              conditionMessage(e), call. = FALSE)
      NULL
    })
    if (!is.null(built) && length(built$x$data) >= 1L) {
      n.slices <- length(built$x$data[[1]]$labels %||% built$x$data[[1]]$values)
      bvec <- if (length(border.vec) == 1L) rep(border.vec[1], n.slices)
                  else rep_len(border.vec, n.slices)
      bvec <- .to_hex(bvec)
      built$x$data[[1]]$marker$line <- list(color = I(bvec), width = rep(1.6,
                                            n.slices))
      built$x$data[[1]]$marker$line$colorsrc <- NULL
      built$x$data[[1]]$marker$line$widthsrc <- NULL

      if (identical(txt.pos, "inside") && isTRUE(hole > 0.62)) {
        built$x$data[[1]]$hole <- 0.62
      }
      plt <- built
    }

    plt$x$layout$template    <- NULL
    plt$x$layout$uniformtext <- NULL
    plt <- plotly::layout(
      plt,
      uniformtext = list(minsize = 8, mode = "show"),
      margin = margins,
      title  = if (!is.null(main) && nzchar(main))
                 list(text = main, y = 0.94, yanchor = "top",
                      font = list(size = title.size))
               else NULL
    )

    if (!identical(toupper(.to_hex(style_opts$panel_fill)),  "#FFFFFF") ||
        !identical(toupper(.to_hex(style_opts$window_fill)), "#FFFFFF")) {
      plt <- .add_panel_bg(plt)
    }

    return(plt)
  }  # end 1D

  # --- GROUPED INPUT (2D): PIE GRID ---------------------------------------
  stopifnot(length(dim(x)) == 2L)
  x <- as.matrix(x)
  groups <- rownames(x)
  if (is.null(groups)) groups <- as.character(seq_len(nrow(x)))
  slices <- colnames(x)
  if (is.null(slices)) slices <- paste0(x_name, "_", seq_len(ncol(x)))
  grand.total <- sum(x, na.rm = TRUE)

  build.pie.grid <- function() {
    k  <- nrow(x)
    nc <- if (is.null(ncols) || ncols < 1)
              ceiling(sqrt(k)) else as.integer(ncols)
    nr <- ceiling(k / nc)

    alpha.fill <- as.numeric(opacity)
    if (!is.finite(alpha.fill)) alpha.fill <- 1
    alpha.fill <- max(0, min(1, alpha.fill))

    fmt.num <- function(z) formatC(z, format = "f", digits = lbl.d)
    label.mode <- tolower(labels[1])

    plt <- plotly::plot_ly()
    if (is.null(plt$x$layout$annotations)) plt$x$layout$annotations <- list()
    by.title <- if (length(by_name) && nzchar(by_name)) by_name else "Group"

    for (i in seq_len(k)) {
      j   <- i - 1L
      col <- j %% nc
      row <- j %/% nc
      x0 <- col / nc
      x1 <- (col + 1) / nc
      y0 <- 1 - (row + 1) / nr
      y1 <- 1 - row / nr

      shrink <- 0.92
      y.mid  <- (y0 + y1) / 2
      y.half <- (y1 - y0) * shrink / 2
      dom <- list(x = c(x0, x1), y = c(y.mid - y.half, y.mid + y.half))

      grp  <- groups[i]
      vals <- suppressWarnings(as.numeric(x[i, ]))
      vals[!is.finite(vals)] <- NA_real_

      val.sum     <- sum(vals, na.rm = TRUE)
      overall.pct <- vals / if (grand.total > 0) grand.total else 1

      num.str <- switch(label.mode,
        "%"     = paste0(fmt.num(100 * vals / max(val.sum, 1e-12)), "%"),
        "prop"  = fmt.num(vals / max(val.sum, 1e-12)),
        "input" = fmt.num(vals),
        "off"   = rep("", length(vals)),
        paste0(fmt.num(100 * vals / max(val.sum, 1e-12)), "%")
      )

      slice.labels <- slices
      text.vec <- if (label.mode == "off")
                      slice.labels else paste0(slice.labels, "<br>", num.str)

      pos.in   <- tolower((labels_position %||% "in")[1]) == "in"
      txt.pos  <- if (pos.in) "inside" else "outside"
      base.sz  <- 12
      txt.size <- round(base.sz * (labels_size %||% 1) *
                    getOption("axis_size", 0.9), 0)

      vals.sc <- vals; vals.sc[!is.finite(vals.sc)] <- 0
      is.seq.fill <- !identical(theme, "colors") &&
                     !identical(theme, "light")
      fills.this <- if (is.seq.fill && !length(names(fill)) &&
                        length(vals.sc) > 1L &&
                        diff(range(vals.sc)) > 0) {
        .maketrans_plotly(
          .scale.clr(vals.sc, fill = NULL, fill_split = 0,
                     fill_chroma = 75, theme = theme),
          alpha = alpha.fill)
      } else if (identical(theme, "colors") && !length(names(fill))) {
        .maketrans_plotly(
          .color_range("hues", length(vals.sc)),
          alpha = alpha.fill)
      } else {
        .maketrans_plotly(
          .pick_by_names(fill, slices, fallback = "#CCCCCC"),
          alpha = alpha.fill)
      }
      borders.this <- .pick_by_names(border, slices, fallback = "gray80")

      txt.colors <- if (!length(labels_color) || identical(labels_color[1],
                        "adjust")) {
        .auto_text_color(fills.this, bg = style_opts$panel_fill)
      } else {
        rep(labels_color[1], length(fills.this))
      }

      plt <- plotly::add_trace(
        plt,
        type         = "pie",
        labels       = slice.labels,
        values       = vals,
        name         = paste0(by.title, ": ", grp),
        legendgroup  = "pies",
        sort         = FALSE,
        direction    = "clockwise",
        hole         = hole,
        domain       = dom,
        text         = text.vec,
        textinfo     = "text",
        textposition = txt.pos,
        insidetextorientation = "radial",
        textfont        = list(color = txt.colors, size = txt.size),
        insidetextfont  = list(color = txt.colors, size = txt.size),
        outsidetextfont = list(color = txt.colors, size = txt.size),
        automargin      = !pos.in,
        marker = list(
          colors = fills.this,
          line   = list(color = borders.this, width = 1)
        ),
        customdata = overall.pct,
        hovertemplate = paste0(
          by.title, ": ", grp,
          "<br>", x_name, ": %{label}",
          "<br>", y_name, ": %{value", val.spec, "}",
          "<br>% of ", grp, ": %{percent}",
          "<br>% of total: %{customdata:.2%}",
          "<extra></extra>"
        ),
        showlegend = FALSE
      )

      if (isTRUE(group_labels) && hole > 0) {
        cx <- mean(dom$x); cy <- mean(dom$y)
        plt$x$layout$annotations <- c(
          plt$x$layout$annotations,
          list(list(
            x = cx, y = cy,
            xref = "paper", yref = "paper",
            text = grp,
            showarrow = FALSE,
            xanchor = "center", yanchor = "middle",
            font = list(size = group_label_size %||% 14, color = "#666666")
          ))
        )
      }
    } # end for i

    plt <- plotly::layout(plt, uniformtext = list(minsize = 10, mode = "show"))
    plt
  } # end build.pie.grid

  plt <- build.pie.grid()
  plt <- plotly::layout(
    plt,
    title  = if (!is.null(main) && nzchar(main))
               list(text = main, y = 0.94, yanchor = "top",
                    font = list(size = title.size))
             else NULL,
    margin = list(t = round(title.size * 2.2),
                  r = 20, b = 20, l = 20)
  )

  if (!identical(toupper(.to_hex(style_opts$panel_fill)),  "#FFFFFF") ||
      !identical(toupper(.to_hex(style_opts$window_fill)), "#FFFFFF")) {
    plt <- .add_panel_bg(plt)
  }

  return(plt)

} # end piechart.plotly
