#' Compute a heatmap of enrichment of gene sets on the context of a diffusion component.
#'
#' @inheritParams doc_function
#' @param colors.use \strong{\code{\link[base]{list}}} | A named list of named vectors. The names of the list correspond to the names of the values provided to metadata and the names of the items in the named vectors correspond to the unique values of that specific metadata variable. The values are the desired colors in HEX code for the values to plot. The used are pre-defined by the package but, in order to get the most out of the plot, please provide your custom set of colors for each metadata column! 
#' @param main.heatmap.size \strong{\code{\link[base]{numeric}}} | A number from 0 to 1 corresponding to how big the main heatmap plot should be with regards to the rest (corresponds to the proportion in size).  
#' @return A list of ggplot2 objects and a Seurat object if desired.
#' @export
#'
#' @example /man/examples/examples_do_RankedExpressionPlot.R
do_RankedExpressionPlot <- function(sample,
                                    features,
                                    assay = NULL,
                                    slot = NULL,
                                    dims = 1:2,
                                    subsample = 2500,
                                    reduction = NULL,
                                    group.by = NULL,
                                    colors.use = NULL,
                                    raster = FALSE,
                                    interpolate = FALSE,
                                    nbin = 24,
                                    ctrl = 100,
                                    main.heatmap.size = 0.95,
                                    enforce_symmetry = TRUE,
                                    use_viridis = FALSE,
                                    viridis.palette = "G",
                                    viridis.direction = -1,
                                    sequential.palette = "YlGnBu",
                                    sequential.direction = 1,
                                    font.size = 14,
                                    font.type = "sans",
                                    na.value = "grey75",
                                    legend.width = 1,
                                    legend.length = 20,
                                    legend.framewidth = 0.5,
                                    legend.tickwidth = 0.5,
                                    legend.framecolor = "grey50",
                                    legend.tickcolor = "white",
                                    legend.type = "colorbar",
                                    legend.position = "bottom",
                                    legend.nrow = NULL,
                                    legend.ncol = NULL,
                                    legend.byrow = FALSE,
                                    number.breaks = 5,
                                    diverging.palette = "RdBu",
                                    diverging.direction = -1,
                                    axis.text.x.angle = 45,
                                    border.color = "black",
                                    return_object = FALSE,
                                    verbose = FALSE,
                                    plot.title.face = "bold",
                                    plot.subtitle.face = "plain",
                                    plot.caption.face = "italic",
                                    axis.title.face = "bold",
                                    axis.text.face = "plain",
                                    legend.title.face = "bold",
                                    legend.text.face = "plain"){
  # Add lengthy error messages.
  withr::local_options(.new = list("warning.length" = 8170))
  
  check_suggests("do_RankedExpressionPlot")
  check_Seurat(sample = sample)
  
  # Check the reduction.
  reduction <- check_and_set_reduction(sample = sample, reduction = reduction)
  
  # Check logical parameters.
  logical_list <- list("enforce_symmetry" = enforce_symmetry,
                       "legend.byrow" = legend.byrow,
                       "return_object" = return_object,
                       "use_viridis" = use_viridis,
                       "verbose" = verbose,
                       "interpolate" = interpolate,
                       "raster" = raster)
  check_type(parameters = logical_list, required_type = "logical", test_function = is.logical)
  
  # Check numeric parameters.
  numeric_list <- list("dims" = dims,
                       "subsample" = subsample,
                       "nbin" = nbin,
                       "ctrl" = ctrl,
                       "font.size" = font.size,
                       "legend.width" = legend.width,
                       "legend.length" = legend.length,
                       "legend.framewidth" = legend.framewidth,
                       "legend.tickwidth" = legend.tickwidth,
                       "number.breaks" = number.breaks,
                       "axis.text.x.angle" = axis.text.x.angle,
                       "legend.nrow" = legend.nrow,
                       "legend.ncol" = legend.ncol,
                       "main.heatmap.size" = main.heatmap.size,
                       "viridis.direction" = viridis.direction,
                       "sequential.direction" = sequential.direction,
                       "diverging.direction" = diverging.direction)
  check_type(parameters = numeric_list, required_type = "numeric", test_function = is.numeric)
  
  # Check character parameters.
  character_list <- list("assay" = assay,
                         "reduction" = reduction,
                         "slot" = slot,
                         "group.by" = group.by,
                         "font.type" = font.type,
                         "na.value" = na.value,
                         "legend.framecolor" = legend.framecolor,
                         "legend.tickcolor" = legend.tickcolor,
                         "legend.type" = legend.type,
                         "legend.position" = legend.position,
                         "viridis.palette" = viridis.palette,
                         "sequential.palette" = sequential.palette,
                         "plot.title.face" = plot.title.face,
                         "plot.subtitle.face" = plot.subtitle.face,
                         "plot.caption.face" = plot.caption.face,
                         "axis.title.face" = axis.title.face,
                         "axis.text.face" = axis.text.face,
                         "legend.title.face" = legend.title.face,
                         "legend.text.face" = legend.text.face)
  check_type(parameters = character_list, required_type = "character", test_function = is.character)
  
  check_colors(na.value, parameter_name = "na.value")
  check_colors(legend.framecolor, parameter_name = "legend.framecolor")
  check_colors(legend.tickcolor, parameter_name = "legend.tickcolor")
  check_colors(border.color, parameter_name = "border.color")
  
  check_parameters(parameter = legend.position, parameter_name = "legend.position")
  check_parameters(parameter = font.type, parameter_name = "font.type")
  check_parameters(parameter = legend.type, parameter_name = "legend.type")
  check_parameters(parameter = number.breaks, parameter_name = "number.breaks")
  check_parameters(parameter = diverging.palette, parameter_name = "diverging.palette")
  check_parameters(parameter = sequential.palette, parameter_name = "sequential.palette")
  check_parameters(parameter = viridis.palette, parameter_name = "viridis.palette")
  check_parameters(plot.title.face, parameter_name = "plot.title.face")
  check_parameters(plot.subtitle.face, parameter_name = "plot.subtitle.face")
  check_parameters(plot.caption.face, parameter_name = "plot.caption.face")
  check_parameters(axis.title.face, parameter_name = "axis.title.face")
  check_parameters(axis.text.face, parameter_name = "axis.text.face")
  check_parameters(legend.title.face, parameter_name = "legend.title.face")
  check_parameters(legend.text.face, parameter_name = "legend.text.face")
  check_parameters(viridis.direction, parameter_name = "viridis.direction")
  check_parameters(sequential.direction, parameter_name = "sequential.direction")
  check_parameters(diverging.direction, parameter_name = "diverging.direction")
  
  `%>%` <- magrittr::`%>%`
  `:=` <- rlang::`:=`
  
  # nocov start
  if (is.null(sample@reductions[[reduction]]@key) | is.na(sample@reductions[[reduction]]@key)){
    stop(paste0(add_cross(),
                crayon_body("Assay "),
                crayon_key("key"), 
                crayon_body(" not found for the provided"),
                crayon_key(" assay"),
                crayon_body(". Please set a key. \n\nYou can do it as: "),
                cli::style_italic(paste0(crayon_key('sample@reductions[['), cli::col_yellow("reduction"), crayon_key(']]@key <- "DC_"')))), call. = FALSE)
  }
  # nocov end
  key <- sample@reductions[[reduction]]@key
  
  if (!is.na(subsample)){
    # Perform subsampling.
    sample <- sample[, sample(colnames(sample, subsample))]
  }
  
  # Check group.by.
  out <- check_group_by(sample = sample,
                        group.by = group.by,
                        is.heatmap = TRUE)
  sample <- out[["sample"]]
  group.by <- out[["group.by"]]
  
  if (isTRUE(enforce_symmetry)){
    colors.gradient <- compute_continuous_palette(name = diverging.palette,
                                                  use_viridis = FALSE,
                                                  direction = diverging.direction,
                                                  enforce_symmetry = enforce_symmetry)
  } else {
    colors.gradient <- compute_continuous_palette(name = ifelse(isTRUE(use_viridis), viridis.palette, sequential.palette),
                                                  use_viridis = use_viridis,
                                                  direction = ifelse(isTRUE(use_viridis), viridis.direction, sequential.direction),
                                                  enforce_symmetry = enforce_symmetry)
  }
  
  genes.use <- features %>% unique()
  genes.use <- genes.use[genes.use %in% rownames(sample)]
  
  
  if (is.null(assay)){assay <- check_and_set_assay(sample)$assay}
  if (is.null(slot)){slot <- check_and_set_slot(slot)}
  
  if (isTRUE(verbose)){message(paste0(add_info(initial_newline = FALSE), crayon_body("Plotting "), crayon_key("heatmaps"), crayon_body("...")))}
  key_col <- stringr::str_remove_all(key, "_")
  # Obtain the DC embeddings, together with the enrichment scores.
  suppressWarnings({
    # Workaround parameter depreciation.
  if (base::isTRUE(utils::packageVersion("Seurat") < "4.9.9")){
    data <- Seurat::GetAssayData(object = sample,
                                 assay = assay,
                                 slot = slot)
  } else {
    data <- SeuratObject::LayerData(object = sample,
                                    assay = assay,
                                    layer = slot)
  }
    
  data.use <- sample@reductions[[reduction]]@cell.embeddings %>% 
              as.data.frame() %>% 
              tibble::rownames_to_column(var = "Cell") %>% 
              as.data.frame() %>% 
              tibble::as_tibble() %>% 
              tidyr::pivot_longer(cols = -dplyr::all_of("Cell"),
                                  names_to = key_col,
                                  values_to = "Score") %>% 
              dplyr::filter(.data[[key_col]] %in% vapply(dims, function(x){paste0(key, x)}, FUN.VALUE = character(1))) %>% 
              dplyr::group_by(.data[[key_col]]) %>% 
              dplyr::reframe("rank" = rank(.data$Score),
                             "Cell" = .data$Cell,
                             "Score" = .data$Score) %>% 
              dplyr::mutate("{key_col}" := factor(.data[[key_col]], levels = rev(vapply(dims, function(x){paste0(key, x)}, FUN.VALUE = character(1))))) %>% 
              dplyr::left_join(y = {sample@meta.data %>% 
                                    tibble::rownames_to_column(var = "Cell") %>% 
                                    tibble::as_tibble() %>% 
                                    dplyr::select(dplyr::all_of(c("Cell", group.by))) %>% 
                                    dplyr::left_join(y = {data[features, , drop = FALSE] %>% 
                                                          as.data.frame() %>% 
                                                          t() %>% 
                                                          as.data.frame() %>% 
                                                          tibble::rownames_to_column(var = "Cell") %>% 
                                                          tibble::as_tibble()},
                                                      by = "Cell")},
                                by = "Cell")
  })
  
  data.use <- as.data.frame(data.use)
  if (isTRUE(enforce_symmetry)){
    # Scale the enrichment scores as we are just interested in where they are enriched the most and not to compare across them.
    for (name in features){
      data.use[, name] <- scale(data.use[, name])[, 1]
    }
  }
  
  
  # Prepare the data to plot.
  data.use <- data.use %>% 
              tidyr::pivot_longer(cols = dplyr::all_of(c(features)),
                                  names_to = "Feature",
                                  values_to = "Expression")
            
  
  # Generate DC-based heatmaps.
  list.out <- list()
  
  for (dc.use in vapply(dims, function(x){paste0(key, x)}, FUN.VALUE = character(1))){
    # Filter for the DC.
    data.plot <- data.use %>% 
                 dplyr::filter(.data[[key_col]] == dc.use)
    
    # Limit the scale to quantiles 0.1 and 0.9 to avoid extreme outliers.
    limits <- c(stats::quantile(data.plot$Expression, 0.05, na.rm = TRUE),
                stats::quantile(data.plot$Expression, 0.95, na.rm = TRUE))
    
    # Bring extreme values to the cutoffs.
    data.plot <- data.plot %>% 
                 dplyr::mutate("Expression" = ifelse(.data$Expression <= limits[1], limits[1], .data$Expression)) %>% 
                 dplyr::mutate("Expression" = ifelse(.data$Expression >= limits[2], limits[2], .data$Expression))
    
    # Compute scale limits, breaks etc.
    scale.setup <- compute_scales(sample = NULL,
                                  feature = NULL,
                                  assay = NULL,
                                  reduction = NULL,
                                  slot = NULL,
                                  number.breaks = 5,
                                  min.cutoff = NA,
                                  max.cutoff = NA,
                                  flavor = "Seurat",
                                  enforce_symmetry = enforce_symmetry,
                                  from_data = TRUE,
                                  limits.use = limits)
    
    # Generate the plot.
    p <- data.plot %>% 
         ggplot2::ggplot(mapping = ggplot2::aes(x = .data$rank,
                                                y = .data$Feature,
                                                fill = .data$Expression))
    
    if (base::isTRUE(raster)){
      p <- p + 
           ggplot2::geom_raster(interpolate = interpolate)
    } else {
      p <- p + 
           ggplot2::geom_tile()
    }
    
    
    legend.name.use <- ifelse(isTRUE(enforce_symmetry), "Z-scored | Expression", "Expression")
    
    p <- p + 
      ggplot2::scale_fill_gradientn(colors = colors.gradient,
                                    na.value = na.value,
                                    name = legend.name.use,
                                    breaks = scale.setup$breaks,
                                    labels = scale.setup$labels,
                                    limits = scale.setup$limits) + 
      ggplot2::xlab(paste0("Ordering of cells along ", dc.use)) + 
      ggplot2::ylab("Features") +
      ggplot2::guides(y.sec = guide_axis_label_trans(~paste0(levels(.data$Features)))) 
    
    # Modify the appearance of the plot.
    p <- modify_continuous_legend(p = p,
                                  legend.title = legend.name.use,
                                  legend.aes = "fill",
                                  legend.type = legend.type,
                                  legend.position = legend.position,
                                  legend.length = legend.length,
                                  legend.width = legend.width,
                                  legend.framecolor = legend.framecolor,
                                  legend.tickcolor = legend.tickcolor,
                                  legend.framewidth = legend.framewidth,
                                  legend.tickwidth = legend.tickwidth)
    
    # Generate metadata plots to use on top of the main heatmap.
    list.plots <- list()
    list.plots[["main"]] <- p
    for (name in group.by){
      
      # Select color palette for metadata.
      if (name %in% names(colors.use)){
        colors.use.iteration <- colors.use[[name]]
      } else {
        names.use <- if(is.factor(sample@meta.data[, name])){levels(sample@meta.data[, name])} else {sort(unique(sample@meta.data[, name]))}
        colors.use.iteration <- generate_color_scale(names_use = names.use)
      }
      
      # Generate the metadata heatmap.
      p <- data.use %>% 
        dplyr::filter(.data[[key_col]] == dc.use) %>% 
        dplyr::mutate("grouped.var" = .env$name) %>% 
        ggplot2::ggplot(mapping = ggplot2::aes(x = .data$rank,
                                               y = .data$grouped.var,
                                               fill = .data[[name]]))
      
      if (base::isTRUE(raster)){
        p <- p + 
          ggplot2::geom_raster(interpolate = interpolate)
      } else {
        p <- p + 
          ggplot2::geom_tile()
      }
      p <- p + 
        ggplot2::scale_fill_manual(values = colors.use.iteration) + 
        ggplot2::guides(fill = ggplot2::guide_legend(title = name,
                                                     title.position = "top",
                                                     title.hjust = 0.5,
                                                     ncol = legend.ncol,
                                                     nrow = legend.nrow,
                                                     byrow = legend.byrow)) +
        ggplot2::xlab(NULL) +
        ggplot2::ylab(NULL) +
        ggplot2::guides(y.sec = guide_axis_label_trans(~paste0(levels(.data$grouped.var)))) 
      
      list.plots[[name]] <- p
    }
    
    # Add theme to all plots.
    for (name in names(list.plots)){
      
      list.plots[[name]] <- list.plots[[name]] +
                            ggplot2::scale_x_discrete(expand = c(0, 0)) +
                            ggplot2::scale_y_discrete(expand = c(0, 0)) +
                            ggplot2::theme_minimal(base_size = font.size) +
                            ggplot2::theme(axis.text.x = ggplot2::element_blank(),
                                           axis.text.y.right = ggplot2::element_text(face = axis.text.face,
                                                                                     color = "black"),
                                           axis.text.y.left = ggplot2::element_blank(),
                                           axis.ticks.y.right = ggplot2::element_line(color = "black"),
                                           axis.ticks.y.left = ggplot2::element_blank(),
                                           axis.ticks.x = ggplot2::element_blank(),
                                           axis.line = ggplot2::element_blank(),
                                           axis.title.y = ggplot2::element_text(face = axis.title.face, color = "black", angle = 90, hjust = 0.5, vjust = 0.5),
                                           axis.title.x = ggplot2::element_text(face = axis.title.face, color = "black", angle = 0),
                                           plot.title = ggplot2::element_text(face = plot.title.face, hjust = 0),
                                           plot.subtitle = ggplot2::element_text(face = plot.subtitle.face, hjust = 0),
                                           plot.caption = ggplot2::element_text(face = plot.caption.face, hjust = 1),
                                           plot.title.position = "plot",
                                           panel.grid = ggplot2::element_blank(),
                                           panel.grid.minor.y = ggplot2::element_line(color = "white"),
                                           text = ggplot2::element_text(family = font.type),
                                           plot.caption.position = "plot",
                                           legend.text = ggplot2::element_text(face = legend.text.face),
                                           legend.position = legend.position,
                                           legend.title = ggplot2::element_text(face = legend.title.face),
                                           legend.justification = "center",
                                           plot.margin = ggplot2::margin(t = ifelse(name == "main", 15, 10), r = 10, b = 0, l = 10),
                                           panel.border = ggplot2::element_rect(color = border.color, fill = NA),
                                           panel.grid.major = ggplot2::element_blank(),
                                           plot.background = ggplot2::element_rect(fill = "white", color = "white"),
                                           panel.background = ggplot2::element_rect(fill = "white", color = "white"),
                                           legend.background = ggplot2::element_rect(fill = "white", color = "white"))
    }
    
    # Reorder heatmaps for correct plotting.
    list.plots <- list.plots[c(group.by, "main")]
    height_unit <- c(rep((1 - main.heatmap.size) / length(group.by), length(group.by)), main.heatmap.size)
    
    
    # Assemble the final heatmap.
    p <- patchwork::wrap_plots(list.plots,
                               ncol = 1,
                               guides = "collect",
                               heights = height_unit) +
      patchwork::plot_annotation(theme = ggplot2::theme(legend.position = legend.position))
    
    list.out[[dc.use]] <- p
  }
  
  # Return the object.
  if (isTRUE(return_object)){
    list.out[["Object"]] <- sample
  }
  
  return(list.out)
}
