# R/write.R  ----------------------------------------------------------------
# CBF Writer: ROI OP --> Conic Benchmark Format (version 4)
# ---------------------------------------------------------------------------
#
# Sign convention  (reciprocal of read.R)
# ----------------------------------------
# CBF encodes   A_i x + b_i  in  K_i
# ROI stores linear constraints as   A x dir rhs
# and conic constraints as           rhs - A x in K
#
# Therefore:
#   linear:  b_cbf = -rhs_lin
#   conic:   A_cbf = -L_roi,   b_cbf = rhs_roi
#
# Concretely:
#   L_constraint(A, ">=", rhs)  ->  A x - rhs >= 0  ->  A x + (-rhs) in L+
#   L_constraint(A, "<=", rhs)  ->  A x - rhs <= 0  ->  A x + (-rhs) in L-
#   L_constraint(A, "==", rhs)  ->  A x - rhs = 0  ->  A x + (-rhs) in L=
#   C_constraint(L, K,    rhs)  ->  rhs - L x in K  ->  (-L) x + rhs in K
# ---------------------------------------------------------------------------


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

#' Write an ROI OP Object to a CBF File
#'
#' Serialises an ROI optimisation problem (\code{\link[ROI]{OP}}) to a plain
#' text file in the Conic Benchmark Format (CBF version 4).
#'
#' @details
#' Supported OP components
#' \describe{
#'   \item{Objective}{Linear objectives (\code{\link[ROI]{L_objective}}).}
#'   \item{Constraints}{
#'     \code{\link[ROI]{L_constraint}} is mapped to \code{L+} (\code{">="}),
#'     \code{L-} (\code{"<="}), or \code{L=} (\code{"=="}) cone chunks.\cr
#'     \code{\link[ROI]{C_constraint}} is mapped to the appropriate CBF cone:
#'     \code{Q.}, \code{QR.}, \code{EXP.}, \code{EXP*.}, \code{SVECPSD.},
#'     \code{@k:POW}, or \code{@k:POW*}.}
#'   \item{Variable types}{Continuous (\code{"C"}) and integer (\code{"I"});
#'     integer variables are written to the INT section.}
#'   \item{Variable bounds}{Translated to \code{F.}, \code{L+}, \code{L-},
#'     or \code{L=} chunks in the VAR section.}
#' }
#'
#' @section Sign convention:
#' CBF encodes \eqn{Ax + b \in K}.  ROI stores \eqn{Ax - \text{rhs} \in K}.
#' Therefore \code{write_cbf} writes \eqn{b = -\text{rhs}} to the BCOORD
#' section.
#'
#' @param op    An \code{\link[ROI]{OP}} object.
#' @param file  Character string. Destination file path (created or
#'   overwritten).
#' @param ...   Currently unused.
#'
#' @return Invisibly returns \code{file}.
#'
#' @examples
#' ## simple LP:  min x + y  s.t.  x + y >= 1,  x, y >= 0
#' op <- ROI::OP(
#'     objective   = ROI::L_objective(c(1, 1)),
#'     constraints = ROI::L_constraint(
#'         L   = matrix(c(1, 1), nrow = 1),
#'         dir = ">=",
#'         rhs = 1
#'     )
#' )
#' f <- file.path(tempdir(), "lp.cbf")
#' write_cbf(op, f)
#'
#' @seealso \code{\link{read_cbf}}
#' @export
write_cbf <- function(op, file, ...) {
    stopifnot(inherits(op, "OP"))
    con <- file(file, open = "wt", encoding = "UTF-8")
    on.exit(close(con), add = TRUE)
    .write_cbf_internal(op, con)
    invisible(file)
}


# ---------------------------------------------------------------------------
# Internal top-level writer
# ---------------------------------------------------------------------------

#' Internal CBF writer -- writes all sections to an open connection.
#' @keywords internal
.write_cbf_internal <- function(op, con) {

    n_var   <- length(ROI::objective(op))
    obj     <- ROI::objective(op)
    cons    <- ROI::constraints(op)
    bds     <- ROI::bounds(op)
    types_v <- ROI::types(op)
    is_max  <- isTRUE(op$maximum)

    # -- VER ------------------------------------------------------------------
    cat("# Written by ROI.format.cbf\n",
        "VER\n4\n\n",
        sep = "", file = con)

    # -- OBJSENSE -------------------------------------------------------------
    cat("OBJSENSE\n",
        if (is_max) "MAX" else "MIN",
        "\n\n",
        sep = "", file = con)

    # -- POWCONES / POW*CONES -------------------------------------------------
    # Scan all conic constraints to collect unique power-cone alpha vectors.
    # These must be written before the problem structure (per CBF spec Sec.2.2).
    pow_tables <- .collect_powcone_tables(cons)
    .write_powcones_section(pow_tables$primal, "POWCONES",  con)
    .write_powcones_section(pow_tables$dual,   "POW*CONES", con)

    # -- VAR ------------------------------------------------------------------
    # Translate variable bounds to CBF cone chunks.
    #
    # ROI's default lower bound is 0 (nonneg), upper bound +Inf.
    # V_bound stores only the non-default values; we materialise full vectors.
    lb_full <- rep(0,    n_var)   # ROI default lb
    ub_full <- rep(Inf,  n_var)   # ROI default ub
    if (!is.null(bds)) {
        if (length(bds$lower$ind) > 0L)
            lb_full[bds$lower$ind] <- bds$lower$val
        if (length(bds$upper$ind) > 0L)
            ub_full[bds$upper$ind] <- bds$upper$val
    }

    var_chunks   <- .bounds_to_var_chunks(lb_full, ub_full)
    n_var_chunks <- length(var_chunks)

    cat("VAR\n", n_var, " ", n_var_chunks, "\n", sep = "", file = con)
    for (vc in var_chunks) {
        cat(vc$cone, " ", vc$count, "\n", sep = "", file = con)
    }
    cat("\n", file = con)

    # -- INT -------------------------------------------------------------------
    # CBF uses 0-based variable indices; R uses 1-based -> subtract 1.
    if (!is.null(types_v)) {
        int_idx <- which(types_v == "I") - 1L
        if (length(int_idx) > 0L) {
            cat("INT\n", length(int_idx), "\n",
                paste(int_idx, collapse = "\n"), "\n\n",
                sep = "", file = con)
        }
    }

    # -- CON + ACOORD + BCOORD -------------------------------------------------
    if (!is.null(cons) && !inherits(cons, "NO_constraint")) {
        flat <- .flatten_all_constraints(cons, pow_tables)
        if (length(flat) > 0L) {
            .write_con_section(flat, con)
            .write_acoord_section(flat, con)
            .write_bcoord_section(flat, con)
        }
    }

    # -- OBJACOORD + OBJBCOORD ------------------------------------------------
    .write_objective_sections(op[["objective"]], con)

    invisible(NULL)
}


# ---------------------------------------------------------------------------
# Variable bounds -> VAR cone chunks
# ---------------------------------------------------------------------------

#' Convert variable bounds to a list of CBF VAR cone chunks.
#'
#' Each "chunk" groups consecutive variables that share the same conic domain.
#' Run-length encoding keeps the VAR section compact.
#'
#' @section Bound -> cone mapping:
#' \describe{
#'   \item{\code{lb=0, ub=+Inf}}{  \code{L+} (positive orthant)}
#'   \item{\code{lb=-Inf, ub=0}}{ \code{L-} (negative orthant)}
#'   \item{\code{lb=0, ub=0}}{    \code{L=} (fixpoint zero)}
#'   \item{anything else}{         \code{F.}  (free)}
#' }
#'
#' @param lb Numeric vector of lower bounds (length = number of variables).
#' @param ub Numeric vector of upper bounds (same length).
#' @return List of \code{list(cone, count)} pairs.
#' @keywords internal
.bounds_to_var_chunks <- function(lb, ub) {
    n <- length(lb)
    if (n == 0L) return(list())

    .cone_for_bounds <- function(l, u) {
        fin_l <- is.finite(l); fin_u <- is.finite(u)
        if ( fin_l && l == 0 && !fin_u)              return("L+")   # [0,+Inf)
        if (!fin_l &&  fin_u && u == 0)              return("L-")   # (-Inf,0]
        if ( fin_l && l == 0 &&  fin_u && u == 0)   return("L=")   # {0}
        return("F")
    }

    ctypes <- mapply(.cone_for_bounds, lb, ub, USE.NAMES = FALSE)

    # Run-length encode: group consecutive variables with the same cone.
    chunks    <- list()
    cur_cone  <- ctypes[1L]
    cur_count <- 1L

    flush_chunk <- function() {
        chunks[[length(chunks) + 1L]] <<-
            list(cone = cur_cone, count = cur_count)
    }

    for (k in seq_len(n - 1L) + 1L) {
        if (ctypes[k] == cur_cone) {
            cur_count <- cur_count + 1L
        } else {
            flush_chunk()
            cur_cone  <- ctypes[k]
            cur_count <- 1L
        }
    }
    flush_chunk()
    chunks
}


# ---------------------------------------------------------------------------
# Constraint flattening
# ---------------------------------------------------------------------------

#' Flatten all ROI constraints into a uniform list of chunk records.
#'
#' Supports \code{L_constraint} and \code{C_constraint} objects, including
#' combined constraint objects created by \code{c()} or \code{rbind()}.
#'
#' Each element of the returned list is a list with fields:
#' \describe{
#'   \item{L}{slam \code{simple_triplet_matrix}: the constraint-matrix rows
#'     for this chunk.}
#'   \item{rhs}{Numeric vector: ROI right-hand sides for this chunk.}
#'   \item{cbf_cone}{Character: CBF cone token (e.g. \code{"Q."}).}
#' }
#'
#' @param cons       ROI constraint object (possibly compound).
#' @param pow_tables List with \code{primal} and \code{dual} sub-lists of
#'   alpha vectors, built by \code{.collect_powcone_tables()}.
#' @return List of chunk records.
#' @keywords internal
.flatten_all_constraints <- function(cons, pow_tables) {
    # Handle a plain R list wrapping multiple constraint objects.
    if (is.list(cons) &&
        !inherits(cons, c("L_constraint", "C_constraint", "Q_constraint"))) {
        return(unlist(
            lapply(cons, .flatten_all_constraints, pow_tables = pow_tables),
            recursive = FALSE
        ))
    }
    if (inherits(cons, "L_constraint"))
        return(.flatten_L_constraint(cons))
    if (inherits(cons, "C_constraint"))
        return(.flatten_C_constraint(cons, pow_tables))
    warning("Constraint class '", class(cons)[1L],
            "' is not supported by write_cbf; skipping.")
    list()
}


#' Flatten one L_constraint into groups of consecutive equal-direction rows.
#'
#' Consecutive rows with the same inequality direction (\code{">="}, etc.)
#' are grouped into a single chunk so they map to one CBF cone line.
#'
#' @param lc  An \code{L_constraint} object.
#' @return List of chunk records (see \code{.flatten_all_constraints}).
#' @keywords internal
.flatten_L_constraint <- function(lc) {
    L_mat <- lc$L
    if (!inherits(L_mat, "simple_triplet_matrix"))
        L_mat <- slam::as.simple_triplet_matrix(L_mat)
    dirs <- lc$dir
    rhs  <- lc$rhs
    n    <- nrow(L_mat)

    # ROI direction -> CBF cone name:
    #   ">="  ->  L+   A x >= rhs  <=>  A x - rhs >= 0  <=>  A x + (-rhs) in L+
    #   "<="  ->  L-   A x <= rhs  <=>  A x + (-rhs) in L-
    #   "=="  ->  L=   A x = rhs  <=>  A x + (-rhs) in L=
    dir_to_cone <- c(">=" = "L+", "<=" = "L-", "==" = "L=")

    result    <- list()
    cur_dir   <- dirs[1L]
    cur_start <- 1L

    flush_chunk <- function(start, end, d) {
        rows <- start:end
        result[[length(result) + 1L]] <<- list(
            L        = L_mat[rows, , drop = FALSE],
            rhs      = rhs[rows],
            cbf_cone = unname(dir_to_cone[d])
        )
    }

    for (k in seq_len(n - 1L) + 1L) {
        if (dirs[k] != cur_dir) {
            flush_chunk(cur_start, k - 1L, cur_dir)
            cur_dir   <- dirs[k]
            cur_start <- k
        }
    }
    flush_chunk(cur_start, n, cur_dir)
    result
}


#' Flatten one C_constraint into per-cone chunk records.
#'
#' Groups rows by cone ID from the combined \code{cones} object (ROI >= 1.0
#' new-style API: class \code{"cone"} with \code{$cone}, \code{$id}, and
#' optional \code{$params} fields).  Each unique ID produces one chunk.
#'
#' @param cc         A \code{C_constraint} object.
#' @param pow_tables Power-cone lookup tables (see \code{.collect_powcone_tables}).
#' @return List of chunk records.
#' @keywords internal
.flatten_C_constraint <- function(cc, pow_tables) {
    L_mat <- cc$L
    if (!inherits(L_mat, "simple_triplet_matrix"))
        L_mat <- slam::as.simple_triplet_matrix(L_mat)
    cones_obj <- cc$cones   # combined cone object (class "cone")
    rhs       <- cc$rhs

    # Group rows by cone ID; each unique ID = one CBF cone chunk.
    unique_ids <- unique(cones_obj$id)

    result <- list()
    for (gid in unique_ids) {
        rows      <- which(cones_obj$id == gid)
        type_code <- cones_obj$cone[rows[1L]]
        params    <- if (!is.null(cones_obj$params))
                         cones_obj$params[[as.character(gid)]]
                     else NULL

        cbf_name <- .roi_cone_type_to_cbf_name(type_code, params, pow_tables)

        result[[length(result) + 1L]] <- list(
            L        = -L_mat[rows, , drop = FALSE],
            rhs      = -rhs[rows],
            cbf_cone = cbf_name
        )
    }
    result
}


# ---------------------------------------------------------------------------
# ROI cone -> CBF cone name
# ---------------------------------------------------------------------------

#' Map an ROI cone type code to its CBF cone name string.
#'
#' ROI >= 1.0 uses a unified \code{"cone"} class; the cone family is encoded
#' as an integer in \code{$cone}.  Known codes:
#' 1 = K_zero, 2 = K_lin (nonneg), 3 = K_soc, 4 = K_psd,
#' 5 = K_expp, 6 = K_expd, 7 = K_powp, 8 = K_powd.
#'
#' For power cones (\code{type_code} 7 or 8), \code{params$a} holds the
#' scalar alpha; the 0-based POWCONES table index is embedded in the name.
#'
#' @param type_code  Integer cone type code from \code{cones_obj$cone}.
#' @param params     Named list of parameters for this cone group
#'   (\code{cones_obj$params[[id_str]]}), or \code{NULL}.
#' @param pow_tables List with \code{primal} and \code{dual} alpha-vector
#'   lists (from \code{.collect_powcone_tables()}).
#' @return Character CBF cone token.
#' @keywords internal
.roi_cone_type_to_cbf_name <- function(type_code, params, pow_tables) {
    switch(as.character(type_code),
        "1" = "L=",       # K_zero
        "2" = "L+",       # K_lin (nonneg)
        "3" = "Q",        # K_soc
        "4" = "SVECPSD",  # K_psd
        "5" = "EXP",      # K_expp
        "6" = "EXP*",     # K_expd
        "7" = {           # K_powp
            alpha <- c(params$a)
            k     <- .find_powcone_idx(alpha, pow_tables$primal)
            paste0("@", k, ":POW")
        },
        "8" = {           # K_powd
            alpha <- c(params$a)
            k     <- .find_powcone_idx(alpha, pow_tables$dual)
            paste0("@", k, ":POW*")
        },
        stop("Cannot map ROI cone type code ", type_code, " to a CBF cone name.")
    )
}


#' Find a power-cone alpha vector in a lookup table, returning its 0-based index.
#'
#' The table is guaranteed to contain \code{alpha} because
#' \code{.collect_powcone_tables()} pre-scans all constraints.
#'
#' @param alpha Numeric vector. Parameter vector to look up.
#' @param table List of numeric vectors (the lookup table).
#' @return Integer. 0-based position in \code{table}.
#' @keywords internal
.find_powcone_idx <- function(alpha, table) {
    for (k in seq_along(table)) {
        if (isTRUE(all.equal(table[[k]], alpha, tolerance = 1e-15)))
            return(k - 1L)   # 0-based
    }
    stop("Power-cone alpha vector not found in lookup table. ",
         "This should not happen -- please report as a bug.")
}


#' Collect unique power-cone alpha vectors from all C_constraints.
#'
#' Returns a list with elements \code{primal} and \code{dual}, each a list
#' of unique alpha vectors for \code{K_powcone} / \code{K_powconedual} cones
#' found in the constraints.  Must be called before flattening constraints
#' so that the POWCONES section can be written first.
#'
#' @param cons ROI constraint object (possibly \code{NULL} or compound).
#' @return List with \code{primal} and \code{dual} sub-lists.
#' @keywords internal
.collect_powcone_tables <- function(cons) {
    tables <- list(primal = list(), dual = list())
    if (is.null(cons) || inherits(cons, "NO_constraint")) return(tables)

    all_cone_objs <- .extract_all_cones(cons)
    for (cones_obj in all_cone_objs) {
        if (!inherits(cones_obj, "cone")) next
        pow_rows <- which(cones_obj$cone %in% c(7L, 8L))   # K_powp, K_powd
        if (length(pow_rows) == 0L) next

        for (gid in unique(cones_obj$id[pow_rows])) {
            type_code <- cones_obj$cone[which(cones_obj$id == gid)[1L]]
            params    <- if (!is.null(cones_obj$params))
                             cones_obj$params[[as.character(gid)]]
                         else NULL
            if (is.null(params)) next

            alpha  <- c(params$a)
            sub_nm <- if (type_code == 7L) "primal" else "dual"
            already_in <- any(sapply(
                tables[[sub_nm]],
                function(a) isTRUE(all.equal(a, alpha, tolerance = 1e-15))
            ))
            if (!already_in)
                tables[[sub_nm]] <- c(tables[[sub_nm]], list(alpha))
        }
    }
    tables
}


#' Recursively collect all combined cone objects from a constraint object.
#'
#' Returns a list of \code{"cone"} objects (one per \code{C_constraint}).
#' @keywords internal
.extract_all_cones <- function(cons) {
    if (is.list(cons) &&
        !inherits(cons, c("L_constraint", "C_constraint"))) {
        return(unlist(lapply(cons, .extract_all_cones), recursive = FALSE))
    }
    if (inherits(cons, "C_constraint")) {
        return(list(cons$cones))   # combined "cone" object (new ROI API)
    }
    list()
}


# ---------------------------------------------------------------------------
# Section writers
# ---------------------------------------------------------------------------

#' Write the CON section: cone chunk declarations for scalar constraints.
#'
#' Format (CBF spec Sec.B):
#' \preformatted{
#' CON
#' n_total  n_chunks
#' CONE_NAME_1  count_1
#' ...
#' CONE_NAME_k  count_k
#' }
#'
#' @param flat List of chunk records from \code{.flatten_all_constraints}.
#' @param con  Writable file connection.
#' @keywords internal
.write_con_section <- function(flat, con) {
    n_total  <- sum(sapply(flat, function(x) nrow(x$L)))
    n_chunks <- length(flat)
    cat("CON\n", n_total, " ", n_chunks, "\n", sep = "", file = con)
    for (chunk in flat) {
        cat(chunk$cbf_cone, " ", nrow(chunk$L), "\n", sep = "", file = con)
    }
    cat("\n", file = con)
}


#' Write the ACOORD section: sparse constraint-matrix triplets.
#'
#' Format (CBF spec Sec.B):
#' \preformatted{
#' ACOORD
#' count
#' con_idx  var_idx  value     <- 0-based indices
#' ...
#' }
#'
#' CBF uses 0-based indices; R/slam uses 1-based.  The offset \code{-1} is
#' applied to both the constraint-row index and the variable-column index.
#'
#' @param flat List of chunk records.
#' @param con  Writable file connection.
#' @keywords internal
.write_acoord_section <- function(flat, con) {
    # Collect all triplet lines first so we know the total count.
    all_lines  <- character(0L)
    row_offset <- 0L

    for (chunk in flat) {
        L_m <- chunk$L
        if (!inherits(L_m, "simple_triplet_matrix"))
            L_m <- slam::as.simple_triplet_matrix(L_m)
        if (length(L_m$i) > 0L) {
            ci        <- L_m$i + row_offset - 1L   # 1-based -> 0-based
            vi        <- L_m$j - 1L                # 1-based -> 0-based
            vv        <- L_m$v
            all_lines <- c(all_lines, sprintf("%d %d %.15g", ci, vi, vv))
        }
        row_offset <- row_offset + nrow(L_m)
    }

    if (length(all_lines) > 0L) {
        cat("ACOORD\n", length(all_lines), "\n", sep = "", file = con)
        cat(all_lines, sep = "\n", file = con)
        cat("\n\n", file = con)
    }
}


#' Write the BCOORD section: constraint constant-term offsets.
#'
#' Format (CBF spec Sec.B):
#' \preformatted{
#' BCOORD
#' count
#' con_idx  value     <- 0-based con_idx; only non-zero values are written
#' }
#'
#' @section Sign convention:
#' CBF encodes \eqn{Ax + b \in K}.  ROI stores \eqn{Ax - \text{rhs} \in K}.
#' Therefore \eqn{b_{\text{CBF}} = -\text{rhs}_{\text{ROI}}}.
#'
#' @param flat List of chunk records.
#' @param con  Writable file connection.
#' @keywords internal
.write_bcoord_section <- function(flat, con) {
    blines     <- character(0L)
    row_offset <- 0L

    for (chunk in flat) {
        rhs_sub <- chunk$rhs
        n_rows  <- length(rhs_sub)
        nz      <- which(rhs_sub != 0)

        if (length(nz) > 0L) {
            ci     <- row_offset + nz - 1L   # 0-based
            b_cbf  <- -rhs_sub[nz]           # b = -rhs (sign flip)
            blines <- c(blines, sprintf("%d %.15g", ci, b_cbf))
        }
        row_offset <- row_offset + n_rows
    }

    if (length(blines) > 0L) {
        cat("BCOORD\n", length(blines), "\n", sep = "", file = con)
        cat(blines, sep = "\n", file = con)
        cat("\n\n", file = con)
    }
}


#' Write OBJACOORD and OBJBCOORD sections for the objective.
#'
#' \describe{
#'   \item{OBJACOORD}{Sparse linear-objective coefficients. Format:
#'     \preformatted{OBJACOORD
#' count
#' var_idx  value    <- 0-based var_idx; only non-zero values}}
#'   \item{OBJBCOORD}{Scalar constant. Format:
#'     \preformatted{OBJBCOORD
#' value}}
#' }
#'
#' @param obj ROI objective object (must be an \code{L_objective}).
#' @param con Writable file connection.
#' @keywords internal
.write_objective_sections <- function(obj, con) {
    if (!inherits(obj, "L_objective"))
        stop("write_cbf currently supports only L_objective.")

    trm   <- stats::terms(obj)
    coef  <- as.vector(as.matrix(trm$L))
    const <- if (!is.null(obj$constant)) as.numeric(obj$constant) else 0

    # OBJACOORD: non-zero coefficients only.
    nz <- which(coef != 0)
    if (length(nz) > 0L) {
        lines <- sprintf("%d %.15g", nz - 1L, coef[nz])   # 0-based idx
        cat("OBJACOORD\n", length(lines), "\n", sep = "", file = con)
        cat(lines, sep = "\n", file = con)
        cat("\n\n", file = con)
    }

    # OBJBCOORD: only if non-zero (no HEADER line per CBF spec Sec.B).
    if (!is.null(const) && const != 0) {
        cat("OBJBCOORD\n", sprintf("%.15g\n\n", const), sep = "", file = con)
    }
}


#' Write a POWCONES or POW*CONES section.
#'
#' Format (CBF spec Sec.B):
#' \preformatted{
#' POWCONES
#' N  total_params
#' k_0           <- chunk header: length of alpha for cone 0
#' alpha_0[0]
#' ...
#' alpha_0[k_0-1]
#' k_1
#' ...
#' }
#'
#' @param table   List of numeric alpha vectors (one per parametric cone).
#' @param keyword Character. Either \code{"POWCONES"} or \code{"POW*CONES"}.
#' @param con     Writable file connection.
#' @keywords internal
.write_powcones_section <- function(table, keyword, con) {
    if (length(table) == 0L) return(invisible(NULL))
    total_params <- sum(sapply(table, length))
    cat(keyword, "\n", length(table), " ", total_params, "\n",
        sep = "", file = con)
    for (alpha in table) {
        cat(length(alpha), "\n", sep = "", file = con)
        cat(sprintf("%.15g", alpha), sep = "\n", file = con)
        cat("\n", file = con)
    }
    cat("\n", file = con)
}


# ---------------------------------------------------------------------------
# Utility functions
# ---------------------------------------------------------------------------

#' Get the total number of rows for a combined ROI cone object.
#'
#' ROI >= 1.0 uses a unified \code{"cone"} class; the dimension is simply
#' the length of the \code{$cone} (or \code{$id}) vector.
#'
#' @param cone A \code{"cone"} object returned by \code{K_soc()}, etc.
#' @return Integer total row count.
#' @keywords internal
.roi_cone_dim <- function(cone) {
    if (inherits(cone, "cone")) return(length(cone$cone))
    stop("Cannot determine dimension of cone of class '",
         class(cone)[1L], "'.")
}
