
#' Displays the annotation of the wells in a plateLayout
#' @param plateData a data.table object containing the information
#' of the plate. This must contain a "WellID".
#' @param plateName The plate name
#' @param valueVariable The name of the variable in 'plateData' to
#' be visualized in a plate layout.
#' @param textVariable The name of the variable in 'plateData' to be
#' shown in the wells of the plate layout. If NULL, the valueVariable
#' is shown.
#' @param colours A named character vector containing the colours
#' for the different levels of the valuevariable. The names should
#' correspond to the dose levels. if not specified, a scheme of blues
#' will be provided.
#' @param breaks Numeric vector indicating breaks for plot coloring.
#' @param colourWellText Colour to display the text in the wells.
#' @param layout Integer vector of length two with number of rows and
#' colums in a plate, e.g. \code{c(16,24)}
#' @param legend.title A title for the legend
#' @param plot.title A title for the plot, will be contracted
#' with the plate name
#' @param ... additional arguments for \code{plateLayout.default} function
#' @import data.table
#' @importFrom platetools fill_plate
#' @export
plateLayout.annotation <- function(
  plateData,
  plateName = character(),
  valueVariable = "Dose",
  textVariable = NULL,
  breaks = NULL, colours = NULL,
  colourWellText = "black",
  layout = c(16, 24),
  legend.title = "Dose",
  plot.title = "Plate Annotation - ",
  textFontSize = 9, ...
) {
  WellID <- Label <- NULL

  if (!(all(c("WellID", "SampleName") %in% colnames(plateData)))) {
    stop(" 'WellID' and 'SampleName' column required in plateData object")
  }
  
  #Check WellID Format
  checkWellID <- grepl("^[[:upper:]]{1,2}[[:digit:]]{1,2}$", plateData$WellID)
  if(!all(checkWellID)){
    stop("WellID does not have the correct format")
  }


  plateData[, WellID := paste0(
    sub(".*([[:alpha:]]).+", "\\1", plateData$WellID),
    sprintf(
      "%02d", as.numeric(sub(".*[[:alpha:]](.+)", "\\1", plateData$WellID))
    )
  )]

  plateData <- platetools::fill_plate(plateData, "WellID", plate = layout[1]*layout[2])

  plateData$column <- factor(
    sprintf(
      "%02d",
      as.numeric(sub(".*[[:alpha:]](.+)", "\\1", plateData$WellID))
    ),
    levels = sprintf("%02d", seq(1, layout[2]))
  )
  plateData$row <- factor(sub(".*([[:alpha:]]).+", "\\1", plateData$WellID),
                          levels = LETTERS[seq(1, layout[1])])

  if (!is.null(valueVariable)){
    plateData[, values := as.character(plateData[, ..valueVariable][[1]])]
    valueVar <- "values"
  }else{
    plateData[, values := "grey"]
    valueVar <- "values"
    colours <- setNames("grey", "grey")
  }


  if (is.null(colours)) {

    blues <- colorRampPalette(c("#d6e0ff", "#2171B5"))
    greens <- colorRampPalette(c("light green", "dark green"))

    numLevels <- sort(as.numeric(as.character(unique(plateData[, values])[
      grepl(
        "^[[:digit:]]+([.][[:digit:]]+)?$",
        trimws(unique(plateData[, values]))
      )
    ])))
    otherLevels <- sort(as.character(unique(plateData[, values])[
      !grepl(
        "^[[:digit:]]+([.][[:digit:]]+)?$",
        trimws(unique(plateData[,values]))
      )
    ]))

    colours <- c(blues(length(numLevels)), greens(length(otherLevels)), "red")
    names(colours) <- c(numLevels, otherLevels, "failed")
  }

  if (!is.null(textVariable)) {
    plateData[,
      Label :=  do.call(paste, c(.SD, sep = "\n ")),
      .SDcols = textVariable
    ]
    plateData[, Label :=  gsub("-", "-\n", Label)]
    plateData[, Label :=  gsub("_", "_\n", Label)]
    textVar <- "Label"
  } else {
    textVar <- NULL
  }


  if (is.null(breaks)){
    breaks <- seq_len(length(colours))
  }

  plateLayout(
    plateData = plateData, valueVariable = valueVar,
    textVariable = textVar, plateName = plateName,
    breaks = breaks, colourWellText = colourWellText,
    legend.title = legend.title, layout = layout,
    colours = colours, plot.title = plot.title,
    textFontSize = textFontSize, ...
  )
}



#' Create a heatmap of values in a plateLayout view. The values can be
#' library sizes, number of genes, qcScore (0/1) or a factor.
#' @param plateData A data.table of the values to be visualized with
#' at least the column of interest (specified in 'varOfInterest')
#' and a 'WellID' column indicating the wells in the plate. The WellID
#' is a combination of a letter (row in the plate) and an integer
#' (column in the plate).
#' @param valueVariable The name of the variable in 'plateData'
#' to be visualized in a plate layout
#' @param textVariable The name of the variable in 'plateData'
#' to be shown in the wells of the plate layout. Defaults to the
#' valueVariable and if NULL, no text will be displayed.
#' @param breaks Numeric vector indicating breaks for plot coloring.
#' @param colours Colours to be used for levels specified by
#' the breaks. If NULL, a colour scheme of purples is shown.
#' @param colourWellText Colour to display the text in the wells.
#' @param layout Integer vector of length two with number of rows
#' and colums in a plate, e.g. \code{c(16,24)}
#' @param makeContourColours Logical, whether or not the plate
#' layout will contain a contour colours for the wells based on the
#' parameters in 'contourColours' and 'categories'
#' @param contourVariable The variable used for the contour colouring
#' @param contourColours Character vector specifying a colour for
#' each range in 'categories'
#' @param labelsCategories Character vector specifying the names
#' (labels) for each range in 'categories'
#' @param categories if contour Variable is not a factor, a numeric
#' vector specifying the categories to divide the 'varOfInterest',
#' including the lower and upper limits.
#' @param plateName The plate name
#' @param plot.title A title for the plot, will be contracted with
#' the plate name
#' @param legend.title A title for the legend
#' @param displayHeatmap Logical, whether to display the plateLayout heatmap
#' @param saveHeatmap Logical, whether to save the plateLayout heatmap
#' @param outputDir The directory where the plateLayout heatmap should be saved
#' @param prefix The prefix to the file name of the saved plateLayout heatmap
#' @param ... additional arguments for \code{ComplexHeatmap::Heatmap} function
#' @importFrom platetools fill_plate
#' @importFrom RColorBrewer brewer.pal
#' @importFrom ComplexHeatmap Heatmap
#' @importFrom circlize colorRamp2
#' @importFrom grid grid.text grid.rect gpar legendGrob	gpar
#' @importFrom grDevices dev.off png
#' @importFrom graphics title
#' @export
plateLayout <- function(
  plateData, valueVariable, textVariable = valueVariable,
  breaks = NULL, colours = NULL, colourWellText = "white", textFontSize = 6,
  layout = c(16, 24), makeContourColours = FALSE, contourVariable = character(),
  contourColours = c("red", "orange", "seagreen3"),lwdContours = c(1, 1, 1),
  labelsCategories = c('1', '2', '3'), categories = NULL, plateName = character(),
  plot.title = character(), legend.title = NULL, legendFontSize = 15,
  row_split = rep("A", 16), col_split = rep("A", 24), legendFontSizeTitle = 15,
  displayHeatmap = TRUE, saveHeatmap = FALSE, outputDir = ".", prefix = ""
) {
  WellID <- NULL
  if (!(all(c("WellID", "SampleName") %in% colnames(plateData)))) {
    stop(" 'WellID' and 'SampleName' column required in plateData object")
  }


  plateData[, WellID := paste0(
    sub(".*([[:alpha:]]).+", "\\1", plateData$WellID),
    sprintf(
      "%02d",
      as.numeric(sub(".*[[:alpha:]](.+)", "\\1", plateData$WellID))
    )
  )]

  plateData <- platetools::fill_plate(plateData, "WellID", plate = 384)

  plateData$column <- factor(
    sprintf("%02d", as.numeric(
      sub(".*[[:alpha:]](.+)", "\\1", plateData$WellID)
    )),
    levels = sprintf("%02d", seq(1, layout[2]))
  )
  plateData$row <- factor(sub(".*([[:alpha:]]).+", "\\1", plateData$WellID),
                          levels = LETTERS[seq(1, layout[1])])


  plateValues <- plateLayoutFormat(
    plateData,
    varOfInterest = valueVariable,
    rows = layout[1],
    cols = layout[2]
  )
  if (!is.null(textVariable)) {
    plateText <- plateLayoutFormat(
      plateData, varOfInterest = textVariable,
      rows = layout[1],
      cols = layout[2]
    )
  }
  plot.title <- gsub(
    "^([a-z])", "\\U\\1",
    gsub("([A-Z])", " \\1",
    plot.title, perl = TRUE), perl = TRUE
  )
  mainTitle <- paste0(plot.title, plateName)
  plateContourColours <- matrix("", nrow = layout[1], ncol = layout[2])

  if (makeContourColours) {
    contourData <- plateData[WellType %in% c("nonEmpty", "Treated Wells"), ]

    if (is.numeric(contourData[, ..contourVariable][[1]])) {
      contourData$contours <- cut(
        contourData[, ..contourVariable][[1]],
        categories, left = TRUE,
        right = TRUE,
        labels = labelsCategories)
    }
    else {
      contourData$contours <- contourData[, ..contourVariable][[1]]
    }
    names(contourColours) <- labelsCategories
    names(lwdContours) <- labelsCategories
    for (i in seq_len(layout[1])) {
      for (j in seq_len(layout[2])) {
        tryCatch({
          sampleHit <- which(
            as.character(contourData$WellID) == paste0(
              LETTERS[i], sprintf("%02d", j)
            )
          )
          if (length(sampleHit) == 1) {
            plateContourColours[i, j] <- as.character(
              contourData[sampleHit,'contours'][[1]]
            )
          }
        },
        error = function(e) {
          print(paste0(LETTERS[i], sprintf("%02d", j), " is missing."))
        }
        )
      }
    }
  }

  plateValues$contours <- plateContourColours
  colnames(plateValues$values) <- seq_len(ncol(plateValues$values))

  if (is.null(breaks)) {
    breakValues <- plateValues$values
    breakValues[which(is.na(breakValues))] <- 0
    if (all(breakValues >= 0)) {
      breaks <- computeBreaks(7, max(plateValues$values, na.rm = TRUE))
    } else {
      breaks <- quantile(plateValues$values,  probs = seq(0, 1, 0.125))
    }
  }

  if (is.null(colours)) {
    colours <- tryCatch({
      circlize::colorRamp2(
        breaks = breaks,
        colors = brewer.pal(length(breaks), "Purples")
      )
    },
    error = function(cond){
      
      message("Recomputed breaks for proper colour mapping")
      
      breakValues <- plateValues$values
      breakValues[which(is.na(breakValues))] <- 0
      if (all(breakValues >= 0)) {
        breaks <- computeBreaks(7, max(plateValues$values, na.rm = TRUE))
      } else {
        breaks <- quantile(plateValues$values,  probs = seq(0, 1, 0.125))
      }
      
      circlize::colorRamp2(
        breaks = breaks,
        colors = brewer.pal(length(breaks), "Purples")
      )
      
    })
  }
  
  ht <- Heatmap(
    plateValues$values,
    column_title = mainTitle, column_title_side = "top",
    rect_gp = gpar(lwd = 0.4),
    cluster_rows = FALSE, cluster_columns = FALSE,
    col = colours, row_title = NULL,
    row_split = row_split, column_split = col_split,
    row_names_side = "left",
    cluster_row_slices = FALSE,
    cluster_column_slices = FALSE,
    show_heatmap_legend = TRUE,
    heatmap_legend_param = list(
      title = ifelse(
        is.null(legend.title),
        paste0(valueVariable, "\n"),
        paste0(legend.title, "\n")
      ),
      grid_height = unit(9, "mm"), border = "black",
      labels_gp = gpar(fontsize = legendFontSize),
      title_gp = gpar(fontsize = legendFontSizeTitle)
    ),
    cell_fun = function(j, i, x, y, width, height, fill) {
      if (is.na(plateValues$values[i, j])) {
        grid.rect(
          x, y, width, height,
          gp = gpar(fill = "white", alpha = 0.7, lwd = 0.7, col = "white")
        )
      }
      else if (!is.null(textVariable)) {
        grid.text(
          plateText$values[i, j], x, y,
          just = "centre",
          gp = gpar(fontsize = textFontSize, col = colourWellText)
        )
      }
      if (makeContourColours) {
        if (!is.na(plateValues$contours[i, j])) {
          grid.rect(
            x, y, width, height,
            gp = gpar(
              col = contourColours[as.character(plateValues$contours[i, j])],
              fill = NA,
              lwd = lwdContours[as.character(plateValues$contours[i, j])]
            )
          )
        }
      }
    }
  )

  if (displayHeatmap) {
    print(ht)
  }
  if (saveHeatmap) {
    png(
      file.path(
        outputDir,
        paste0(prefix,gsub(" |-", "",plot.title), "_", plateName, ".png")
      ),
      width = 30, height = 10, units = "cm", res = 1200
    )
    print(ht)
    dev.off()
  }

  return(ht)
}


#' Return numerical matrix with number of reads that corresponds to the
#' plate layout
#' @param data A data.frame of the values to be visualized with at least
#' the columnof interest (specified in 'varOfInterest') and a 'WellID' column
#' indicating the wells in the plate. The WellID is a combination of a
#' letter (row in the plate) and an integer (column in the plate).
#' @param varOfInterest The name of the variable in 'data' to be visualized
#' in a plate layout
#' @param rows number of rows in a plate layout
#' @param cols number of columns in a plate layout
#' @param verbose if \code{TRUE}, samples missing from the plate
#' will be reported
#' @export
plateLayoutFormat <- function(
  data, varOfInterest,
  rows = 16, cols = 24,
  verbose = FALSE
) {
  plateValues <- matrix(NA, nrow = rows, ncol = cols)
  for (i in seq_len(rows)) {
    for (j in seq_len(cols)) {
      tryCatch({
        sampleHit <- which(
          as.character(data$WellID) == paste0(LETTERS[i], sprintf("%02d", j))
        )
        if(length(sampleHit) == 1){
          plateValues[i, j] <- data[sampleHit, ..varOfInterest][[1]]
        }
      },
      error = function(e) {
        if (verbose == TRUE) {
          print(paste0(LETTERS[i], sprintf("%02d", j), " is missing."))
        }
      }
      )
    }
  }

  row.names(plateValues) <- LETTERS[1:rows]
  return(list("values" = plateValues))
}



#' Helper function to automate break selection for raw count data
#'
#' This function creates an exponentially increasing vector for given number
#' breaks between zero and some element of choice. It is particularly useful for
#' raw counts or raw counts per million.
#'
#' @param nBreaks Number of breaks to be generated
#' @param maxElement Maximum value of data entries
#' @export
computeBreaks <- function(nBreaks, variable) {

  maxElement <- max(variable, na.rm = TRUE)
  if (length(unique(variable)) == 1) {
    breaks <-  c(0, 0.5, ifelse(maxElement < 1, 1,  maxElement))
  } else {
    coefSystem <- solve(
      rbind(c(1, 1), c(1, (nBreaks - 1)))) %*% c(0, log(maxElement)
    )
    coefExp <- c(exp(coefSystem[1]), coefSystem[2])
    breaks <- coefExp[1] * exp((1:(nBreaks - 1)) * coefExp[2])
    breaks <- unique(c(0, breaks))
  }
  return(breaks)
}
