###############################
# class for single heatmap
#


# the layout of the heatmap is 7 x 9

# == title
# Class for a single heatmap
#
# == details
# The `Heatmap-class` is not responsible for heatmap legend and annotation legends. The `draw,Heatmap-method` method
# will construct a `HeatmapList-class` object which only contains one single heatmap
# and call `draw,HeatmapList-method` to make a complete heatmap.
#
# == methods
# The `Heatmap-class` provides following methods:
#
# - `Heatmap`: constructor method.
# - `draw,Heatmap-method`: draw a single heatmap.
# - `add_heatmap,Heatmap-method` append heatmaps and row annotations to a list of heatmaps.
# - `row_order,HeatmapList-method`: get order of rows
# - `column_order,HeatmapList-method`: get order of columns
# - `row_dend,HeatmapList-method`: get row dendrograms
# - `column_dend,HeatmapList-method`: get column dendrograms
#
# == author
# Zuguang Gu <z.gu@dkfz.de>
#
Heatmap = setClass("Heatmap",
    slots = list(
        name = "character",

        matrix = "matrix",  # one or more matrix which are spliced by rows
        matrix_param = "list",
        matrix_color_mapping = "ANY",
        matrix_legend_param = "ANY",

        row_title = "ANY",
        row_title_param = "list",
        column_title = "ANY",
        column_title_param = "list",

        row_dend_list = "list", # one or more row clusters
        row_dend_slice = "ANY",
        row_dend_param = "list", # parameters for row cluster
        row_order_list = "list",
        row_order = "numeric",

        column_dend_list = "list",
        column_dend_slice = "ANY",
        column_dend_param = "list", # parameters for column cluster
        column_order_list = "list",
        column_order = "numeric",

        row_names_param = "list",
        column_names_param = "list",

        top_annotation = "ANY", # NULL or a `HeatmapAnnotation` object
        top_annotation_param = "list",
        bottom_annotation = "ANY",
        bottom_annotation_param = "list",
        left_annotation = "ANY", # NULL or a `HeatmapAnnotation` object
        left_annotation_param = "list",
        right_annotation = "ANY",
        right_annotation_param = "list",

        heatmap_param = "list",

        layout = "list"
    ),
    contains = "AdditiveUnit"
)



# == title
# Constructor method for Heatmap class
#
# == param
# -matrix a matrix. Either numeric or character. If it is a simple vector, it will be
#         converted to a one-column matrix.
# -col a vector of colors if the color mapping is discrete or a color mapping 
#      function if the matrix is continuous numbers (should be generated by `circlize::colorRamp2`. If the matrix is continuous,
#      the value can also be a vector of colors so that colors will be interpolated. Pass to `ColorMapping`.
# -name name of the heatmap. The name is used as the title of the heatmap legend.
# -na_col color for ``NA`` values.
# -rect_gp graphic parameters for drawing rectangles (for heatmap body).
# -color_space the color space in which colors are interpolated. Only used if ``matrix`` is numeric and 
#            ``col`` is a vector of colors. Pass to `circlize::colorRamp2`.
# -border whether draw border or the color of border.
# -cell_fun self-defined function to add graphics on each cell. Seven parameters will be passed into 
#           this function: ``i``, ``j``, ``x``, ``y``, ``width``, ``height``, ``fill`` which are row index,
#           column index in ``matrix``, coordinate of the middle points in the heatmap body viewport,
#           the width and height of the cell and the filled color. ``x``, ``y``, ``width`` and ``height`` are all `grid::unit` objects.
# -layer_fun similar as ``cell_fun``, but is vectorized. 
# -row_title title on row.
# -row_title_side will the title be put on the left or right of the heatmap?
# -row_title_gp graphic parameters for drawing text.
# -row_title_rot rotation of row titles. Only 0, 90, 270 are allowed to set.
# -column_title title on column.
# -column_title_side will the title be put on the top or bottom of the heatmap?
# -column_title_gp graphic parameters for drawing text.
# -column_title_rot rotation of column titles. Only 0, 90, 270 are allowed to set.
# -cluster_rows If the value is a logical, it means whether make cluster on rows. The value can also
#               be a `stats::hclust` or a `stats::dendrogram` that already contains clustering information.
#               This means you can use any type of clustering methods and render the `stats::dendrogram`
#               object with self-defined graphic settings.
# -clustering_distance_rows it can be a pre-defined character which is in 
#                ("euclidean", "maximum", "manhattan", "canberra", "binary", 
#                "minkowski", "pearson", "spearman", "kendall"). It can also be a function.
#                If the function has one argument, the input argument should be a matrix and 
#                the returned value should be a `stats::dist` object. If the function has two arguments,
#                the input arguments are two vectors and the function calculates distance between these
#                two vectors.
# -clustering_method_rows method to make cluster, pass to `stats::hclust`.
# -row_dend_side should the row cluster be put on the left or right of the heatmap?
# -row_dend_width width of the row cluster, should be a `grid::unit` object.
# -show_row_dend whether show row clusters. 
# -row_dend_gp graphics parameters for drawing lines. If users already provide a `stats::dendrogram`
#                object with edges rendered, this argument will be ignored.
# -row_dend_reorder apply reordering on rows. The value can be a logical value or a vector which contains weight 
#               which is used to reorder rows
# -cluster_columns whether make cluster on columns. Same settings as ``cluster_rows``.
# -clustering_distance_columns same setting as ``clustering_distance_rows``.
# -clustering_method_columns method to make cluster, pass to `stats::hclust`.
# -column_dend_side should the column cluster be put on the top or bottom of the heatmap?
# -column_dend_height height of the column cluster, should be a `grid::unit` object.
# -show_column_dend whether show column clusters.
# -column_dend_gp graphic parameters for drawling lines. Same settings as ``row_dend_gp``.
# -column_dend_reorder apply reordering on columns. The value can be a logical value or a vector which contains weight 
#               which is used to reorder columns
# -row_order order of rows. It makes it easy to adjust row order for a list of heatmaps if this heatmap 
#      is selected as the main heatmap. Manually setting row order should turn off clustering
# -column_order order of column. It makes it easy to adjust column order for both matrix and column annotations.
# -row_labels row labels
# -row_names_side should the row names be put on the left or right of the heatmap?
# -show_row_names whether show row names.
# -row_names_max_width maximum width of row names viewport. Because some times row names can be very long, it is not reasonable
#                      to show them all.
# -row_names_gp graphic parameters for drawing text.
# -row_names_rot rotation of row labels
# -column_labels column labels
# -column_names_side should the column names be put on the top or bottom of the heatmap?
# -column_names_max_height maximum height of column names viewport.
# -show_column_names whether show column names.
# -column_names_gp graphic parameters for drawing text.
# -column_names_rot rotation of column labels
# -top_annotation a `HeatmapAnnotation` object which contains a list of annotations.
# -bottom_annotation a `HeatmapAnnotation` object.
# -left_annotation should specified in `rowAnnotation`
# -right_annotation should shpecified in `rowAnnotation`
# -km do k-means clustering on rows. If the value is larger than 1, the heatmap will be split by rows according to the k-means clustering.
#     For each row-clusters, hierarchical clustering is still applied with parameters above.
# -split a vector or a data frame by which the rows are split. But if ``cluster_rows`` is a clustering object, ``split`` can be a single number
#        indicating rows are to be split according to the split on the tree.
# -row_km row km
# -row_split row split
# -column_km column km
# -column_split column split
# -gap gap between row-slices if the heatmap is split by rows, should be `grid::unit` object. If it is a vector, the order corresponds
#   to top to bottom in the heatmap
# -row_gap row gap
# -column_gap column gap
# -width width of the heatmap body
# -height height of the heatmap body
# -heatmap_width width of the whole heatmap (including heatmap components)
# -heatmap_height height of the whole heatmap (including heatmap components)
# -show_heatmap_legend whether show heatmap legend?
# -heatmap_legend_param a list contains parameters for the heatmap legend. See `color_mapping_legend,ColorMapping-method` for all available parameters.
# -use_raster whether render the heatmap body as a raster image. It helps to reduce file size when the matrix is huge. Note if ``cell_fun``
#       is set, ``use_raster`` is enforced to be ``FALSE``.
# -raster_device graphic device which is used to generate the raster image
# -raster_quality a value set to larger than 1 will improve the quality of the raster image.
# -raster_device_param a list of further parameters for the selected graphic device
# -post_fun a function which will be executed after the plot is drawn.
#
# == details
# The initialization function only applies parameter checking and fill values to the slots with proper values.
# 
# Following methods can be applied on the `Heatmap-class` object:
#
# - `show,Heatmap-method`: draw a single heatmap with default parameters
# - `draw,Heatmap-method`: draw a single heatmap.
# - ``+`` or `\%v\%` append heatmaps and row annotations to a list of heatmaps.
#
# The constructor function pretends to be a high-level graphic function because the ``show`` method
# of the `Heatmap-class` object actually plots the graphics.
#
# == value
# A `Heatmap-class` object.
#
# == author
# Zuguang Gu <z.gu@dkfz.de>
#
Heatmap = function(matrix, col, name, 
    na_col = "grey", 
    color_space = "LAB",
    rect_gp = gpar(col = NA), 
    border = NA,
    cell_fun = NULL,
    layer_fun = NULL,

    row_title = character(0), 
    row_title_side = c("left", "right"), 
    row_title_gp = gpar(fontsize = 14), 
    row_title_rot = switch(row_title_side[1], "left" = 90, "right" = 270),
    column_title = character(0), 
    column_title_side = c("top", "bottom"), 
    column_title_gp = gpar(fontsize = 14), 
    column_title_rot = 0,

    cluster_rows = TRUE, 
    clustering_distance_rows = "euclidean",
    clustering_method_rows = "complete", 
    row_dend_side = c("left", "right"),
    row_dend_width = unit(10, "mm"), 
    show_row_dend = TRUE, 
    row_dend_reorder = TRUE,
    row_dend_gp = gpar(), 
    cluster_columns = TRUE, 
    clustering_distance_columns = "euclidean", 
    clustering_method_columns = "complete",
    column_dend_side = c("top", "bottom"), 
    column_dend_height = unit(10, "mm"), 
    show_column_dend = TRUE, 
    column_dend_gp = gpar(), 
    column_dend_reorder = TRUE,

    row_order = NULL, 
    column_order = NULL,

    row_labels = rownames(matrix),
    row_names_side = c("right", "left"), 
    show_row_names = TRUE, 
    row_names_max_width = unit(6, "cm"), 
    row_names_gp = gpar(fontsize = 12), 
    row_names_rot = 0,
    column_labels = colnames(matrix),
    column_names_side = c("bottom", "top"), 
    show_column_names = TRUE, 
    column_names_max_height = unit(6, "cm"), 
    column_names_gp = gpar(fontsize = 12),
    column_names_rot = 90,

    top_annotation = NULL,
    bottom_annotation = NULL,
    left_annotation = NULL,
    right_annotation = NULL,

    km = 1, 
    split = NULL, 
    row_km = km,
    row_split = split,
    column_km = 1,
    column_split = NULL,
    gap = unit(1, "mm"),
    row_gap = unit(1, "mm"),
    column_gap = unit(1, "mm"),

    heatmap_width = unit(1, "npc"),
    width = NULL,
    heatmap_height = unit(1, "npc"), 
    height = NULL,

    show_heatmap_legend = TRUE,
    heatmap_legend_param = list(title = name),

    use_raster = (nrow(matrix) > 2000 && ncol(matrix) > 1) || (ncol(matrix) > 2000 && nrow(matrix) > 1), 
    raster_device = c("png", "jpeg", "tiff", "CairoPNG", "CairoJPEG", "CairoTIFF"),
    raster_quality = 2,
    raster_device_param = list(),

    post_fun = NULL) {

    verbose = ht_opt("verbose")

    .Object = new("Heatmap")
    if(missing(name)) {
        name = paste0("matrix_", get_heatmap_index() + 1)
        increase_heatmap_index()
    }
    .Object@name = name

    # re-define some of the argument values according to global settings
    called_args = names(as.list(match.call())[-1])
    for(opt_name in c("row_names_gp", "column_names_gp", "row_title_gp", "column_title_gp")) {
        opt_name2 = paste0("heatmap_", opt_name)
        if(! opt_name %in% called_args) { # if this argument is not called
            if(!is.null(ht_opt(opt_name2))) {
                if(verbose) qqcat("re-assign @{opt_name} with `ht_opt('@{opt_name2}'')`\n")
                assign(opt_name, ht_opt(opt_name2))
            }
        }
    }

    if("heatmap_legend_param" %in% called_args) {
        for(opt_name in setdiff(c("title_gp", "title_position", "labels_gp", "grid_width", "grid_height", "border"), names(heatmap_legend_param))) {
            opt_name2 = paste0("legend_", opt_name)
            if(!is.null(ht_opt(opt_name2)))
                if(verbose) qqcat("re-assign heatmap_legend_param$@{opt_name} with `ht_opt('@{opt_name2}'')`\n")
                heatmap_legend_param[[opt_name]] = ht_opt(opt_name2)
        }
    } else {
        for(opt_name in c("title_gp", "title_position", "labels_gp", "grid_width", "grid_height", "border")) {
            opt_name2 = paste0("legend_", opt_name)
            if(!is.null(ht_opt(opt_name2)))
                if(verbose) qqcat("re-assign heatmap_legend_param$@{opt_name} with `ht_opt('@{opt_name2}'')`\n")
                heatmap_legend_param[[opt_name]] = ht_opt(opt_name2)
        }
    }

    if(is.data.frame(matrix)) {
        if(verbose) qqcat("convert data frame to matrix\n")
        warning_wrap("The input is a data frame, convert it to the matrix.")
        matrix = as.matrix(matrix)
    }
    if(!is.matrix(matrix)) {
        if(is.atomic(matrix)) {
            rn = names(matrix)
            matrix = matrix(matrix, ncol = 1)
            if(!is.null(rn)) rownames(matrix) = rn
            if(!missing(name)) colnames(matrix) = name
            if(verbose) qqcat("convert simple vector to one-column matrix\n")
        } else {
            stop_wrap("If input is not a matrix, it should be a simple vector.")
        }
    }

    if(ncol(matrix) == 0) {
        .Object@heatmap_param$show_heatmap_legend = FALSE
    }

    ### normalize km/split and row_km/row_split
    if(missing(row_km)) row_km = km
    if(missing(row_split)) row_split = split
    if(missing(row_gap)) row_gap = gap

    ####### zero and one column matrix ########
    if(ncol(matrix) == 0 || nrow(matrix) == 0) {
        if(!inherits(cluster_columns, c("dendrogram", "hclust"))) {
            cluster_columns = FALSE
            show_column_dend = FALSE
        }
        if(!inherits(cluster_rows, c("dendrogram", "hclust"))) {
            cluster_rows = FALSE
            show_row_dend = FALSE
        }
        row_km = 1
        column_km = 1
        if(verbose) qqcat("zero row/column matrix, set cluster_columns/rows to FALSE\n")
    }
    if(ncol(matrix) == 1) {
        if(!inherits(cluster_columns, c("dendrogram", "hclust"))) {
            cluster_columns = FALSE
            show_column_dend = FALSE
        }
        column_km = 1
        if(verbose) qqcat("one-column matrix, set cluster_columns to FALSE\n")
    }
    if(nrow(matrix) == 1) {
        if(!inherits(cluster_rows, c("dendrogram", "hclust"))) {
            cluster_rows = FALSE
            show_row_dend = FALSE
        }
        row_km = 1
        if(verbose) qqcat("one-row matrix, set cluster_rows to FALSE\n")
    }

    if(is.character(matrix)) {
        called_args = names(match.call()[-1])
        if("clustering_distance_rows" %in% called_args) {
        } else if(inherits(cluster_rows, c("dendrogram", "hclust"))) {
        } else {
            cluster_rows = FALSE
            show_row_dend = FALSE
        }
        row_dend_reorder = FALSE

        if("clustering_distance_columns" %in% called_args) {
        } else if(inherits(cluster_columns, c("dendrogram", "hclust"))) {
        } else {
            cluster_columns = FALSE
            show_column_dend = FALSE
        }
        column_dend_reorder = FALSE
        row_km = 1
        column_km = 1
        if(verbose) qqcat("matrix is character. Do not cluster unless distance method is provided.\n")
    }
    .Object@matrix = matrix

    .Object@matrix_param$row_km = row_km
    .Object@matrix_param$row_gap = row_gap
    .Object@matrix_param$column_km = column_km
    .Object@matrix_param$column_gap = column_gap

    ### check row_split and column_split ###
    if(!is.null(row_split)) {
        if(inherits(cluster_rows, c("dendrogram", "hclust"))) {
            if(is.numeric(row_split) && length(row_split) == 1) {
                .Object@matrix_param$row_split = row_split
            } else {
                stop_wrap("When `cluster_rows` is a dendrogram, `row_split` can only be a single number.")
            }
        } else {
            if(identical(cluster_rows, TRUE) && is.numeric(row_split) && length(row_split) == 1) {

            } else {
                if(!is.data.frame(row_split)) row_split = data.frame(row_split)
                if(nrow(row_split) != nrow(matrix)) {
                    stop_wrap("Length or nrow of `row_split` should be same as nrow of `matrix`.")
                }
            }
        }
    }
    .Object@matrix_param$row_split = row_split

    if(!is.null(column_split)) {
        if(inherits(cluster_columns, c("dendrogram", "hclust"))) {
            if(is.numeric(column_split) && length(column_split) == 1) {
                .Object@matrix_param$column_split = column_split
            } else {
               stop_wrap("When `cluster_columns` is a dendrogram, `column_split` can only be a single number.")
            }
        } else {
            if(identical(cluster_columns, TRUE) && is.numeric(column_split) && length(column_split) == 1) {

            } else {
                if(!is.data.frame(column_split)) column_split = data.frame(column_split)
                if(nrow(column_split) != ncol(matrix)) {
                    stop("Length or ncol of `column_split` should be same as ncol of `matrix`.")
                }
            }
        }
    }
    .Object@matrix_param$column_split = column_split


    ### parameters for heatmap body ###
    .Object@matrix_param$gp = check_gp(rect_gp)
    if(missing(border)) {
        if(!is.null(ht_opt$heatmap_border)) border = ht_opt$heatmap_border
    }
    if(identical(border, TRUE)) border = "black"
    .Object@matrix_param$border = border
    .Object@matrix_param$cell_fun = cell_fun
    .Object@matrix_param$layer_fun = layer_fun
    
    ### color for main matrix #########
    if(ncol(matrix) > 0 && nrow(matrix) > 0) {
        if(missing(col)) {
            col = default_col(matrix, main_matrix = TRUE)
            if(verbose) qqcat("color is not specified, use randomly generated colors\n")
        }
        if(is.function(col)) {
            .Object@matrix_color_mapping = ColorMapping(col_fun = col, name = name, na_col = na_col)
            if(verbose) qqcat("input color is a color mapping function\n")
        } else {
            if(is.null(names(col))) {
                if(length(col) == length(unique(as.vector(matrix)))) {
                    names(col) = sort(unique(as.vector(matrix)))
                    .Object@matrix_color_mapping = ColorMapping(colors = col, name = name, na_col = na_col)
                    if(verbose) qqcat("input color is a vector with no names, treat it as discrete color mapping\n")
                } else if(is.numeric(matrix)) {
                    col = colorRamp2(seq(min(matrix, na.rm = TRUE), max(matrix, na.rm = TRUE), length = length(col)),
                                     col, space = color_space)
                    .Object@matrix_color_mapping = ColorMapping(col_fun = col, name = name, na_col = na_col)
                    if(verbose) qqcat("input color is a vector with no names, treat it as continuous color mapping\n")
                } else {
                    stop_wrap("`col` should have names to map to values in `mat`.")
                }
            } else {
                col = col[intersect(c(names(col), "_NA_"), as.character(matrix))]
                .Object@matrix_color_mapping = ColorMapping(colors = col, name = name, na_col = na_col)
                if(verbose) qqcat("input color is a named vector\n")
            }
        }
        .Object@matrix_legend_param = heatmap_legend_param
    }
    
    ##### titles, should also consider titles after row splitting #####
    if(identical(row_title, NA) || identical(row_title, "")) {
        row_title = character(0)
    }
    .Object@row_title = row_title
    .Object@row_title_param$rot = row_title_rot %% 360
    .Object@row_title_param$side = match.arg(row_title_side)[1]
    .Object@row_title_param$gp = check_gp(row_title_gp)  # if the number of settings is same as number of row-splits, gp will be adjusted by `make_row_dend`
    .Object@row_title_param$just = get_text_just(rot = row_title_rot, side = .Object@row_title_param$side)

    if(identical(column_title, NA) || identical(column_title, "")) {
        column_title = character(0)
    }
    .Object@column_title = column_title
    .Object@column_title_param$rot = column_title_rot %% 360
    .Object@column_title_param$side = match.arg(column_title_side)[1]
    .Object@column_title_param$gp = check_gp(column_title_gp)
    .Object@column_title_param$just = get_text_just(rot = column_title_rot, side = .Object@column_title_param$side)

    ### row labels/column labels ###
    if(is.null(rownames(matrix))) {
        show_row_names = FALSE
    }
    .Object@row_names_param$labels = row_labels
    .Object@row_names_param$side = match.arg(row_names_side)[1]
    .Object@row_names_param$show = show_row_names
    .Object@row_names_param$gp = check_gp(row_names_gp)
    .Object@row_names_param$rot = row_names_rot
    .Object@row_names_param$max_width = row_names_max_width + unit(2, "mm")
    # we use anno_text to draw row/column names because it already takes care of text rotation
    if(length(row_labels)) {
        if(length(row_labels) != nrow(matrix)) {
            stop_wrap("Length of `row_labels` should be the same as the nrow of matrix.")
        }
        row_names_anno = anno_text(row_labels, which = "row", gp = row_names_gp, rot = row_names_rot,
            location = ifelse(.Object@row_names_param$side == "left", 1, 0), 
            just = ifelse(.Object@row_names_param$side == "left", "right", "left"))
        .Object@row_names_param$anno = row_names_anno
    }

    if(is.null(colnames(matrix))) {
        show_column_names = FALSE
    }
    .Object@column_names_param$labels = column_labels
    .Object@column_names_param$side = match.arg(column_names_side)[1]
    .Object@column_names_param$show = show_column_names
    .Object@column_names_param$gp = check_gp(column_names_gp)
    .Object@column_names_param$rot = column_names_rot
    .Object@column_names_param$max_height = column_names_max_height + unit(2, "mm")
    if(length(column_labels)) {
        if(length(column_labels) != ncol(matrix)) {
            stop_wrap("Length of `column_labels` should be the same as the ncol of matrix.")
        }
        column_names_anno = anno_text(column_labels, which = "column", gp = column_names_gp, rot = column_names_rot,
            location = ifelse(.Object@column_names_param$side == "top", 0, 1), 
            just = ifelse(.Object@column_names_param$side == "top", 
                     ifelse(.Object@column_names_param$rot >= 0, "left", "right"),
                     ifelse(.Object@column_names_param$rot >= 0, "right", "left")
                    ))
        .Object@column_names_param$anno = column_names_anno
    }

    #### dendrograms ########
    if(missing(cluster_rows) && !missing(row_order)) {
        cluster_rows = FALSE
    }
    if(inherits(cluster_rows, "dendrogram") || inherits(cluster_rows, "hclust")) {
        .Object@row_dend_param$obj = cluster_rows
        .Object@row_dend_param$cluster = TRUE
    } else if(inherits(cluster_rows, "function")) {
        .Object@row_dend_param$fun = cluster_rows
        .Object@row_dend_param$cluster = TRUE
    } else {
        .Object@row_dend_param$cluster = cluster_rows
        if(!cluster_rows) {
            row_dend_width = unit(0, "mm")
            show_row_dend = FALSE
        }
    }
    if(!show_row_dend) {
        row_dend_width = unit(0, "mm")
    }
    .Object@row_dend_list = list()
    .Object@row_dend_param$distance = clustering_distance_rows
    .Object@row_dend_param$method = clustering_method_rows
    .Object@row_dend_param$side = match.arg(row_dend_side)[1]
    .Object@row_dend_param$width = row_dend_width + ht_opt$DENDROGRAM_PADDING  # append the gap
    .Object@row_dend_param$show = show_row_dend
    .Object@row_dend_param$gp = check_gp(row_dend_gp)
    .Object@row_dend_param$reorder = row_dend_reorder
    .Object@row_order_list = list() # default order
    if(is.null(row_order)) {
        .Object@row_order = seq_len(nrow(matrix))
    }  else {
        if(is.character(row_order)) {
            row_order = structure(seq_len(nrow(matrix)), names = rownames(matrix))[row_order]
        }
        .Object@row_order = row_order
    }

    if(missing(cluster_columns) && !missing(column_order)) {
        cluster_columns = FALSE
    }
    if(inherits(cluster_columns, "dendrogram") || inherits(cluster_columns, "hclust")) {
        .Object@column_dend_param$obj = cluster_columns
        .Object@column_dend_param$cluster = TRUE
    } else if(inherits(cluster_columns, "function")) {
        .Object@column_dend_param$fun = cluster_columns
        .Object@column_dend_param$cluster = TRUE
    } else {
        .Object@column_dend_param$cluster = cluster_columns
        if(!cluster_columns) {
            column_dend_height = unit(0, "mm")
            show_column_dend = FALSE
        }
    }
    if(!show_column_dend) {
        column_dend_height = unit(0, "mm")
    }
    .Object@column_dend_list = list()
    .Object@column_dend_param$distance = clustering_distance_columns
    .Object@column_dend_param$method = clustering_method_columns
    .Object@column_dend_param$side = match.arg(column_dend_side)[1]
    .Object@column_dend_param$height = column_dend_height + ht_opt$DENDROGRAM_PADDING  # append the gap
    .Object@column_dend_param$show = show_column_dend
    .Object@column_dend_param$gp = check_gp(column_dend_gp)
    .Object@column_dend_param$reorder = column_dend_reorder
    if(is.null(column_order)) {
        .Object@column_order = seq_len(ncol(matrix))
    } else {
        if(is.character(column_order)) {
            column_order = structure(seq_len(ncol(matrix)), names = colnames(matrix))[column_order]
        }
        .Object@column_order = column_order
    }

    ######### annotations #############
    .Object@top_annotation = top_annotation # a `HeatmapAnnotation` object
    if(is.null(top_annotation)) {
        .Object@top_annotation_param$height = unit(0, "mm")    
    } else {
        .Object@top_annotation_param$height = height(top_annotation) + ht_opt$COLUMN_ANNO_PADDING  # append the gap
    }
    if(!is.null(top_annotation)) {
        if(length(top_annotation) > 0) {
            if(!.Object@top_annotation@which == "column") {
                stop_wrap("`which` in `top_annotation` should only be `column`.")
            }
        }
        nb = nobs(top_annotation)
        if(!is.na(nb)) {
            if(nb != ncol(.Object@matrix)) {
                stop_wrap("number of observations in top annotation should be as same as ncol of the matrix.")
            }
        }
    }
    
    .Object@bottom_annotation = bottom_annotation # a `HeatmapAnnotation` object
    if(is.null(bottom_annotation)) {
        .Object@bottom_annotation_param$height = unit(0, "mm")
    } else {
        .Object@bottom_annotation_param$height = height(bottom_annotation) + ht_opt$COLUMN_ANNO_PADDING  # append the gap
    }
    if(!is.null(bottom_annotation)) {
        if(length(bottom_annotation) > 0) {
            if(!.Object@bottom_annotation@which == "column") {
                stop_wrap("`which` in `bottom_annotation` should only be `column`.")
            }
        }
        nb = nobs(bottom_annotation)
        if(!is.na(nb)) {
            if(nb != ncol(.Object@matrix)) {
                stop_wrap("number of observations in bottom anntotion should be as same as ncol of the matrix.")
            }
        }
    }

    .Object@left_annotation = left_annotation # a `rowAnnotation` object
    if(is.null(left_annotation)) {
        .Object@left_annotation_param$width = unit(0, "mm")
    } else {
        .Object@left_annotation_param$width = width(left_annotation) + ht_opt$ROW_ANNO_PADDING  # append the gap
    }
    if(!is.null(left_annotation)) {
        if(length(left_annotation) > 0) {
            if(!.Object@left_annotation@which == "row") {
                stop_wrap("`which` in `left_annotation` should only be `row`, or consider using `rowAnnotation()`.")
            }
        }
        nb = nobs(left_annotation)
        if(!is.na(nb)) {
            if(nb != nrow(.Object@matrix)) {
                stop_wrap("number of observations in left anntotion should be same as nrow of the matrix.")
            }
        }
    }

    .Object@right_annotation = right_annotation # a `rowAnnotation` object
    if(is.null(right_annotation)) {
        .Object@right_annotation_param$width = unit(0, "mm")
    } else {
        .Object@right_annotation_param$width = width(right_annotation) + ht_opt$ROW_ANNO_PADDING  # append the gap
    }
    if(!is.null(right_annotation)) {
        if(length(right_annotation) > 0) {
            if(!.Object@right_annotation@which == "row") {
                stop_wrap("`which` in `right_annotation` should only be `row`, or consider using `rowAnnotation()`.")
            }
        }
        nb = nobs(right_annotation)
        if(!is.na(nb)) {
            if(nb != nrow(.Object@matrix)) {
                stop_wrap("number of observations in right anntotion should be same as nrow of the matrix.")
            }
        }
    }

    .Object@layout = list(
        layout_size = list(
            column_title_top_height = unit(0, "mm"),
            column_dend_top_height = unit(0, "mm"),
            column_anno_top_height = unit(0, "mm"),
            column_names_top_height = unit(0, "mm"),
            column_title_bottom_height = unit(0, "mm"),
            column_dend_bottom_height = unit(0, "mm"),
            column_anno_bottom_height = unit(0, "mm"),
            column_names_bottom_height = unit(0, "mm"),

            row_title_left_width = unit(0, "mm"),
            row_dend_left_width = unit(0, "mm"),
            row_names_left_width = unit(0, "mm"),
            row_dend_right_width = unit(0, "mm"),
            row_names_right_width = unit(0, "mm"),
            row_title_right_width = unit(0, "mm"),
            row_anno_left_width = unit(0, "mm"),
            row_anno_right_width = unit(0, "mm")
        ),

        layout_index = NULL,
        graphic_fun_list = list(),
        initialized = FALSE
    )

    if(is.null(width)) {
        width = unit(ncol(matrix), "null")
    } else if(is.numeric(width) && !inherits(width, "unit")) {
        width = unit(width, "null")
    } else if(!inherits(width, "unit")) {
        stop_wrap("`width` should be a `unit` object or a single number.")
    }

    if(is.null(height)) {
        height = unit(nrow(matrix), "null")
    } else if(is.numeric(height) && !inherits(height, "unit")) {
        height = unit(height, "null")
    } else if(!inherits(height, "unit")) {
        stop_wrap("`height` should be a `unit` object or a single number.")
    }

    if(!is.null(width) && !is.null(heatmap_width)) {
        if(is_abs_unit(width) && is_abs_unit(heatmap_width)) {
            stop_wrap("`heatmap_width` and `width` should not all be the absolute units.")
        }
    }
    if(!is.null(height) && !is.null(heatmap_height)) {
        if(is_abs_unit(height) && is_abs_unit(heatmap_height)) {
            stop_wrap("`heatmap_height` and `width` should not all be the absolute units.")
        }
    }
    
    .Object@matrix_param$width = width
    .Object@matrix_param$height = height

    .Object@heatmap_param$width = heatmap_width
    .Object@heatmap_param$height = heatmap_height
    .Object@heatmap_param$show_heatmap_legend = show_heatmap_legend
    .Object@heatmap_param$use_raster = use_raster
    .Object@heatmap_param$raster_device = match.arg(raster_device)[1]
    .Object@heatmap_param$raster_quality = raster_quality
    .Object@heatmap_param$raster_device_param = raster_device_param
    .Object@heatmap_param$verbose = verbose
    .Object@heatmap_param$post_fun = post_fun
    .Object@heatmap_param$calling_env = parent.frame()

    if(nrow(matrix) == 0) {
        .Object@matrix_param$height = unit(0, "mm")
    }
    if(ncol(matrix) == 0) {
        .Object@matrix_param$width = unit(0, "mm")
    }

    return(.Object)

}


# == title
# Make Cluster on Rows
#
# == param
# -object A `Heatmap-class` object.
#
# == details
# The function will fill or adjust ``row_dend_list``, ``row_order_list``, ``row_title`` and ``matrix_param`` slots.
#
# If ``order`` is defined, no clustering will be applied.
#
# This function is only for internal use.
#
# == value
# A `Heatmap-class` object.
#
# == author
# Zuguang Gu <z.gu@dkfz.de>
#
setMethod(f = "make_row_cluster",
    signature = "Heatmap",
    definition = function(object) {

    object = make_cluster(object, "row")
    if(length(object@row_title) > 1) {
        if(length(object@row_title) != length(object@row_order_list)) {
            stop_wrap("If `row_title` is set with length > 1, the length should be as same as the number of row slices.")
        }
    }
    return(object)  
})

# == title
# Make Cluster on Columns
#
# == param
# -object A `Heatmap-class` object.
#
# == details
# The function will fill or adjust ``column_dend_list``,
# ``column_order_list``, ``column_title`` and ``matrix_param`` slots.
#
# If ``order`` is defined, no clustering will be applied.
#
# This function is only for internal use.
#
# == value
# A `Heatmap-class` object.
#
# == author
# Zuguang Gu <z.gu@dkfz.de>
#
setMethod(f = "make_column_cluster",
    signature = "Heatmap",
    definition = function(object) {

    object = make_cluster(object, "column")
    if(length(object@column_title) > 1) {
        if(length(object@column_title) != length(object@column_order_list)) {
            stop_wrap("If `column_title` is set with length > 1, the length should be as same as the number of column slices.")
        }
    }
    return(object)
})

make_cluster = function(object, which = c("row", "column")) {

    which = match.arg(which)[1]

    verbose = object@heatmap_param$verbose

    if(ht_opt("fast_hclust")) {
        hclust = fastcluster::hclust
        if(verbose) qqcat("apply hclust by fastcluster::hclust\n")
    } else {
        hclust = stats::hclust
    }

    mat = object@matrix
    distance = slot(object, paste0(which, "_dend_param"))$distance
    method = slot(object, paste0(which, "_dend_param"))$method
    order = slot(object, paste0(which, "_order"))  # pre-defined row order
    km = getElement(object@matrix_param, paste0(which, "_km"))
    split = getElement(object@matrix_param, paste0(which, "_split"))
    reorder = slot(object, paste0(which, "_dend_param"))$reorder
    cluster = slot(object, paste0(which, "_dend_param"))$cluster
    gap = getElement(object@matrix_param, paste0(which, "_gap"))

    dend_param = slot(object, paste0(which, "_dend_param"))
    dend_list = slot(object, paste0(which, "_dend_list"))
    dend_slice = slot(object, paste0(which, "_dend_slice"))
    order_list = slot(object, paste0(which, "_order_list"))
    order = slot(object, paste0(which, "_order"))

    names_param = slot(object, paste0(which, "_names_param"))

    if(cluster) {

        if(is.numeric(split) && length(split) == 1) {
            if(is.null(dend_param$obj)) {
                if(verbose) qqcat("split @{which}s by cutree, apply hclust on the entire @{which}s\n")
                if(which == "row") {
                    dend_param$obj = hclust(get_dist(mat, distance), method = method)
                } else {
                    dend_param$obj = hclust(get_dist(t(mat), distance), method = method)
                }
            }
        }

        if(!is.null(dend_param$obj)) {
            if(km > 1) {
                stop_wrap("You can not perform k-means clustering since you have already specified a clustering object.")
            }

            if(inherits(dend_param$obj, "hclust")) {
                dend_param$obj = as.dendrogram(dend_param$obj)
                if(verbose) qqcat("convert hclust object to dendrogram object\n")
            }

            if(is.null(split)) {
                dend_list = list(dend_param$obj)
                order_list = list(get_dend_order(dend_param$obj))
                if(verbose) qqcat("since you provided a clustering object and @{which}_split is null, the entrie clustering object is taken as an one-element list.\n")
            } else {
                if(length(split) > 1 || !is.numeric(split)) {
                    stop_wrap(qq("Since you specified a clustering object, you can only split @{which}s by providing a number (number of @{which} slices)."))
                }
                if(split < 2) {
                    stop_wrap("Here `split` should be equal or larger than 2.")
                }
                
                ct = cut_dendrogram(dend_param$obj, split)
                dend_list = ct$lower
                dend_slice = ct$upper
                sth = tapply(order.dendrogram(dend_param$obj), 
                    rep(seq_along(dend_list), times = sapply(dend_list, nobs)), 
                    function(x) x)
                attributes(sth) = NULL
                order_list = sth
                if(verbose) qqcat("cut @{which} dendrogram into @{split} slices.\n")
            }

            ### do reordering if specified
            if(identical(reorder, NULL)) {
                if(is.numeric(mat)) {
                    reorder = TRUE
                } else {
                    reorder = FALSE
                }
            }

            do_reorder = TRUE
            if(identical(reorder, NA) || identical(reorder, FALSE)) {
                do_reorder = FALSE
            }
            if(identical(reorder, TRUE)) {
                do_reorder = TRUE
                if(which == "row") {
                    reorder = -rowMeans(mat, na.rm = TRUE)
                } else {
                    reorder = -colMeans(mat, na.rm = TRUE)
                }
            }

            if(do_reorder) {

                if(which == "row") {
                    if(length(reorder) != nrow(mat)) {
                        stop_wrap("weight of reordering should have same length as number of rows.\n")
                    }
                } else {
                    if(length(reorder) != ncol(mat)) {
                        stop_wrap("weight of reordering should have same length as number of columns\n")
                    }
                }
                
                for(i in seq_along(dend_list)) {
                    if(length(order_list[[i]]) > 1) {
                        sub_ind = sort(order_list[[i]])
                        dend_list[[i]] = reorder(dend_list[[i]], reorder[sub_ind])
                        # the order of object@row_dend_list[[i]] is the order corresponding to the big dendrogram
                        order_list[[i]] = order.dendrogram(dend_list[[i]])
                    }
                }
            }

            dend_list = lapply(dend_list, adjust_dend_by_x)

            slot(object, paste0(which, "_order")) = unlist(order_list)
            slot(object, paste0(which, "_order_list")) = order_list
            slot(object, paste0(which, "_dend_list")) = dend_list
            slot(object, paste0(which, "_dend_param")) = dend_param
            slot(object, paste0(which, "_dend_slice")) = dend_slice

            if(!is.null(split)) {
                split = data.frame(rep(seq_along(order_list), times = sapply(order_list, length)))
                object@matrix_param[[ paste0(which, "_split") ]] = split

                # adjust row_names_param$gp if the length of some elements is the same as row slices
                for(i in seq_along(names_param$gp)) {
                    if(length(names_param$gp[[i]]) == length(order_list)) {
                        gp_temp = NULL
                        for(j in seq_along(order_list)) {
                            gp_temp[ order_list[[j]] ] = names_param$gp[[i]][j]
                        }
                        names_param$gp[[i]] = gp_temp
                    }
                }
                if(!is.null(names_param$anno)) {
                    names_param$anno@var_env$gp = names_param$gp
                }
                slot(object, paste0(which, "_names_param")) = names_param

                n_slice = length(order_list)
                if(length(gap) == 1) {
                    gap = rep(gap, n_slice)
                } else if(length(gap) == n_slice - 1) {
                    gap = unit.c(gap, unit(0, "mm"))
                } else if(length(gap) != n_slice) {
                    stop_wrap(qq("Length of `gap` should be 1 or number of @{which} slices."))
                }
                object@matrix_param[[ paste0(which, "_gap") ]] = gap # adjust title
                
                title = slot(object, paste0(which, "_title"))
                if(!is.null(split)) {
                    if(length(title) == 0 && !is.null(title)) { ## default title
                        title = apply(unique(split), 1, paste, collapse = ",")
                    } else if(length(title) == 1) {
                        if(grepl("%s", title)) {
                            title = apply(unique(split), 1, function(x) {
                                lt = lapply(x, function(x) x)
                                lt$fmt = title
                                do.call(sprintf, lt)
                            })
                        } else if(grepl("@\\{.+\\}", title)) {
                            title = apply(unique(split), 1, function(x) {
                                x = x
                                envir = environment()
                                title = get("title")
                                op = parent.env(envir)
                                calling_env = object@heatmap_param$calling_env
                                parent.env(envir) = calling_env
                                title = GetoptLong::qq(title, envir = envir)
                                parent.env(envir) = op
                                return(title)
                            })
                        } else if(grepl("\\{.+\\}", title)) {
                            if(!requireNamespace("glue")) {
                                stop_wrap("You need to install glue package.")
                            }
                            title = apply(unique(split), 1, function(x) {
                                x = x
                                envir = environment()
                                title = get("title")
                                op = parent.env(envir)
                                calling_env = object@heatmap_param$calling_env
                                parent.env(envir) = calling_env
                                title = glue::glue(title, envir = calling_env)
                                parent.env(envir) = op
                                return(title)
                            })
                        }
                    }
                }
                slot(object, paste0(which, "_title")) = title
            }
            return(object)
        }

    } else {
        if(verbose) qqcat("no clustering is applied/exists on @{which}s\n")
    }
    
    if(verbose) qq("clustering object is not pre-defined, clustering is applied to each @{which} slice\n")
    # make k-means clustering to add a split column
    if(km > 1 && is.numeric(mat)) {
        if(which == "row") {
            km.fit = kmeans(mat, centers = km)
            cl = km.fit$cluster
            meanmat = lapply(unique(cl), function(i) {
                colMeans(mat[cl == i, , drop = FALSE])
            })
        } else {
            km.fit = kmeans(t(mat), centers = km)
            cl = km.fit$cluster
            meanmat = lapply(unique(cl), function(i) {
                rowMeans(mat[, cl == i, drop = FALSE])
            })
        }
        
        meanmat = as.matrix(as.data.frame(meanmat))
        hc = hclust(dist(t(meanmat)))
        weight = colMeans(meanmat)
        hc = as.hclust(reorder(as.dendrogram(hc), -weight))
        cl2 = numeric(length(cl))
        for(i in seq_along(hc$order)) {
            cl2[cl == hc$order[i]] = i
        }
        cl2 = factor(cl2, levels = seq_along(hc$order))

        if(is.null(split)) {
            split = data.frame(cl2)
        } else if(is.matrix(split)) {
            split = as.data.frame(split)
            split = cbind(cl2, split)
        } else if(is.null(ncol(split))) {
            split = data.frame(cl2, split)
        } else {
            split = cbind(cl2, split)
        }
        if(verbose) qqcat("apply k-means (@{km} groups) on @{which}s, append to the `split` data frame\n")
            
    }

    # split the original order into a list according to split
    order_list = list()
    if(is.null(split)) {
        order_list[[1]] = order
    } else {

        if(verbose) cat("process `split` data frame\n")
        if(is.null(ncol(split))) split = data.frame(split)
        if(is.matrix(split)) split = as.data.frame(split)

        for(i in seq_len(ncol(split))) {
            if(is.numeric(split[[i]])) {
                split[[i]] = factor(as.character(split[[i]]), levels = as.character(sort(unique(split[[i]]))))
            } else if(!is.factor(split[[i]])) {
                split[[i]] = factor(split[[i]])
            } else {
                # re-factor
                split[[i]] = factor(split[[i]], levels = intersect(levels(split[[i]]), unique(split[[i]])))
            }
        }

        split_name = apply(as.matrix(split), 1, paste, collapse = ",")

        order2 = do.call("order", split)
        level = unique(split_name[order2])
        for(k in seq_along(level)) {
            l = split_name == level[k]
            order_list[[k]] = intersect(order, which(l))
        }
        names(order_list) = level
    }

    # make dend in each slice
    if(cluster) {
        if(verbose) qqcat("apply clustering on each slice (@{length(order_list)} slices)\n")
        dend_list = rep(list(NULL), length(order_list))
        for(i in seq_along(order_list)) {
            if(which == "row") {
                submat = mat[ order_list[[i]], , drop = FALSE]
            } else {
                submat = mat[, order_list[[i]], drop = FALSE]
            }
            nd = 0
            if(which == "row") nd = nrow(submat) else nd = ncol(submat)
            if(nd > 1) {
                if(!is.null(dend_param$fun)) {
                    if(which == "row") {
                        obj = dend_param$fun(submat)
                    } else {
                        obj = dend_param$fun(t(submat))
                    }
                    if(inherits(obj, "dendrogram") || inherits(obj, "hclust")) {
                        dend_list[[i]] = obj
                    } else {
                        oe = try(obj <- as.dendrogram(obj), silent = TRUE)
                        if(inherits(oe, "try-error")) {
                            stop_wrap("the clustering function must return a `dendrogram` object or a object that can be coerced to `dendrogram` class.")
                        }
                        dend_list[[i]] = obj
                    }
                    order_list[[i]] = order_list[[i]][ get_dend_order(dend_list[[i]]) ]
                } else {

                        if(which == "row") {
                            dend_list[[i]] = hclust(get_dist(submat, distance), method = method)
                        } else {
                            dend_list[[i]] = hclust(get_dist(t(submat), distance), method = method)
                        }
                        order_list[[i]] = order_list[[i]][ get_dend_order(dend_list[[i]]) ]
                    #}
                }
            } else {
                # a dendrogram with one leaf
                dend_list[[i]] = structure(1, members = 1, height = 0, leaf = TRUE, class = "dendrogram")
                order_list[[i]] = order_list[[i]][1]
            }
        }
        names(dend_list) = names(order_list)

        for(i in seq_along(dend_list)) {
            if(inherits(dend_list[[i]], "hclust")) {
                dend_list[[i]] = as.dendrogram(dend_list[[i]])
            }
        }

        if(identical(reorder, NULL)) {
            if(is.numeric(mat)) {
                reorder = TRUE
            } else {
                reorder = FALSE
            }
        }

        do_reorder = TRUE
        if(identical(reorder, NA) || identical(reorder, FALSE)) {
            do_reorder = FALSE
        }
        if(identical(reorder, TRUE)) {
            do_reorder = TRUE
            if(which == "row") {
                reorder = -rowMeans(mat, na.rm = TRUE)
            } else {
                reorder = -colMeans(mat, na.rm = TRUE)
            }
        }

        if(do_reorder) {

            if(which == "row") {
                if(length(reorder) != nrow(mat)) {
                    stop_wrap("weight of reordering should have same length as number of rows\n")
                }
            } else {
                if(length(reorder) != ncol(mat)) {
                    stop_wrap("weight of reordering should have same length as number of columns\n")
                }
            }
            for(i in seq_along(dend_list)) {
                if(length(order_list[[i]]) > 1) {
                    sub_ind = sort(order_list[[i]])
                    dend_list[[i]] = reorder(dend_list[[i]], reorder[sub_ind])
                    order_list[[i]] = sub_ind[ order.dendrogram(dend_list[[i]]) ]
                }
            }
            if(verbose) qqcat("reorder dendrograms in each @{which} slice\n")
        }

        if(length(order_list) > 1) {
            if(which == "row") {
                slice_mean = sapply(order_list, function(ind) colMeans(mat[ind, , drop = FALSE]))
            } else {
                slice_mean = sapply(order_list, function(ind) rowMeans(mat[, ind, drop = FALSE]))
            }
            if(!is.matrix(slice_mean)) {
                slice_mean = matrix(slice_mean, nrow = 1)
            }
            dend_slice = as.dendrogram(hclust(dist(t(slice_mean))))
            if(verbose) qqcat("perform clustering on mean of @{which} slices\n")
        }
    }

    dend_list = lapply(dend_list, adjust_dend_by_x)

    slot(object, paste0(which, "_order")) = unlist(order_list)
    slot(object, paste0(which, "_order_list")) = order_list
    slot(object, paste0(which, "_dend_list")) = dend_list
    slot(object, paste0(which, "_dend_param")) = dend_param
    slot(object, paste0(which, "_dend_slice")) = dend_slice
    object@matrix_param[[ paste0(which, "_split") ]] = split

    if(which == "row") {
        if(nrow(mat) != length(order)) {
            stop_wrap(qq("Number of rows in the matrix are not the same as the length of the cluster or the @{which} orders."))
        }
    } else {
        if(ncol(mat) != length(order)) {
            stop_wrap(qq("Number of columns in the matrix are not the same as the length of the cluster or the @{which} orders."))
        }
    }

    # adjust names_param$gp if the length of some elements is the same as slices
    for(i in seq_along(names_param$gp)) {
        if(length(names_param$gp[[i]]) == length(order_list)) {
            gp_temp = NULL
            for(j in seq_along(order_list)) {
                gp_temp[ order_list[[j]] ] = names_param$gp[[i]][j]
            }
            names_param$gp[[i]] = gp_temp   
        }
    }
    if(!is.null(names_param$anno)) {
        names_param$anno@var_env$gp = names_param$gp
    }
    slot(object, paste0(which, "_names_param")) = names_param

    n_slice = length(order_list)
    if(length(gap) == 1) {
        gap = rep(gap, n_slice)
    } else if(length(gap) == n_slice - 1) {
        gap = unit.c(gap, unit(0, "mm"))
    } else if(length(gap) != n_slice) {
        stop_wrap(qq("Length of `gap` should be 1 or number of @{which} slices."))
    }
    object@matrix_param[[ paste0(which, "_gap") ]] = gap
    
    # adjust title
    title = slot(object, paste0(which, "_title"))
    if(!is.null(split)) {
        if(length(title) == 0 && !is.null(title)) { ## default title
            title = names(order_list)
        } else if(length(title) == 1) {
            if(grepl("%s", title)) {
                title = apply(unique(split[order2, , drop = FALSE]), 1, function(x) {
                    lt = lapply(x, function(x) x)
                    lt$fmt = title
                    do.call(sprintf, lt)
                })
            } else if(grepl("@\\{.+\\}", title)) {
                title = apply(unique(split), 1, function(x) {
                    x = x
                    envir = environment()
                    title = get("title")
                    op = parent.env(envir)
                    calling_env = object@heatmap_param$calling_env
                    parent.env(envir) = calling_env
                    title = GetoptLong::qq(title, envir = envir)
                    parent.env(envir) = op
                    return(title)
                })
            } else if(grepl("\\{.+\\}", title)) {
                if(!requireNamespace("glue")) {
                    stop_wrap("You need to install glue package.")
                }
                title = apply(unique(split), 1, function(x) {
                    x = x
                    envir = environment()
                    title = get("title")
                    op = parent.env(envir)
                    calling_env = object@heatmap_param$calling_env
                    parent.env(envir) = calling_env
                    title = glue::glue(title, envir = calling_env)
                    parent.env(envir) = op
                    return(title)
                })
            }
        }
    }
    slot(object, paste0(which, "_title")) = title
    return(object)

}

# == title
# Draw a Single Heatmap
#
# == param
# -object A `Heatmap-class` object.
# -internal If ``TRUE``, it is only used inside the calling of `draw,HeatmapList-method`. 
#           It only draws the heatmap without legends where the legend will be drawn by `draw,HeatmapList-method`. 
# -test Only for testing. If it is ``TRUE``, the heatmap body is directly drawn.
# -... Pass to `draw,HeatmapList-method`.
#
# == detail
# The function creates a `HeatmapList-class` object which only contains a single heatmap
# and call `draw,HeatmapList-method` to make the final heatmap.
#
# There are some arguments which control the some settings of the heatmap such as legends.
# Please go to `draw,HeatmapList-method` for these arguments.
#
# == value
# A `HeatmapList-class` object.
#
# == author
# Zuguang Gu <z.gu@dkfz.de>
#
setMethod(f = "draw",
    signature = "Heatmap",
    definition = function(object, internal = FALSE, test = FALSE, ...) {

    if(test) {
        object = prepare(object)
        grid.newpage()
        if(is_abs_unit(object@heatmap_param$width)) {
            width = object@heatmap_param$width
        } else {
            width = 0.8
        }
        if(is_abs_unit(object@heatmap_param$height)) {
            height = object@heatmap_param$height
        } else {
            height = 0.8
        }
        pushViewport(viewport(width = width, height = height))
        draw(object, internal = TRUE)
        upViewport()
    } else {
        if(internal) {  # a heatmap without legend
            if(ncol(object@matrix) == 0 || nrow(object@matrix) == 0) return(invisible(NULL))
            layout = grid.layout(nrow = length(HEATMAP_LAYOUT_COLUMN_COMPONENT), 
                ncol = length(HEATMAP_LAYOUT_ROW_COMPONENT), widths = component_width(object), 
                heights = component_height(object))
            pushViewport(viewport(layout = layout))
            ht_layout_index = object@layout$layout_index
            ht_graphic_fun_list = object@layout$graphic_fun_list
            for(j in seq_len(nrow(ht_layout_index))) {
                if(HEATMAP_LAYOUT_COLUMN_COMPONENT["heatmap_body"] %in% ht_layout_index[j, 1] && 
                   HEATMAP_LAYOUT_ROW_COMPONENT["heatmap_body"] %in% ht_layout_index[j, 2]) {
                    pushViewport(viewport(layout.pos.row = ht_layout_index[j, 1], layout.pos.col = ht_layout_index[j, 2], name = paste(object@name, "heatmap_body_wrap", sep = "_")))
                } else {
                    pushViewport(viewport(layout.pos.row = ht_layout_index[j, 1], layout.pos.col = ht_layout_index[j, 2]))
                }
                ht_graphic_fun_list[[j]](object)
                upViewport()
            }
            upViewport()
        } else {
            if(ncol(object@matrix) == 0) {
                stop_wrap("Single heatmap should contains a matrix with at least one column. Zero-column matrix can only be appended to the heatmap list.")
            }
            ht_list = new("HeatmapList")
            ht_list = add_heatmap(ht_list, object)
            draw(ht_list, ...)
        }
    }
})

# == title
# Prepare the Heatmap
#
# == param
# -object A `Heatmap-class` object.
# -process_rows Whether to process rows of the heatmap.
# -process_columns Whether to process columns of the heatmap.
#
# == detail
# The preparation of the heatmap includes following steps:
#
# - making clustering on rows (by calling `make_row_cluster,Heatmap-method`)
# - making clustering on columns (by calling `make_column_cluster,Heatmap-method`)
# - making the layout of the heatmap (by calling `make_layout,Heatmap-method`)
#
# This function is only for internal use.
#
# == value
# The `Heatmap-class` object.
#
# == author
# Zuguang Gu <z.gu@dkfz.de>
#
setMethod(f = "prepare",
    signature = "Heatmap",
    definition = function(object, process_rows = TRUE, process_columns = TRUE) {

    if(object@layout$initialized) {
        return(object)
    }
    
    if(process_rows) {
        object = make_row_cluster(object)
    }
    if(process_columns) {
        object = make_column_cluster(object)
    }

    object = make_layout(object)
    return(object)

})