#' Enhanced Visualization of Dendrogram
#' 
#' @description Draws easily beautiful dendrograms using either R base plot or 
#'   ggplot2. Provides also an option for drawing a circular dendrogram and 
#'   phylogenic trees.
#' @param x an object of class dendrogram, hclust, agnes, diana, hcut, 
#'   hkmeans or HCPC (FactoMineR).
#' @param k the number of groups for cutting the tree.
#' @param h a numeric value. Cut the dendrogram by cutting at height h. (k 
#'   overrides h)
#' @param k_colors,palette a vector containing colors to be used for the groups.
#'   It should contains k number of colors. Allowed values include also "grey" 
#'   for grey color palettes; brewer palettes e.g. "RdBu", "Blues", ...;  and 
#'   scientific journal palettes from ggsci R package, e.g.: "npg", "aaas", 
#'   "lancet", "jco", "ucscgb", "uchicago", "simpsons" and "rickandmorty".
#' @param show_labels a logical value. If TRUE, leaf labels are shown. Default 
#'   value is TRUE.
#' @param color_labels_by_k logical value. If TRUE, labels are colored
#'   automatically by group when k != NULL.
#' @param match_coord_colors logical value. Default is FALSE, where dendrogram
#'   colors follow the left-to-right leaf order. If TRUE, cluster colors are
#'   remapped to cluster-label order so they match \code{\link{fviz_cluster}()}
#'   and \code{\link{fviz_silhouette}()} for the same clustering.
#' @param label_cols a vector containing the colors for labels.
#' @param labels_font font face for the leaf labels of "rectangle"/"circular"
#'   dendrograms. One of "plain" (default), "bold", "italic" or "bold.italic".
#'   Default "plain" leaves labels unchanged.
#' @param labels_track_height a positive numeric value for adjusting the room for the 
#'   labels. Used only when type = "rectangle".
#' @param repel logical value. Use repel = TRUE to avoid label overplotting when
#'   type = "phylogenic".
#' @param lwd a numeric value specifying dendrogram branch and rectangle line
#'   width.
#' @param type type of plot. Allowed values are one of "rectangle", "triangle", 
#'   "circular", "phylogenic".
#' @param phylo_layout the layout to be used for phylogenic trees. Default value
#'   is "layout.auto", which is kept as a compatibility alias for
#'   \code{"layout_nicely"}. Allowed values include:
#'   \code{\link[igraph]{layout.auto}}, \code{\link[igraph]{layout_nicely}},
#'   \code{\link[igraph]{layout_with_drl}}, \code{\link[igraph]{layout_as_tree}},
#'   \code{\link[igraph]{layout.gem}}, \code{\link[igraph]{layout_with_gem}},
#'   \code{\link[igraph]{layout.mds}}, \code{\link[igraph]{layout_with_mds}} and
#'   \code{\link[igraph]{layout_with_lgl}}.
#' @param rect logical value specifying whether to add a rectangle around 
#'   groups. Used only when k != NULL.
#' @param rect_border,rect_lty border color and line type for rectangles.
#' @param rect_fill a logical value. If TRUE, fill the rectangle.
#' @param lower_rect a value of how low should the lower part of the rectangle 
#'   around clusters. Ignored when rect = FALSE.
#' @param horiz a logical value. If TRUE, an horizontal dendrogram is drawn.
#' @param cex size of labels
#' @param main,xlab,ylab main and axis titles
#' @param sub Plot subtitle. Default is NULL (no subtitle). Set to a character
#'   string to display a subtitle below the title, e.g.
#'   \code{sub = paste0("Method: ", "ward.D2")}.
#' @param ggtheme function, ggplot2 theme name. Default value is 
#'   theme_classic(). Allowed values include ggplot2 official themes: 
#'   theme_gray(), theme_bw(), theme_minimal(), theme_classic(), theme_void(), 
#'   ....
#' @param ... other arguments to be passed to the function plot.dendrogram()
#' @return an object of class fviz_dend which is a ggplot with the attributes 
#'   "dendrogram" accessible using attr(x, "dendrogram"), where x is the result 
#'   of fviz_dend().
#' @examples 
#' \donttest{
#' # Load and scale the data
#' data(USArrests)
#' df <- scale(USArrests)
#' 
#' # Hierarchical clustering
#' res.hc <- hclust(dist(df))
#' 
#' # Default plot
#' fviz_dend(res.hc)
#'
#' # Increase branch and rectangle line widths
#' fviz_dend(res.hc, lwd = 2)
#' 
#' # Cut the tree
#' fviz_dend(res.hc, cex = 0.5, k = 4, color_labels_by_k = TRUE)
#' 
#' # Don't color labels, add rectangles
#' fviz_dend(res.hc, cex = 0.5, k = 4, 
#'  color_labels_by_k = FALSE, rect = TRUE)
#'  
#' # Change the color of tree using black color for all groups
#' # Change rectangle border colors
#' fviz_dend(res.hc, rect = TRUE, k_colors ="black",
#' rect_border = 2:5, rect_lty = 1)
#' 
#' # Customized color for groups
#' fviz_dend(res.hc, k = 4, 
#'  k_colors = c("#1B9E77", "#D95F02", "#7570B3", "#E7298A"))
#'  
#'  
#'  # Color labels using k-means clusters
#'  km.clust <- kmeans(df, 4)$cluster
#'  fviz_dend(res.hc, k = 4, 
#'    k_colors = c("blue", "green3", "red", "black"),
#'    label_cols =  km.clust[res.hc$order], cex = 0.6)
#' 
#'  # Phylogenic tree layouts support both compatibility aliases and
#'  # current igraph layout names
#'  if (requireNamespace("igraph", quietly = TRUE)) {
#'    fviz_dend(res.hc, type = "phylogenic", phylo_layout = "layout_nicely",
#'              show_labels = FALSE)
#'  }
#' 
#' }
#' @export
fviz_dend <- function(x, k = NULL, h = NULL, k_colors = NULL, palette = NULL,  show_labels = TRUE, color_labels_by_k = TRUE,
                      match_coord_colors = FALSE,
                      label_cols = NULL, labels_font = "plain", labels_track_height = NULL, repel = FALSE, lwd = 0.7,
                      type = c("rectangle",  "circular", "phylogenic"),
                      phylo_layout = "layout.auto",
                      rect = FALSE, rect_border = "gray", rect_lty = 2, rect_fill = FALSE, lower_rect,
                      horiz = FALSE, cex = 0.8, main = "Cluster Dendrogram", xlab = "", ylab = "Height", 
                      sub = NULL, ggtheme = theme_classic(),  ...)
{
  
  if(missing(k_colors) && !is.null(palette)) {
    k_colors <- palette
    palette <- NULL
  }
  if(!color_labels_by_k && is.null(label_cols)) label_cols <- "black"
  type <- match.arg(type)
  circular <- type == "circular"
  phylogenic <- type == "phylogenic"
  rectangle <- type == "rectangle"
  
  if(inherits(x, "HCPC")){
    # Honor an explicit k; default to the number of HCPC clusters when k is
    # NULL. The previous code always overwrote k, so a user-supplied k (e.g.
    # fviz_dend(hcpc, k = 5)) was silently ignored and colored at the HCPC
    # cluster count instead. (#81)
    if(is.null(k)) k <- length(unique(x$data.clust$clust))
    x <- x$call$t$tree #hclust
  }
    
  if(inherits(x, "hcut")){
    k <- x$nbclust
    dend <- as.dendrogram(x)
    method <- x$method
  }
  else if(inherits(x, "hkmeans")){
    k <- length(unique(x$cluster))
    dend <- as.dendrogram(x$hclust)
    method <- x$hclust$method
  }
  else if(inherits(x, c("hclust", "agnes", "diana"))) {
    dend <- as.dendrogram(x)
    method <- x$method
  }
  else if(inherits(x, "dendrogram")) {
    dend <- x
    method <- ""
  }
  else stop("Can't handle an object of class ", paste(class(x), collapse =", ") )
  if(is.null(method)) method <- ""
  else if(is.na(method)) method <- ""
  # `sub` defaults to NULL (no subtitle), preserving the previous appearance.
  # When set, it is rendered via labs(subtitle=) below (#54).
  
  if(!is.null(dendextend::labels_cex(dend))) cex <- dendextend::labels_cex(dend)
  dend <- dendextend::set(dend, "labels_cex", cex) 
  dend <- dendextend::set(dend, "branches_lwd", lwd) 
  
  k <- .get_k(dend, k, h)
  if(!is.null(k)) {
    if(.is_color_palette(k_colors)) k_colors <- ggpubr::get_palette(k_colors, k = k)
    else if(is.null(k_colors)) k_colors <- ggpubr::get_palette("default", k = k)
    # By default colours follow the left-to-right leaf order (dendextend). With
    # match_coord_colors = TRUE, remap them to cluster-label order so they match
    # fviz_cluster()/fviz_silhouette() for the same clustering (#103).
    branch_colors <- k_colors
    if(match_coord_colors){
      ord <- .coord_color_order(dend, k)
      if(!is.null(ord)) branch_colors <- k_colors[ord]
    }
    dend <- dendextend::set(dend, what = "branches_k_color", k = k, value = branch_colors)
    if(color_labels_by_k) dend <- dendextend::set(dend, "labels_col",  k = k, value = branch_colors)
  }
  
  if(!is.null(label_cols)){
    dend <- dendextend::set(dend, "labels_col", label_cols) 
  }
  
  leaflab <- ifelse(show_labels, "perpendicular", "none")
  
  if(xlab =="") xlab <- NULL
  if(ylab=="") ylab <- NULL
  
  max_height <- max(dendextend::get_branches_heights(dend))
  if(missing(labels_track_height))
    labels_track_height <- max_height/8
  if(max_height < 1) offset_labels <- -max_height/100
  else offset_labels <- -0.1
  
  if(rectangle || circular){
    p <- .ggplot_dend(dend, type = "rectangle", offset_labels = offset_labels, nodes = FALSE,
                      ggtheme = ggtheme, horiz = horiz, circular = circular, palette = palette,
                      labels = show_labels, label_cols = label_cols, labels_font = labels_font,
                      labels_track_height = labels_track_height, ...)
    if(!circular) p <- p + labs(title = main, subtitle = sub, x = xlab, y = ylab)
  }
  else if(phylogenic){
    p <- .phylogenic_tree(dend, labels = show_labels, label_cols = label_cols,
                                 palette = palette, repel = repel,
                                 ggtheme = ggtheme, phylo_layout = phylo_layout, ...)
  }
  # Add rectangle around clusters
  if(circular || phylogenic || is.null(k)) rect <- FALSE
  if(rect_fill && missing(rect_lty)) rect_lty = "blank"
  if(missing(lower_rect)) {
    # The absolute -0.5 offset overwhelms short trees (e.g. correlation/gower
    # distances with max height < 1), pushing the rectangles far below the
    # labels. For those, scale the offset to the tree height instead; taller
    # trees (>= 1) keep the previous default unchanged (#55).
    lower_rect <- if(max_height < 1) -(labels_track_height + max_height/8)
                  else -(labels_track_height + 0.5)
  }
  if(rect){
    p <- p + .rect_dendrogram(dend, k = k, palette = rect_border, rect_fill = rect_fill,
                              rect_lty = rect_lty, linewidth = lwd,
                              lower_rect = lower_rect, match_coord_colors = match_coord_colors)
  }
  
  # Keep the leaf-label text layer out of the legend (no stray "a" glyph),
  # mirroring the scatter-plot cleanup in .fviz_finish() (#14).
  p <- .hide_text_legend(p)

  attr(p, "dendrogram") <- dend
  structure(p, class = c(class(p), "fviz_dend"))
  return(p)
}


# require igraph
.phylogenic_tree <- function(dend, labels = TRUE, label_cols = NULL,
                              palette = NULL, repel = FALSE,
                              ggtheme = theme_classic(),
                             phylo_layout = "layout.auto", ...){
  
  if (!requireNamespace("igraph", quietly = TRUE)) {
    stop("igraph package needed for phylogenic tree. Please install it using install.packages('igraph').")
  }
  
  
  allowed_layouts <- c("layout.auto", "layout_nicely", "layout_with_drl", "layout_as_tree",
                      "layout.gem", "layout_with_gem", "layout.mds", "layout_with_mds", "layout_with_lgl")
  
  if(!(phylo_layout %in% allowed_layouts)) stop( phylo_layout, " is not supported as layout. ", "Allowed phylogenic layout are: ",
                                                paste( allowed_layouts, collapse = ", "))
  
  layout_func <- switch(phylo_layout,
                        layout.auto = igraph::layout_nicely,
                        layout_nicely = igraph::layout_nicely,
                        layout_with_drl = igraph::layout_with_drl,
                        layout_as_tree = igraph::layout_as_tree,
                        layout.gem = igraph::layout_with_gem,
                        layout_with_gem = igraph::layout_with_gem,
                        layout.mds = igraph::layout_with_mds,
                        layout_with_mds = igraph::layout_with_mds,
                        layout_with_lgl = igraph::layout_with_lgl
                        )
  
  # Convert to 'phylo' object
  hc <- stats::as.hclust(dend)
  phylo_tree <- .as.phylo(hc)
  graph_edges <- phylo_tree$edge
  
  # get graph from edge list
  graph_net <- igraph::graph_from_edgelist(graph_edges)
  
  # extract layout (x-y coords)
  graph_layout <- .with_preserved_seed(123, layout_func(graph_net))
  # number of observations
  nobs <- length(hc$labels)
  
  # draw tree branches
  data.segments <- data.frame(
    x = graph_layout[graph_edges[,1],1], 
    y = graph_layout[graph_edges[,1],2],
    xend = graph_layout[graph_edges[,2],1],
    yend = graph_layout[graph_edges[,2],2]
  )
  
  data.labels <- data.frame(
    x = graph_layout[1:nobs,1], 
    y = graph_layout[1:nobs,2],
    label = phylo_tree$tip.label
  )
  data.labels <- data.labels[order(as.vector(data.labels$label)), ]
  
  # Support for dendextend
  gdend <- dendextend::as.ggdend(dend)
  gdat <- dendextend::prepare.ggdend(gdend)
  gdat$labels <- gdat$labels[order(as.vector(gdat$labels$label)), ]
  data.labels <- cbind(data.labels, gdat$labels[, c("col", "cex")])
  if(!is.null(dendextend::labels_cex(dend))) font.label <- round(dendextend::labels_cex(dend)[1]*12)
  else font.label <- 12
  
  # FIX: ggplot2 3.0.0+ deprecation - aes_string() replaced with aes() + .data pronoun
  # See: https://github.com/kassambara/factoextra/issues/190
  p <- ggplot() + geom_segment(data = data.segments,
                    aes(x = .data[["x"]], y = .data[["y"]], xend = .data[["xend"]], yend = .data[["yend"]]),
                    lineend = "square")
  if(is.null(label_cols)) label_cols <- "col"
  if(!labels) labels <- NULL
  else labels <- "label"
  
  p <- ggpubr::ggscatter(data.labels, "x", "y", label = labels,
                         color = label_cols,
                         ggp = p, repel = repel, font.label = font.label, ...)
  
  if(is.null(palette)) p <- p + scale_colour_identity()
  
  p <- ggpubr::ggpar(p, ggtheme = ggtheme, palette = palette, ...) 
  
  p <- p + theme(axis.title.x = element_blank(), axis.title.y = element_blank(),
                 axis.text= element_blank(), 
                 axis.line = element_blank(), axis.ticks = element_blank(),
                 legend.position = "none")
  p
}


# Helper functions
#%%%%%%%%%%%%%%%%%%%%

# Plot dendrogram using ggplot
# .ggplot_dend derrived from dendextend::ggplot.ggdend
# data: a ggdend class object.
.ggplot_dend <- function (dend, segments = TRUE, labels = TRUE, nodes = TRUE, 
                        horiz = FALSE, ggtheme = theme_classic(), 
                        offset_labels = 0, circular = FALSE, type = "rectangle",
                        palette = NULL, label_cols = NULL, labels_font = "plain", labels_track_height = 1,
                        ...) {
  
  gdend <- dendextend::as.ggdend(dend, type = type)
  #angle <- ifelse(horiz, 0, 90)
  #hjust <- ifelse(horiz, 0, 1)
  gdend$labels$angle <- ifelse(horiz, 0, 90)
  gdend$labels$hjust <- ifelse(horiz, 0, 1)
  gdend$labels$vjust <- 0.5
  if(circular){
    # If circular, change the angle and hjust so that the labels rotate
    if(circular) {
      pms <- .get_label_params(gdend$labels)
      gdend$labels$angle <- pms$angle
      gdend$labels$hjust <- pms$hjust
    }
  }
  
  data <- dendextend::prepare.ggdend(gdend)
  # To avoid overlaping of labels at coord_polar start
  if(circular) {
    n_rows <- nrow(data$labels)
    data$labels$x[1] <- 0.7
    data$labels$vjust[1] <- 1.7
  }

  
  p <- ggplot()
  if (segments) {
    # FIX: ggplot2 3.0.0+ deprecation - aes_string() replaced with aes() + .data pronoun
    # FIX: ggplot2 3.4.0+ deprecation - size replaced with linewidth for line geoms
    # See: https://github.com/kassambara/factoextra/issues/190, #191
    p <- p + geom_segment(data = data$segments,
      aes(x = .data[["x"]], y = .data[["y"]], xend = .data[["xend"]], yend = .data[["yend"]],
                 colour = .data[["col"]], linetype = .data[["lty"]], linewidth = .data[["lwd"]]), lineend = "square") +
      # FIX: ggplot2 3.3.4+ deprecation - use "none" instead of FALSE for guides()
      # See: https://github.com/kassambara/factoextra/issues/179
      guides(linetype = "none", col = "none", linewidth = "none") + #scale_colour_identity() +
      scale_linewidth_identity() + scale_linetype_identity()
    if(is.null(palette)) p <- p + scale_colour_identity()
  }
  if (nodes) {
    # FIX: ggplot2 3.0.0+ deprecation - aes_string() replaced with aes() + .data pronoun
    # See: https://github.com/kassambara/factoextra/issues/190
    p <- p + geom_point(data = data$nodes,
      aes(x = .data[["x"]], y = .data[["y"]], colour = .data[["col"]], shape = .data[["pch"]], size = .data[["cex"]])) +
      # FIX: ggplot2 3.3.4+ deprecation - use "none" instead of FALSE for guides()
      # See: https://github.com/kassambara/factoextra/issues/179
      guides(shape = "none", col = "none", size = "none") +
      scale_shape_identity()
  }
  if (labels) {
    data$labels$cex <- 5 * data$labels$cex
    data$labels$y <- data$labels$y + offset_labels
    if(is.null(label_cols)) label_cols <- "col"
    p <- p + ggpubr::geom_exec(geom_text, data = data$labels,
                               x = "x", y = "y", label = "label", color = label_cols, size = "cex",
                                angle = "angle", hjust = "hjust", vjust = "vjust")
    # Apply a single font face to all leaf labels (e.g. "italic") as a fixed
    # layer parameter. Only when non-default, so the "plain" path is untouched (#121).
    if(!identical(labels_font, "plain"))
      p$layers[[length(p$layers)]]$aes_params$fontface <- labels_font
  }
  p <- ggpubr::ggpar(p, ggtheme = ggtheme, palette = palette, ...) + theme(axis.line = element_blank())
  if (horiz && !circular) {
    p <- p + coord_flip() + scale_y_reverse()+
      theme(axis.text.y = element_blank(), axis.ticks.y = element_blank(),
            axis.text.x = element_text())
  }
  else p <- p + theme(axis.text.x = element_blank(), axis.ticks.x = element_blank())
  
  if(circular){
    p <- p + theme(plot.margin = margin(0, 0, 0, 0),
                  axis.title.x = element_blank(), axis.title.y = element_blank(),
                  axis.text= element_blank(), 
                  axis.line = element_blank(), axis.ticks = element_blank())+
                  ylim(max(dendextend::get_branches_heights(dend)), -1)+
                  coord_polar(theta = 'x', direction = 1)
    
  }
  else{
    p <- p + expand_limits(y=-labels_track_height)
  }
  
  p
}

# Function used for circular dendrogram
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Create the angle and hjust vectors so that the labels 
# rotation switches from 6 o'clock to 12 o'clock to improve readability. 
.get_label_params <- function(labeldf) {        
  nn <- length(labeldf$y)
  halfn <- floor(nn/2)
  firsthalf <- rev(90 + seq(0,360, length.out = nn))
  secondhalf <- rev(-90 + seq(0,360, length.out = nn))
  angle <- numeric(nn)
  angle[1:halfn] <- firsthalf[1:halfn]
  angle[(halfn+1):nn] <- secondhalf[(halfn+1):nn]
  
  hjust <- numeric(nn)
  hjust[1:halfn] <- 0
  hjust[(halfn+1):nn] <- 1
  
  return(list(angle = angle, hjust = hjust))
}


# Convert 'hclust' to 'phylo' object
# used for phylogenic tree
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# from ape::as.phylo.hclust
# x hclust
.as.phylo <- function (x, ...) 
{
  N <- dim(x$merge)[1]
  edge <- matrix(0L, 2 * N, 2)
  edge.length <- numeric(2 * N)
  node <- integer(N)
  node[N] <- N + 2L
  cur.nod <- N + 3L
  j <- 1L
  for (i in N:1) {
    edge[j:(j + 1), 1] <- node[i]
    for (l in 1:2) {
      k <- j + l - 1L
      y <- x$merge[i, l]
      if (y > 0) {
        edge[k, 2] <- node[y] <- cur.nod
        cur.nod <- cur.nod + 1L
        edge.length[k] <- x$height[i] - x$height[y]
      }
      else {
        edge[k, 2] <- -y
        edge.length[k] <- x$height[i]
      }
    }
    j <- j + 2L
  }
  if (is.null(x$labels)) 
    x$labels <- as.character(1:(N + 1))
  obj <- list(edge = edge, edge.length = edge.length/2, tip.label = x$labels, 
              Nnode = N)
  class(obj) <- "phylo"
  obj
  #ape::reorder.phylo(obj)
}

# Get k value if h specified
# Make also some checking
# dend a dendrogram object
# h: tree height
.get_k <- function(dend, k = NULL, h = NULL){
  
  if (!dendextend::is.dendrogram(dend)) stop("x is not a dendrogram object.")
  if (length(h) > 1L || length(k) > 1L) 
    stop("'k' and 'h' must be a scalar(i.e.: of length 1)")
  tree_heights <- dendextend::heights_per_k.dendrogram(dend)[-1]
  tree_order <- stats::order.dendrogram(dend)
  if (!is.null(h)) {
    if (!is.null(k)) 
      stop("specify exactly one of 'k' and 'h'")
    ss_ks <- tree_heights < h
    k <- min(as.numeric(names(ss_ks))[ss_ks])
    k <- max(k, 2)
  }
  k
}


# Left-to-right cluster-label order of a dendrogram cut into k groups.
# dendextend colours branches in leaf (left-to-right) order, whereas
# fviz_cluster()/fviz_silhouette() colour by cluster label. Remapping a length-k
# colour vector by this order makes the colours match across the three plots
# (#103). Returns NULL when the cut is not a clean permutation of seq_len(k).
.coord_color_order <- function(dend, k){
  # dendextend colours the g-th cluster in LEAF (left-to-right) order with
  # value[g]. fviz_cluster()/fviz_silhouette() colour by the DATA-order cluster
  # label (cutree's labelling). So value[g] should be k_colors[ data-label of the
  # g-th leaf cluster ]. That permutation is the data-order labels read in leaf
  # order: unique(cutree(.., as_data = TRUE)[order.dendrogram(dend)]).
  cl  <- tryCatch(dendextend::cutree(dend, k = k, order_clusters_as_data = TRUE),
                  error = function(e) NULL)
  ord <- tryCatch(stats::order.dendrogram(dend), error = function(e) NULL)
  if(is.null(cl) || is.null(ord) || length(cl) != length(ord)) return(NULL)
  lr <- unique(cl[ord])
  lr <- lr[!is.na(lr)]
  if(length(lr) == k && setequal(lr, seq_len(k))) as.integer(lr) else NULL
}

# Add rectangle to a dendrogram
# lower_rect: a (scalar) value of how low should the lower part of the rect be.
.rect_dendrogram <- function (dend, k = NULL,  h = NULL,
                             k_colors = NULL, palette = NULL, rect_fill = FALSE, rect_lty = 2,
                             lower_rect=-1.5, match_coord_colors = FALSE,
          ...)
{

  if(missing(k_colors) && !is.null(palette)) k_colors <- palette
  # value (should be between 0 to 1): proportion of the height 
  # our rect will be between the height needed for k and k+1 clustering.
  prop_k_height <-  0.5
  
  if (!dendextend::is.dendrogram(dend)) 
    stop("x is not a dendrogram object.")
  
  k <- .get_k(dend, k, h)
  tree_heights <- dendextend::heights_per_k.dendrogram(dend)[-1]
  tree_order <- stats::order.dendrogram(dend)
  
  if (is.null(k)) stop("specify k")
  if (k < 2) {
      stop(gettextf("k must be between 2 and %d", length(tree_heights)), 
              domain = NA)
  }

  cluster <- dendextend::cutree(dend, k = k)
  clustab <- table(cluster)[unique(cluster[tree_order])]
  m <- c(0, cumsum(clustab))
  which <- 1L:k
  
  # Collapse height lookups to a single value: tied merge heights can make
  # `names(tree_heights) == k` match more than one entry, which would make
  # ytop (and thus the rectangle data frame) longer than k rows and break the
  # geom_rect aesthetics. Well-behaved dendrograms match exactly one (#154, #168).
  k_height <- tree_heights[names(tree_heights) == k]
  k_height <- if (length(k_height) == 0) 0 else max(k_height)
  next_k_height <- tree_heights[names(tree_heights) == k + 1]
  if (length(next_k_height) == 0) {
    next_k_height <- 0
    prop_k_height <- 1
  } else next_k_height <- max(next_k_height)

  xleft <- ybottom <- xright <- ytop <- list()
  for (n in seq_along(which)) {
    xleft[[n]] = m[which[n]] + 0.66
    ybottom[[n]] = lower_rect
    xright[[n]] = m[which[n] + 1] + 0.33
    ytop[[n]] <- k_height * prop_k_height + next_k_height * (1 - prop_k_height)
  }
  
  df <- data.frame(xmin = unlist(xleft), ymin = unlist(ybottom), xmax = unlist(xright), ymax = unlist(ytop))
  
  color <- k_colors
  # FIX: R 4.2.0+ deprecation - condition has length > 1 warning
  # Use length() == 1 check before scalar comparison
  # See: https://github.com/kassambara/factoextra/issues/163, #180
  if(length(color) == 1 && color == "cluster") color <- "default"
  if(.is_color_palette(color)) color <- ggpubr::get_palette(color, k = k)
  else if(length(color) > 1 && length(color) != k){
    # recycle/trim a multi-colour vector to exactly k (one per rectangle)
    color <- rep_len(color, k)
  }
  # Align rectangle colours to cluster-label order (matches branch/label colours
  # and fviz_cluster()/fviz_silhouette()) when requested (#103).
  if(match_coord_colors && length(color) == k){
    ord <- .coord_color_order(dend, k)
    if(!is.null(ord)) color <- color[ord]
  }
  if(rect_fill){
    fill <- color
    alpha <- 0.2
  }
  else {
    fill <- "transparent"
    alpha <- 0
  }
  df$color <- color
  
  df$cluster <- as.factor(paste0("c", 1:k))
  ggpubr::geom_exec(geom_rect, data = df, 
                    xmin = "xmin", ymin = "ymin", xmax = "xmax", ymax = "ymax",
                    fill = fill, color = color, linetype = rect_lty, alpha = alpha,  ...)
}
