#' @title InMemoryAnnData
#'
#' @description
#' Implementation of an in-memory `AnnData` object where data is stored within
#' the R session. This is the simplest back end and will be most familiar to
#' users. It is want you will want to use in most cases where you want to
#' interact with an `AnnData` object.
#'
#' See [AnnData-usage] for details on creating and using `AnnData` objects.
#'
#' @return An `InMemoryAnnData` object
#'
#' @seealso [AnnData-usage] for details on creating and using `AnnData` objects
#'
#' @family AnnData classes
#'
#' @examples
#' ## complete example
#' ad <- AnnData(
#'   X = matrix(1:15, 3L, 5L),
#'   layers = list(
#'     A = matrix(5:1, 3L, 5L),
#'     B = matrix(letters[1:5], 3L, 5L)
#'   ),
#'   obs = data.frame(row.names = LETTERS[1:3], cell = 1:3),
#'   var = data.frame(row.names = letters[1:5], gene = 1:5)
#' )
#' ad
#'
#' ## minimum example
#' AnnData(
#'   obs = data.frame(row.names = letters[1:10]),
#'   var = data.frame(row.names = LETTERS[1:5])
#' )
#'
#' @importFrom Matrix as.matrix
InMemoryAnnData <- R6::R6Class(
  "InMemoryAnnData", # nolint
  inherit = AbstractAnnData,
  private = list(
    .X = NULL,
    .layers = NULL,
    .obs = NULL,
    .var = NULL,
    .obs_names = NULL,
    .var_names = NULL,
    .obsm = NULL,
    .varm = NULL,
    .obsp = NULL,
    .varp = NULL,
    .uns = NULL
  ),
  active = list(
    #' @field X See [AnnData-usage]
    X = function(value) {
      if (missing(value)) {
        # trackstatus: class=InMemoryAnnData, feature=get_X, status=done
        private$.X |>
          private$.add_matrix_dimnames("X")
      } else {
        # trackstatus: class=InMemoryAnnData, feature=set_X, status=done
        private$.X <- private$.validate_aligned_array(
          value,
          "X",
          shape = c(self$n_obs(), self$n_vars()),
          expected_rownames = self$obs_names,
          expected_colnames = self$var_names
        )
        self
      }
    },
    #' @field layers See [AnnData-usage]
    layers = function(value) {
      if (missing(value)) {
        # trackstatus: class=InMemoryAnnData, feature=get_layers, status=done
        private$.layers |>
          private$.add_mapping_dimnames("layers")
      } else {
        # trackstatus: class=InMemoryAnnData, feature=set_layers, status=done
        private$.layers <- private$.validate_aligned_mapping(
          value,
          "layers",
          c(self$n_obs(), self$n_vars()),
          expected_rownames = self$obs_names,
          expected_colnames = self$var_names
        )
        self
      }
    },
    #' @field obs See [AnnData-usage]
    obs = function(value) {
      if (missing(value)) {
        # trackstatus: class=InMemoryAnnData, feature=get_obs, status=done
        private$.obs |>
          private$.add_obsvar_dimnames("obs")
      } else {
        # trackstatus: class=InMemoryAnnData, feature=set_obs, status=done
        # Extract obs_names if present before validation
        if (!is.null(value) && has_row_names(value)) {
          private$.obs_names <- rownames(value)
          rownames(value) <- NULL
        }
        private$.obs <- private$.validate_obsvar_dataframe(value, "obs")
        self
      }
    },
    #' @field var See [AnnData-usage]
    var = function(value) {
      if (missing(value)) {
        # trackstatus: class=InMemoryAnnData, feature=get_var, status=done
        private$.var |>
          private$.add_obsvar_dimnames("var")
      } else {
        # trackstatus: class=InMemoryAnnData, feature=set_var, status=done
        # Extract var_names if present before validation
        if (!is.null(value) && has_row_names(value)) {
          private$.var_names <- rownames(value)
          rownames(value) <- NULL
        }
        private$.var <- private$.validate_obsvar_dataframe(value, "var")
        self
      }
    },
    #' @field obs_names See [AnnData-usage]
    obs_names = function(value) {
      if (missing(value)) {
        # trackstatus: class=InMemoryAnnData, feature=get_obs_names, status=done
        private$.obs_names
      } else {
        # trackstatus: class=InMemoryAnnData, feature=set_obs_names, status=done
        private$.obs_names <- private$.validate_obsvar_names(value, "obs")
        self
      }
    },
    #' @field var_names See [AnnData-usage]
    var_names = function(value) {
      if (missing(value)) {
        # trackstatus: class=InMemoryAnnData, feature=get_var_names, status=done
        private$.var_names
      } else {
        # trackstatus: class=InMemoryAnnData, feature=set_var_names, status=done
        private$.var_names <- private$.validate_obsvar_names(value, "var")
        self
      }
    },
    #' @field obsm See [AnnData-usage]
    obsm = function(value) {
      if (missing(value)) {
        # trackstatus: class=InMemoryAnnData, feature=get_obsm, status=done
        private$.obsm |>
          private$.add_mapping_dimnames("obsm")
      } else {
        # trackstatus: class=InMemoryAnnData, feature=set_obsm, status=done
        private$.obsm <- private$.validate_aligned_mapping(
          value,
          "obsm",
          c(self$n_obs()),
          expected_rownames = self$obs_names,
          strip_rownames = TRUE,
          strip_colnames = FALSE
        )
        self
      }
    },
    #' @field varm See [AnnData-usage]
    varm = function(value) {
      if (missing(value)) {
        # trackstatus: class=InMemoryAnnData, feature=get_varm, status=done
        private$.varm |>
          private$.add_mapping_dimnames("varm")
      } else {
        # trackstatus: class=InMemoryAnnData, feature=set_varm, status=done
        private$.varm <- private$.validate_aligned_mapping(
          value,
          "varm",
          c(self$n_vars()),
          expected_rownames = self$var_names,
          strip_rownames = TRUE,
          strip_colnames = FALSE
        )
        self
      }
    },
    #' @field obsp See [AnnData-usage]
    obsp = function(value) {
      if (missing(value)) {
        # trackstatus: class=InMemoryAnnData, feature=get_obsp, status=done
        private$.obsp |>
          private$.add_mapping_dimnames("obsp")
      } else {
        # trackstatus: class=InMemoryAnnData, feature=set_obsp, status=done
        private$.obsp <- private$.validate_aligned_mapping(
          value,
          "obsp",
          c(self$n_obs(), self$n_obs()),
          expected_rownames = self$obs_names,
          expected_colnames = self$obs_names
        )
        self
      }
    },
    #' @field varp See [AnnData-usage]
    varp = function(value) {
      if (missing(value)) {
        # trackstatus: class=InMemoryAnnData, feature=get_varp, status=done
        private$.varp |>
          private$.add_mapping_dimnames("varp")
      } else {
        # trackstatus: class=InMemoryAnnData, feature=set_varp, status=done
        private$.varp <- private$.validate_aligned_mapping(
          value,
          "varp",
          c(self$n_vars(), self$n_vars()),
          expected_rownames = self$var_names,
          expected_colnames = self$var_names
        )
        self
      }
    },
    #' @field uns See [AnnData-usage]
    uns = function(value) {
      if (missing(value)) {
        # trackstatus: class=InMemoryAnnData, feature=get_uns, status=done
        private$.uns
      } else {
        # trackstatus: class=InMemoryAnnData, feature=set_uns, status=done
        private$.uns <- private$.validate_named_list(value, "uns")
        self
      }
    }
  ),
  public = list(
    #' @description Creates a new instance of an in-memory `AnnData` object.
    #'   Inherits from [AbstractAnnData].
    #'
    #' @param X See the `X` slot in [AnnData-usage]
    #' @param layers See the `layers` slot in [AnnData-usage]
    #' @param obs See the `obs` slot in [AnnData-usage]
    #' @param var See the `var` slot in [AnnData-usage]
    #' @param obsm See the `obsm` slot in [AnnData-usage]
    #' @param varm See the `varm` slot in [AnnData-usage]
    #' @param obsp See the `obsp` slot in [AnnData-usage]
    #' @param varp See the `varp` slot in [AnnData-usage]
    #' @param uns See the `uns` slot in [AnnData-usage]
    #' @param shape Shape tuple (e.g. `c(n_obs, n_vars)`). Can be provided if
    #'   both `X` or `obs` and `var` are not provided.
    initialize = function(
      X = NULL,
      obs = NULL,
      var = NULL,
      layers = NULL,
      obsm = NULL,
      varm = NULL,
      obsp = NULL,
      varp = NULL,
      uns = NULL,
      shape = NULL
    ) {
      # Determine initial obs and var
      shape <- get_shape(obs, var, X, shape)
      obs <- get_initial_obs(obs, X, shape)
      var <- get_initial_var(var, X, shape)

      # set obs and var first
      if (!is.data.frame(obs)) {
        cli_abort("{.arg obs} must be a {.cls data.frame}")
      }
      if (!is.data.frame(var)) {
        cli_abort("{.arg var} must be a {.cls data.frame}")
      }

      # Extract and store obs_names/var_names separately
      private$.obs_names <- rownames(obs)
      private$.var_names <- rownames(var)

      # Remove rownames from the data.frames before storing
      rownames(obs) <- NULL
      rownames(var) <- NULL
      private$.obs <- obs
      private$.var <- var

      # write other slots later
      self$X <- X
      self$layers <- layers
      self$obsm <- obsm
      self$varm <- varm
      self$obsp <- obsp
      self$varp <- varp
      self$uns <- uns
    }
  )
)

#' Convert an `AnnData` to an `InMemoryAnnData`
#'
#' Convert another `AnnData` object to an [`InMemoryAnnData`] object
#'
#' @param adata An `AnnData` object to be converted to [`InMemoryAnnData`]
#'
#' @return An [`InMemoryAnnData`] object with the same data as the input
#'   `AnnData` object
#' @keywords internal
#'
#' @family object converters
#'
#' @examples
#' ad <- AnnData(
#'   X = matrix(1:5, 3L, 5L),
#'   layers = list(
#'     A = matrix(5:1, 3L, 5L),
#'     B = matrix(letters[1:5], 3L, 5L)
#'   ),
#'   obs = data.frame(row.names = LETTERS[1:3], cell = 1:3),
#'   var = data.frame(row.names = letters[1:5], gene = 1:5)
#' )
#' ad$as_InMemoryAnnData()
# nolint start: object_name_linter
as_InMemoryAnnData <- function(adata) {
  # nolint end: object_name_linter
  if (!(inherits(adata, "AbstractAnnData"))) {
    cli_abort(
      "{.arg adata} must be a {.cls AbstractAnnData} but has class {.cls {class(adata)}}"
    )
  }

  InMemoryAnnData$new(
    X = adata$X,
    obs = adata$obs,
    var = adata$var,
    layers = adata$layers,
    obsm = adata$obsm,
    varm = adata$varm,
    obsp = adata$obsp,
    varp = adata$varp,
    uns = adata$uns,
    shape = adata$shape()
  )
}
