---
title: "Levey-Jennings Raport: `r paste0('*', params$report_title, '*')`"
output: html_document
params:
  list_of_plates: !r NULL
  report_title: !r NULL
  dilutions: !r NULL
  additional_notes: !r NULL
  extra_args: !r NULL
---

```{r param-check, echo=FALSE}
list_of_plates <- params$list_of_plates
additional_notes <- params$additional_notes
extra_args <- params$extra_args


if (is.null(list_of_plates)) {
  stop("The `list_of_plates` must be provided when rendering the document.")
}
if (!is.list(list_of_plates)) {
  stop("The `list_of_plates` must be a list of `Plate` instances.")
}
if (length(list_of_plates) == 0) {
  stop("The `list_of_plates` must contain at least one `Plate` instance.")
}
if (length(list_of_plates) < 10) {
  warning("The `list_of_plates` should contain at least 10 `Plate` instances to generate a comprehensive report.")
}
if (!all(sapply(list_of_plates, inherits, "Plate"))) {
  stop("The list_of_plates contains objects that are not of class Plate.")
}
if (is.null(params$report_title)) {
  stop("The `report_title` must be provided when rendering the document.")
}
if (!is(params$report_title, "character")) {
  stop("The `report_title` must be a character value.")
}
if (is.null(params$dilutions)) {
  stop("The `dilutions` must be provided when rendering the document.")
}
if (!is.character(params$dilutions)) {
  stop("The `dilutions` must be a character value.")
}
if (!all(grepl("(^1\\s*)\\/(\\s*\\d+$)", params$dilutions))) {
  stop("The `dilutions` must be in the format '1/digit'.")
}
for (dilution in params$dilutions) {
  if (!all(sapply(list_of_plates, function(plate) dilution %in% plate$get_dilution("STANDARD CURVE")))) {
    plate_where_dilution_is_missing <- which(sapply(list_of_plates, function(plate) !(dilution %in% plate$get_dilution("STANDARD CURVE"))))
    stop(
      "The dilution",
      dilution,
      "is not present in plates ",
      paste(plate_where_dilution_is_missing, collapse = ", ")
    )
  }
}
all_analyte_names <- c()

intersection_analytes <- list_of_plates[[1]]$analyte_names

for (plate in list_of_plates) {
  all_analyte_names <- union(all_analyte_names, plate$analyte_names)
  intersection_analytes <- intersect(intersection_analytes, plate$analyte_names)
}

if (length(intersection_analytes) != length(all_analyte_names)) {
  warning(
    "Not all plates contain the same analytes. The report will contain only the analytes that are present in all plates. Number of all analytes: ",
    length(all_analyte_names),
    ". Number of common analytes: ",
    length(intersection_analytes)
  )
}

analyte_names <- intersection_analytes


if (!is.null(additional_notes) && !is(additional_notes, "character")) {
  stop("The `additional_notes` must be a character value.")
}

date_of_first_plate <- min(sapply(list_of_plates, function(plate) plate$plate_datetime))
date_of_last_plate <- max(sapply(list_of_plates, function(plate) plate$plate_datetime))

# i need to add this trick otherwise i get error in format function
# Error in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L,  :
#  invalid 'trim' argument
# it may cause some problems with timezones
# but few hours difference is not a big deal
date_of_first_plate <- as.POSIXct(date_of_first_plate,
  origin = "1970-01-01",
  tz = "UTC"
)
date_of_last_plate <- as.POSIXct(date_of_last_plate,
  origin = "1970-01-01",
  tz = "UTC"
)
time_span <- difftime(date_of_last_plate, date_of_first_plate, units = "days")


# perform additional check for extra_args

# Validate extra_args: ensure it's a named list
if (!is.list(extra_args)) {
  stop("`extra_args` must be a list.")
}
if (length(extra_args) > 0 && (is.null(names(extra_args)) || any(names(extra_args) == ""))) {
  stop("All elements in `extra_args` must be named.")
}

# Hard-coded list of arguments passed explicitly in this Rmd script
reserved_names <- c(
  "list_of_plates", "analyte_name", "monochromatic"
)

# Check for forbidden overlaps
conflicting_args <- intersect(names(extra_args), reserved_names)
if (length(conflicting_args) > 0) {
  stop(
    "The following `extra_args` fields are not allowed, as they are explicitly passed in the report script: ",
    paste(conflicting_args, collapse = ", "),
    ". Please rename or remove them."
  )
}
```

Report generated on:  `r format(Sys.time(), SerolyzeR.env$report_datetime_format)`.  
First plate was run on: `r if(!is.null(date_of_first_plate)) format(date_of_first_plate, SerolyzeR.env$report_datetime_format) else "Date of running plate was not found in Luminex file"`.  
Last plate was run on: `r if(!is.null(date_of_last_plate)) format(date_of_last_plate, SerolyzeR.env$report_datetime_format) else "Date of running plate was not found in Luminex file"`.  
Time span: `r if(!is.null(time_span)) paste0(round(time_span, 2), " days") else "Dates were not found in Luminex file"`.  
Number of plates used in report: `r length(list_of_plates)`.  
Plates have `r paste0(length(analyte_names), " analytes")`.  
Standard curve sample dilutions: `r format_dilutions(list_of_plates[[1]]$dilutions, list_of_plates[[1]]$dilution_values, list_of_plates[[1]]$sample_types)`.

------------------

`r if(!is.null(additional_notes)) "### Additional notes \n" `  

`r if(!is.null(additional_notes)) additional_notes `  

`r if(!is.null(additional_notes)) "\n -------------------" ` 

### Plates used in this report

<details open>
  <summary>Click to show/hide list</summary>
```{r list-of-plates, echo=FALSE, results='asis'}
for (plate in list_of_plates) {
  cat(paste0("- ", plate$plate_name, "\n"))
}
```
</details>

### Plate layout

<details open>
  <summary>Click to show/hide layout</summary>
```{r plate-layout, fig.width=7, fig.height=4, fig.align='center', dev='jpeg', echo=FALSE, dpi=72}
plot_layout(list_of_plates[[1]])
```
</details>

------------------

```{r setup_parallel_execution, include=FALSE, echo=FALSE, message=FALSE, warning=FALSE}
library(furrr)
library(rlang)
library(svglite)
library(ggplot2)
library(qs2) # Fast serialization


# 2. PREPARE DIRECTORIES
plot_dir <- file.path(tempdir(), "report_svgs")
if (!dir.exists(plot_dir)) dir.create(plot_dir, recursive = TRUE)

# 3. SAVE DATA TO DISK (Windows workaround)
plates_data_path <- file.path(tempdir(), "list_of_plates_shared.qs2")
qs2::qs_save(list_of_plates, plates_data_path)

# Helper function
save_plot_to_svg <- function(plot, full_path, width, height) {
  svglite::svglite(full_path, width = width, height = height)
  print(plot)
  grDevices::dev.off()
  return(full_path)
}

# WRAPPER FUNCTION FOR PARALLEL EXECUTION
process_analyte_plots <- function(analyte, data_path, dilutions, extra_args, plot_dir) {
  # A. Read data inside the worker (Fast)
  list_of_plates <- qs2::qs_read(data_path)

  # B. Re-load essential libraries inside the worker
  library(ggplot2)
  library(rlang)

  n <- length(list_of_plates)
  safe_name <- gsub("[^a-zA-Z0-9]", "_", analyte)

  # Monochromatic
  l_h_mono <- 0.35 * (1 + (n %/% 3))
  h_mono <- 3.5 + l_h_mono
  p_mono <- inject(plot_standard_curve_stacked(
    list_of_plates = list_of_plates,
    analyte_name = analyte,
    monochromatic = TRUE,
    separate_legend = TRUE,
    !!!extra_args
  ))
  path_mono <- file.path(plot_dir, paste0(safe_name, "_mono.svg"))
  save_plot_to_svg(p_mono, path_mono, 5, h_mono)

  # Colored
  l_h_color <- 0.35 * (1 + (n %/% 2))
  h_color <- 3.5 + l_h_color
  p_color <- inject(plot_standard_curve_stacked(
    list_of_plates = list_of_plates,
    analyte_name = analyte,
    monochromatic = FALSE,
    separate_legend = TRUE,
    max_legend_items_per_row = 2,
    !!!extra_args
  ))
  path_color <- file.path(plot_dir, paste0(safe_name, "_color.svg"))
  save_plot_to_svg(p_color, path_color, 5, h_color)

  # Levey-Jennings for each dilution
  lj_paths <- list()
  for (dilution in dilutions) {
    p_lj <- inject(plot_levey_jennings(
      list_of_plates = list_of_plates,
      analyte_name = analyte,
      dilution = dilution,
      !!!extra_args
    ))
    safe_dil <- gsub("[^a-zA-Z0-9]", "_", as.character(dilution))
    f_path <- file.path(plot_dir, paste0(safe_name, "_lj_", safe_dil, ".svg"))
    lj_paths[[as.character(dilution)]] <- save_plot_to_svg(p_lj, f_path, 7, 5)
  }

  return(list(
    name = analyte,
    path_mono = path_mono,
    path_color = path_color,
    lj_paths = lj_paths
  ))
}
```

```{r generate_plots, echo=FALSE, message=FALSE, warning=FALSE}
if (length(analyte_names) == 0) {
  stop("No common analytes found across plates. Cannot generate plots.")
}
if (length(analyte_names) < 4) {
  # EXECUTE SEQUENTIAL MAP
  plot_results <- list()
  for (analyte in analyte_names) {
    plot_results[[analyte]] <- process_analyte_plots(
      analyte, plates_data_path, params$dilutions, extra_args, plot_dir
    )
  }
} else {
  # SETUP PLAN
  num_workers <- min(4, future::availableCores() - 1) # Leave one core free
  future::plan(future::multisession, workers = num_workers)

  # EXECUTE PARALLEL MAP
  plot_results <- furrr::future_map(
    analyte_names,
    ~ process_analyte_plots(
      .x, plates_data_path, params$dilutions, extra_args, plot_dir
    ),
    .options = furrr::furrr_options(
      seed = TRUE,
      # Explicitly load packages and global variables in each worker
      packages = c("ggplot2", "svglite", "rlang", "qs2"),
      globals = c(
        "plot_standard_curve_stacked", "plot_levey_jennings", "save_plot_to_svg"
      )
    )
  )
}
```

 
### Details for given analyte {.tabset .tabset-fade}

```{r quality-control, results='asis', echo=FALSE, message=FALSE, dpi=72, warning=FALSE}
# SYNCHRONOUS RENDERING
for (res in plot_results) {
  cat(paste0("#### ", res$name, "\n"))
  # Print Monochromatic
  cat(sprintf("![](%s){width='50%%'}", res$path_mono))
  # Print Colored
  cat(sprintf("![](%s){width='50%%'}", res$path_color))
  # Print Levey-Jennings for each dilution
  for (lj_path in res$lj_paths) {
    cat(sprintf("![](%s){width='50%%'}", lj_path))
  }
  cat("\n\n")
}
```

------------------

### &nbsp;

<hr />
<p style="text-align: center;">Generated using the <a href="https://github.com/mini-pw/SerolyzeR">SerolyzeR </a> package </p>


