#' Cardinality between time granules
#'
#' This S7 generic function defines the calendrical relationships between two 
#' chronons, and is one of the building block for defining calendars in mixtime.
#' It calculates how many `x` chronons fit into the `y` chronon. Some chronon 
#' sizes are context-dependent (such as the number of days in a month),
#' and so an optional time point defined in terms of `y` chronons can be
#' provided with `at`.
#' 
#' @param x The finer time granule (e.g. `cal_gregorian$month(1L)`)
#' @param y The coarser time granule (e.g. `cal_gregorian$year(1L)`)
#' @param ... Additional arguments for methods.
# #' @param at Optional time point for context-dependent cardinality, defined in
# #' terms of `y` (e.g., if `y` is `month()`, then `at` could be a 
# #' `yearmonth()`)
#'
#' @return Numeric describing how many `x` time granules fit into `y` at time `at`.
#' 
#' @details
#' 
#' The methods are dispatched based on the shortest path along defined methods.
#' This allows for defining only the direct relationships between adjacent
#' time units, and relying on graph traversal to find how to convert between
#' more distant units. For example the number of seconds in an hour can be
#' calculated from the number of seconds in a minute and then number of minutes
#' in an hour.
#' 
#' If a method is defined for converting between time units of different 
#' calendar systems (e.g., Gregorian calendar days to Chinese calendar days),
#' then that method can be used to convert times at any granularity between the
#' two systems.
#' 
#' @examples
#' # There are 12 months in a year
#' with(cal_gregorian, chronon_cardinality(month(1L), year(1L)))
#' 
#' # There are 7 days in a week
#' with(cal_isoweek, chronon_cardinality(day(1L), week(1L)))
#' 
#' # There are 3600 seconds in an hour
#' with(cal_gregorian, chronon_cardinality(second(1L), hour(1L)))
#' 
#' # There are 18 "2 months" in 3 years
#' with(cal_gregorian, chronon_cardinality(month(2L), year(3L)))
#' 
#' # There are 365 days in 2025 (a common year)
#' chronon_cardinality(
#'   cal_gregorian$day(1L), cal_gregorian$year(1L),
#'   at = year(as.Date("2025-01-01"))
#' )
#' 
#' # There are 366 days in 2024 (a leap year)
#' chronon_cardinality(
#'   cal_gregorian$day(1L), cal_gregorian$year(1L), 
#'   at = mixtime::year(as.Date("2024-01-01"))
#' )
#' 
#' # There are 29 days in February 2024 (a leap year)
#' chronon_cardinality(
#'   cal_gregorian$day(1L), cal_gregorian$month(1L), 
#'   at = yearmonth(as.Date("2024-02-01"))
#' )
#'
#' @export
chronon_cardinality <- S7::new_generic("chronon_cardinality", c("x", "y"))

# #' @rdname chronon_cardinality
method(chronon_cardinality, list(mt_unit, mt_unit)) <- function(x, y, at = NULL) {
  # Check if x and y are the same class
  if (S7_class_id(x) == S7_class_id(y)) {
    return(y@n/x@n)
  }

  # Try to find a method with arguments swapped
  # (This feels unsafe for finding exact matching of S7 dispatch.)
  
  if (!is.null(y_env <- chronon_cardinality@methods[[S7_class_id(y)]])) {
    if (S7_class_id(x) %in% names(y_env)) {
      # Matching inverse method found, use it with inversion.
      # TODO: Convert at to x units?
      return(1/chronon_cardinality(y, x, at = at))
    }
  }

  # No specific method defined between these classes
  # Attempt graph traversal to find a sequence of methods
  path <- S7_graph_dispatch(method_signatures(chronon_cardinality), y, x)

  path[[1]] <- y
  path[[length(path)]] <- x
  # Initialise intermediate classes with 1L
  path[c(-1, -length(path))] <- lapply(path[c(-1, -length(path))], function(x) x(1L))

  result <- path[[1]]
  for (i in seq(2, length.out = length(path)-1)) {
    ## QUESTION: Why does this not work with `generic` instead of `chronon_cardinality`? S7 bug?

    result <- chronon_cardinality(path[[i]], result, at)
    if (!is.null(at)) at <- at * result
    # Class the result with the next class in the path
    result <- S7::S7_class(path[[i]])(result)
  }

  result@n
  # vec_data(result)
}