#' Append matched datasets together
#'
#' These functions are [rbind()] methods for objects resulting from calls to
#' [match_data()] and [get_matches()]. They function nearly identically to
#' `rbind.data.frame()`; see Details for how they differ.
#'
#' @aliases rbind.matchdata rbind.getmatches
#'
#' @param \dots Two or more `matchdata` or `getmatches` objects the
#' output of calls to [match_data()] and [get_matches()], respectively.
#' Supplied objects must either be all `matchdata` objects or all
#' `getmatches` objects.
#' @param deparse.level Passed to [rbind()].
#'
#' @return An object of the same class as those supplied to it (i.e., a
#' `matchdata` object if `matchdata` objects are supplied and a
#' `getmatches` object if `getmatches` objects are supplied).
#' [rbind()] is called on the objects after adjusting the variables so that the
#' appropriate method will be dispatched corresponding to the class of the
#' original data object.
#'
#' @details
#' `rbind()` appends two or more datasets row-wise. This can be useful
#' when matching was performed separately on subsets of the original data and
#' they are to be combined into a single dataset for effect estimation. Using
#' the regular `data.frame` method for `rbind()` would pose a
#' problem, however; the `subclass` variable would have repeated names
#' across different datasets, even though units only belong to the subclasses
#' in their respective datasets. `rbind.matchdata()` renames the
#' subclasses so that the correct subclass membership is maintained.
#'
#' The supplied matched datasets must be generated from the same original
#' dataset, that is, having the same variables in it. The added components
#' (e.g., weights, subclass) can be named differently in different datasets but
#' will be changed to have the same name in the output.
#'
#' `rbind.getmatches()` and `rbind.matchdata()` are identical.
#'
#' @author Noah Greifer
#' @seealso [match_data()], [rbind()]
#'
#' See `vignettes("estimating-effects")` for details on using
#' `rbind()` for effect estimation after subsetting the data.
#'
#' @examples
#'
#' data("lalonde")
#'
#' # Matching based on race subsets
#' m.out_b <- matchit(treat ~ age + educ + married +
#'                     nodegree + re74 + re75,
#'                   data = subset(lalonde, race == "black"))
#' md_b <- match_data(m.out_b)
#'
#' m.out_h <- matchit(treat ~ age + educ + married +
#'                     nodegree + re74 + re75,
#'                   data = subset(lalonde, race == "hispan"))
#' md_h <- match_data(m.out_h)
#'
#' m.out_w <- matchit(treat ~ age + educ + married +
#'                     nodegree + re74 + re75,
#'                   data = subset(lalonde, race == "white"))
#' md_w <- match_data(m.out_w)
#'
#' #Bind the datasets together
#' md_all <- rbind(md_b, md_h, md_w)
#'
#' #Subclass conflicts are avoided
#' levels(md_all$subclass)
#'
#' @exportS3Method rbind matchdata
rbind.matchdata <- function(..., deparse.level = 1) {

  allargs <- list(...)
  allargs <- allargs[lengths(allargs) > 0L]
  if (is_null(names(allargs))) {
    md_list <- allargs
    allargs <- list()
  }
  else {
    md_list <- allargs[!nzchar(names(allargs))]
    allargs[!nzchar(names(allargs))] <- NULL
  }

  allargs$deparse.level <- deparse.level

  type <- intersect(c("matchdata", "getmatches"), unlist(lapply(md_list, class)))

  if (is_null(type)) {
    .err("a `matchdata` or `getmatches` object must be supplied")
  }

  if (length(type) == 2L) {
    .err("supplied objects must be all `matchdata` objects or all `getmatches` objects")
  }

  attrs <- c("distance", "weights", "subclass", "id")
  attr_list <- setNames(vector("list", length(attrs)), attrs)
  key_attrs <- setNames(rep.int(NA_character_, length(attrs)), attrs)

  for (i in attrs) {
    attr_list[[i]] <- unlist(lapply(md_list, function(m) {
      a <- attr(m, i)
      if (is_null(a)) NA_character_ else a
    }))

    if (all(is.na(attr_list[[i]]))) {
      attr_list[[i]] <- NULL
    }
    else {
      key_attrs[i] <- Find(Negate(is.na), attr_list[[i]])
    }
  }
  attrs <- names(attr_list)
  key_attrs <- key_attrs[attrs]

  #Check if all non-attr columns are the same across datasets
  other_col_list <- lapply(seq_along(md_list), function(d) {
    setdiff(names(md_list[[d]]), unlist(lapply(attr_list, `[`, d)))
  })

  for (d in seq_along(md_list)[-1L]) {
    if (length(other_col_list[[d]]) != length(other_col_list[[1L]]) ||
        !all(other_col_list[[d]] %in% other_col_list[[1L]])) {
      .err(sprintf("the %s inputs must come from the same dataset",
                 switch(type, "matchdata" = "`match_data()`", "`get_matches()`")))
    }
  }

  for (d in seq_along(md_list)) {
    for (i in attrs) {
      #Rename columns of each attribute the same across datasets
      if (is_null(attr(md_list[[d]], i))) {
        md_list[[d]] <- setNames(cbind(md_list[[d]], NA), c(names(md_list[[d]]), key_attrs[i]))
      }
      else {
        names(md_list[[d]])[names(md_list[[d]]) == attr_list[[i]][d]] <- key_attrs[i]
      }

      #Give subclasses unique values across datasets
      if (i == "subclass") {
        if (all(is.na(md_list[[d]][[key_attrs[i]]]))) {
          md_list[[d]][[key_attrs[i]]] <- factor(md_list[[d]][[key_attrs[i]]], levels = NA)
        }
        else {
          levels(md_list[[d]][[key_attrs[i]]]) <- paste(d, levels(md_list[[d]][[key_attrs[i]]]), sep = "_")
        }
      }
    }

    #Put all columns in the same order
    if (d > 1) {
      md_list[[d]] <- md_list[[d]][names(md_list[[1L]])]
    }

    class(md_list[[d]]) <- setdiff(class(md_list[[d]]), type)
  }

  out <- do.call("rbind", c(md_list, allargs))

  for (i in attrs) {
    attr(out, i) <- unname(key_attrs[i])
  }

  class(out) <- c(type, class(out))

  out
}

#' @exportS3Method rbind getmatches
#' @rdname rbind.matchdata
rbind.getmatches <- rbind.matchdata
