#' Create Tesselation Visualization Plots
#'
#' Creates three types of visualization plots for 2D projected data: ellipse plots,
#' Voronoi diagram plots, and combined ellipse-Voronoi plots. The function is designed
#' to visualize class separation in 2D data.
#'
#' @param data A data frame containing projected data. Must have at least 2 numeric columns.
#'   If more than 2 columns are provided, the first 2 are used as coordinates.
#' @param class_column Character string specifying the column name containing class labels,
#'   or a vector of class labels. If NULL, all observations are treated as a single class.
#'   Default: NULL.
#' @param alternative_class_column Character string specifying the column name containing
#'   alternative class labels, or a vector of alternative class labels. If NULL, uses
#'   class_column. Default: NULL.
#' @param coordinate_columns Character vector of length 2 specifying the column names
#'   to use as coordinates. If NULL, uses the first two numeric columns. Default: NULL.
#' @param case_labels Character vector of case labels for individual observations.
#'   If NULL, row numbers are used. Default: NULL.
#' @param coord_names Character vector of length 2 specifying names for the coordinate axes.
#'   Default: c("Dim1", "Dim2").
#' @param title Character string for plot title. If NULL, no title is added. Default: NULL.
#' @param show_labels Logical indicating whether to show case labels on plots. Default: FALSE.
#' @param ellipse_alpha Numeric value (0-1) for ellipse transparency. Default: 0.1.
#' @param voronoi_alpha Numeric value (0-1) for Voronoi polygon transparency. Default: 0.3.
#' @param point_size Numeric value for point size. Default: 2.
#' @param legend_position Character string or numeric vector specifying legend position.
#'   Default: "bottom".
#' @param color_palette Function or character vector for color palette. If NULL, uses
#'   ggplot2 default colors. Default: NULL.
#' @param add_grid_lines Logical indicating whether to add dashed grid lines at origin.
#'   Default: FALSE.
#' @param color_points Character string specifying which classification to use for point colors.
#'   Either "primary" (uses class_column) or "alternative" (uses alternative_class_column).
#'   Default: "primary".
#' @param fill_voronoi Character string specifying which classification to use for Voronoi fill.
#'   Either "primary" (uses class_column) or "alternative" (uses alternative_class_column).
#'   Default: "primary".
#' @param point_shape Character string specifying which classification to use for point shapes.
#'   Either "primary" (uses class_column), "alternative" (uses alternative_class_column),
#'   or "none" (no shape differentiation). Default: "none".
#' @param label_fontface Character string specifying the font face for text labels.
#'   Options include "plain", "bold", "italic", "bold.italic". Default: "plain".
#' @param label_size Numeric value specifying the size of text labels. Default: 3.
#' @param show_island_count Logical indicating whether to display the Voronoi island count
#'   as a subtitle on the Voronoi-based plots (voronoi_plot and voronoi_plot_plus_ellipse).
#'   A "Voronoi island" is a data point whose Voronoi cell is completely surrounded by cells
#'   belonging to a different class, making it a visualization-intrinsic measure of class
#'   structure disruption. The ellipse_plot is unaffected. Default: FALSE.
#' @param label_islands_only Logical indicating whether to show case labels on plots only for
#'   Voronoi islands. Default: FALSE.
#'
#' @return A list containing three ggplot objects:
#'   \item{ellipse_plot}{Plot with confidence ellipses for each class}
#'   \item{voronoi_plot}{Plot with Voronoi tessellation regions (subtitle shows island
#'     count if show_island_count = TRUE)}
#'   \item{voronoi_plot_plus_ellipse}{Combined plot with both Voronoi regions and ellipses
#'     (subtitle shows island count if show_island_count = TRUE)}
#'
#' @details The function creates visualizations for 2D data, particularly useful
#'   for displaying results from dimensionality reduction techniques like PCA, PLS-DA, or UMAP.
#'
#'   Voronoi tessellation divides the plot space into regions based on proximity to data points,
#'   providing an intuitive visualization of class boundaries and decision regions.
#'
#'   Confidence ellipses show the distribution spread and correlation structure within each class.
#'
#'   The Voronoi island count is computed from the neighbor graph embedded in the Delaunay
#'   triangulation (the dirsgs component of the deldir output). Two cells are neighbors if
#'   they share a Voronoi edge. A cell is an island if all its neighbors belong to a different
#'   class. The metric uses the same classification as \code{fill_voronoi}, so it reflects
#'   the visually displayed class assignments in the plot.
#'
#' @examples
#' # Basic usage with iris dataset
#' data <- iris[, c("Sepal.Length", "Petal.Length", "Species")]
#' plots <- create_tesselation_plots(
#'   data = data,
#'   class_column = "Species",
#'   legend_position = "bottom",
#'   add_grid_lines = FALSE
#' )
#'
#' # With island count displayed on Voronoi plots
#' plots <- create_tesselation_plots(
#'   data = data,
#'   class_column = "Species",
#'   show_island_count = TRUE
#' )
#'
#' @importFrom ggplot2 ggplot aes geom_point geom_polygon stat_ellipse theme_light
#' @importFrom ggplot2 labs theme element_rect alpha guides geom_vline geom_hline
#' @importFrom ggplot2 scale_color_manual scale_fill_manual
#' @importFrom deldir deldir tile.list
#' @importFrom ggrepel geom_text_repel
#' @export
create_tesselation_plots <- function(data,
                                    class_column = NULL,
                                    alternative_class_column = NULL,
                                    coordinate_columns = NULL,
                                    case_labels = NULL,
                                    coord_names = c("Dim1", "Dim2"),
                                    title = NULL,
                                    show_labels = FALSE,
                                    ellipse_alpha = 0.1,
                                    voronoi_alpha = 0.3,
                                    point_size = 2,
                                    legend_position = "bottom",
                                    color_palette = NULL,
                                    add_grid_lines = FALSE,
                                    color_points = "primary",
                                    fill_voronoi = "primary",
                                    point_shape = "none",
                                    label_fontface = "plain",
                                    label_size = 3.88,
                                    show_island_count = FALSE,
                                    label_islands_only = FALSE) {

  # Input validation
  if (!is.data.frame(data)) {
    stop("'data' must be a data.frame")
  }

  if (ncol(data) < 2) {
    stop("'data' must have at least 2 columns for coordinates")
  }

  if (!color_points %in% c("primary", "alternative")) {
    stop("'color_points' must be either 'primary' or 'alternative'")
  }

  if (!fill_voronoi %in% c("primary", "alternative")) {
    stop("'fill_voronoi' must be either 'primary' or 'alternative'")
  }

  if (!point_shape %in% c("primary", "alternative", "none")) {
    point_shape <- "none"
    warning("'point_shape' must be either 'primary', 'alternative', or 'none'. Setting to 'none'.")
  }

  # Extract coordinates
  if (is.null(coordinate_columns)) {
    numeric_cols <- sapply(data, is.numeric)
    if (sum(numeric_cols) < 2) {
      stop("'data' must have at least 2 numeric columns")
    }
    coord_cols <- names(data)[numeric_cols][1:2]
  } else {
    if (length(coordinate_columns) != 2) {
      stop("'coordinate_columns' must specify exactly 2 column names")
    }
    if (!all(coordinate_columns %in% names(data))) {
      missing_cols <- coordinate_columns[!coordinate_columns %in% names(data)]
      stop(paste("Coordinate columns not found in data:", paste(missing_cols, collapse = ", ")))
    }
    if (!all(sapply(data[coordinate_columns], is.numeric))) {
      stop("Specified coordinate columns must be numeric")
    }
    coord_cols <- coordinate_columns
  }

  # Prepare plot dataframe
  plot_dataframe <- data.frame(
    x = data[[coord_cols[1]]],
    y = data[[coord_cols[2]]]
  )

  # Helper function to process class column
  process_class_column <- function(class_col, col_name) {
    if (is.null(class_col)) {
      factor(rep(1, nrow(data)))
    } else if (is.character(class_col) && length(class_col) == 1) {
      if (!class_col %in% names(data)) {
        stop(paste("Column", class_col, "not found in data"))
      }
      as.factor(data[[class_col]])
    } else {
      if (length(class_col) != nrow(data)) {
        stop(paste("Length of", col_name, "must match number of rows in 'data'"))
      }
      as.factor(class_col)
    }
  }

  # Handle primary class column
  plot_dataframe$group_primary <- process_class_column(class_column, "'class_column'")

  # Handle alternative class column
  if (is.null(alternative_class_column)) {
    plot_dataframe$group_alternative <- plot_dataframe$group_primary
  } else {
    plot_dataframe$group_alternative <- process_class_column(alternative_class_column, "'alternative_class_column'")
  }

  # Set the active grouping variables based on parameters
  plot_dataframe$group_color <- if (color_points == "primary") {
    plot_dataframe$group_primary
  } else {
    plot_dataframe$group_alternative
  }

  plot_dataframe$group_fill <- if (fill_voronoi == "primary") {
    plot_dataframe$group_primary
  } else {
    plot_dataframe$group_alternative
  }

  # Add shape grouping variable
  plot_dataframe$group_shape <- if (point_shape == "primary") {
    plot_dataframe$group_primary
  } else if (point_shape == "alternative") {
    plot_dataframe$group_alternative
  } else {
    factor(rep(1, nrow(plot_dataframe))) # All same shape when "none"
  }

  # Handle case labels
  if (is.null(case_labels)) {
    plot_dataframe$labels <- as.character(seq_len(nrow(data)))
  } else {
    if (length(case_labels) != nrow(data)) {
      stop("Length of 'case_labels' must match number of rows in 'data'")
    }
    plot_dataframe$labels <- as.character(case_labels)
  }

  rownames(plot_dataframe) <- plot_dataframe$labels

  # ---------------------------------------------------------------------------
  # Helper: get axis limits from a built ggplot object
  # ---------------------------------------------------------------------------
  get_plot_limits <- function(plot) {
    gb <- ggplot2::ggplot_build(plot)
    list(
      xmin = gb$layout$panel_params[[1]]$x.range[1],
      xmax = gb$layout$panel_params[[1]]$x.range[2],
      ymin = gb$layout$panel_params[[1]]$y.range[1],
      ymax = gb$layout$panel_params[[1]]$y.range[2]
    )
  }

  # ---------------------------------------------------------------------------
  # Helper: compute Voronoi tessellation
  # Returns a list with:
  #   $polygon_data  - data frame of polygon vertices for ggplot2
  #   $tessellation  - raw deldir object (reused for island detection at no
  #                    extra cost, since deldir is only called once)
  # ---------------------------------------------------------------------------
  compute_voronoi_diagram <- function(x_coords, y_coords, class_groups,
                                      bounding_box = NULL) {
    if (!requireNamespace("deldir", quietly = TRUE)) {
      stop("Package 'deldir' is required for Voronoi diagrams. Please install it.")
    }

    if (is.null(bounding_box)) {
      coordinate_range <- function(values) range(values, na.rm = TRUE)
      bounding_box <- c(coordinate_range(x_coords), coordinate_range(y_coords))
    }

    tessellation <- deldir::deldir(x_coords, y_coords, rw = bounding_box)
    tiles <- deldir::tile.list(tessellation)

    polygon_data <- do.call(rbind, lapply(seq_along(tiles), function(i) {
      data.frame(
        x = tiles[[i]]$x,
        y = tiles[[i]]$y,
        id = i,
        class = class_groups[i],
        stringsAsFactors = FALSE
      )
    }))

    list(polygon_data = polygon_data, tessellation = tessellation)
  }

  # ---------------------------------------------------------------------------
  # Helper: compute Voronoi island count
  #
  # A "Voronoi island" is a data point whose cell is entirely surrounded by
  # cells belonging to a different class. Neighbor relationships are read from
  # the Delaunay triangulation (dirsgs), which is the geometric dual of the
  # Voronoi tessellation: two cells share a Voronoi edge if and only if their
  # points are connected by a Delaunay edge.
  #
  # Uses the same classification as fill_voronoi (group_fill), so the metric
  # reflects the visually displayed class assignments in the plot.
  #
  # Returns a list: $count, $rate, $indices
  # ---------------------------------------------------------------------------
  compute_voronoi_islands <- function(tessellation, class_groups) {
    neighbor_pairs <- tessellation$dirsgs[, c("ind1", "ind2")]

    n <- length(class_groups)
    island_flags <- logical(n)

    for (k in seq_len(n)) {
      neighbors <- c(
        neighbor_pairs$ind2[neighbor_pairs$ind1 == k],
        neighbor_pairs$ind1[neighbor_pairs$ind2 == k]
      )
      # Points touching only the bounding box may have no dirsgs neighbors; skip.
      if (length(neighbors) == 0) next
      island_flags[k] <- all(class_groups[neighbors] != class_groups[k])
    }

    list(
      count = sum(island_flags),
      rate = mean(island_flags),
      indices = which(island_flags)
    )
  }

  # ---------------------------------------------------------------------------
  # Create base plot for ellipse plot
  # ---------------------------------------------------------------------------
  create_ellipse_base_plot <- function() {
    if (point_shape == "none") {
      p <- ggplot2::ggplot(data = plot_dataframe,
                           ggplot2::aes(x = x, y = y, color = group_color,
                                        fill = group_primary))
    } else {
      p <- ggplot2::ggplot(data = plot_dataframe,
                           ggplot2::aes(x = x, y = y, color = group_color,
                                        fill = group_primary, shape = group_shape))
    }

    if (!is.null(color_palette)) {
      if (is.function(color_palette)) {
        p <- p + color_palette()
      } else if (is.character(color_palette)) {
        p <- p + ggplot2::scale_color_manual(values = color_palette) +
          ggplot2::scale_fill_manual(values = color_palette)
      }
    }

    p <- p + ggplot2::theme_light() +
      ggplot2::theme(
        legend.position = legend_position,
        legend.background = ggplot2::element_rect(fill = ggplot2::alpha("white", 0.2))
      )

    if (point_shape == "none") {
      p <- p + ggplot2::labs(title = title, x = coord_names[1], y = coord_names[2],
                             color = "Class", fill = "Class")
    } else {
      p <- p + ggplot2::labs(title = title, x = coord_names[1], y = coord_names[2],
                             color = "Class", fill = "Class", shape = "Class")
    }

    if (add_grid_lines) {
      p <- p +
        ggplot2::geom_vline(xintercept = 0, color = "grey20", linetype = "dashed") +
        ggplot2::geom_hline(yintercept = 0, color = "grey20", linetype = "dashed")
    }

    return(p)
  }

  # ---------------------------------------------------------------------------
  # Ellipse plot  (no island count — ellipses have no tessellation)
  # ---------------------------------------------------------------------------
  ellipse_plot <- create_ellipse_base_plot() +
    ggplot2::geom_point(size = point_size) +
    ggplot2::stat_ellipse(geom = "polygon", alpha = ellipse_alpha) +
    ggplot2::guides(shape = "none", fill = "none")

  if (point_shape == "none") {
    ellipse_plot <- ellipse_plot + ggplot2::guides(fill = "none")
  } else {
    ellipse_plot <- ellipse_plot + ggplot2::guides(shape = "none", fill = "none")
  }

  if (show_labels && requireNamespace("ggrepel", quietly = TRUE)) {
    ellipse_plot <- ellipse_plot +
      ggrepel::geom_text_repel(
        ggplot2::aes(label = labels),
        fontface = label_fontface,
        size = label_size,
        max.overlaps = Inf,
        show.legend = FALSE
      )
  }

  # ---------------------------------------------------------------------------
  # Voronoi tessellation  (single deldir call; tessellation reused for islands)
  # ---------------------------------------------------------------------------
  voronoi_result <- compute_voronoi_diagram(
    plot_dataframe$x,
    plot_dataframe$y,
    plot_dataframe$group_fill,
    bounding_box = unlist(get_plot_limits(ellipse_plot))
  )
  voronoi_data <- voronoi_result$polygon_data
  voronoi_tessellation <- voronoi_result$tessellation

  # Optionally compute island count and build subtitle string.
  # The subtitle is the same for both Voronoi-based plots.
  subtitle_text <- NULL
  if (show_island_count) {
    islands <- compute_voronoi_islands(
      voronoi_tessellation,
      plot_dataframe$group_fill # use same classification as Voronoi fill
    )
    subtitle_text <- sprintf(
      "Voronoi islands: %d (%.1f%%)",
      islands$count,
      islands$rate * 100
    )
  }

  # ---------------------------------------------------------------------------
  # Build Voronoi plot
  # ---------------------------------------------------------------------------
  build_voronoi_base <- function() {
    if (point_shape == "none") {
      plt <- ggplot2::ggplot() +
        ggplot2::geom_polygon(
          data = voronoi_data,
          ggplot2::aes(x = x, y = y, group = id, fill = class),
          alpha = voronoi_alpha, color = NA, show.legend = FALSE
        ) +
        ggplot2::geom_point(
          data = plot_dataframe,
          ggplot2::aes(x = x, y = y, color = group_color),
          size = point_size
        )
    } else {
      plt <- ggplot2::ggplot() +
        ggplot2::geom_polygon(
          data = voronoi_data,
          ggplot2::aes(x = x, y = y, group = id, fill = class),
          alpha = voronoi_alpha, color = NA, show.legend = FALSE
        ) +
        ggplot2::geom_point(
          data = plot_dataframe,
          ggplot2::aes(x = x, y = y, color = group_color, shape = group_shape),
          size = point_size
        )
    }

    plt <- plt +
      ggplot2::theme_light() +
      ggplot2::labs(
        title = title,
        subtitle = subtitle_text, # NULL when show_island_count = FALSE
        x = coord_names[1],
        y = coord_names[2],
        color = "Class"
      ) +
      ggplot2::theme(
        legend.position = legend_position,
        legend.background = ggplot2::element_rect(fill = ggplot2::alpha("white", 0.2))
      )

    if (!is.null(color_palette)) {
      if (is.function(color_palette)) {
        plt <- plt + color_palette()
      } else if (is.character(color_palette)) {
        plt <- plt +
          ggplot2::scale_color_manual(values = color_palette) +
          ggplot2::scale_fill_manual(values = color_palette)
      }
    }

    if (add_grid_lines) {
      plt <- plt +
        ggplot2::geom_vline(xintercept = 0, color = "grey20", linetype = "dashed") +
        ggplot2::geom_hline(yintercept = 0, color = "grey20", linetype = "dashed")
    }

    if (show_labels && requireNamespace("ggrepel", quietly = TRUE)) {
      if (label_islands_only && show_island_count) plot_dataframe$labels[-islands$indices] <- NA
      plt <- plt +
        ggrepel::geom_text_repel(
          data = plot_dataframe,
          ggplot2::aes(x = x, y = y, color = group_color, label = labels),
          fontface = label_fontface,
          size = label_size,
          max.overlaps = Inf,
          show.legend = FALSE
        )
    }

    return(plt)
  }

  voronoi_plot <- build_voronoi_base()

  # ---------------------------------------------------------------------------
  # Combined Voronoi + ellipse plot (reuses the same base; same subtitle)
  # ---------------------------------------------------------------------------
  voronoi_plot_plus_ellipse <- build_voronoi_base() +
    ggplot2::stat_ellipse(
      data = plot_dataframe,
      ggplot2::aes(x = x, y = y, color = group_color, fill = group_primary),
      geom = "polygon",
      alpha = ellipse_alpha,
      show.legend = FALSE
    )

  return(list(
    ellipse_plot = ellipse_plot,
    voronoi_plot = voronoi_plot,
    voronoi_plot_plus_ellipse = voronoi_plot_plus_ellipse
  ))
}
