# Transpose a named list of equal-length vectors into a list of named lists.
# E.g., list(a=1:3, b=4:6) -> list(list(a=1,b=4), list(a=2,b=5), list(a=3,b=6))
list_transpose = function(x) {
    nms = names(x)
    n = length(x[[1]])
    lapply(seq_len(n), function(i) {
        out = lapply(x, `[[`, i)
        names(out) = nms
        out
    })
}

#' Set Up Model Adjustment Specifications
#'
#' Takes a set of new sampling statements, which can be parametrized by other
#' arguments, data frames, or lists, and creates an \code{adjustr_spec} object
#' suitable for use in \code{\link{adjust_weights}}.
#'
#' @param ... Model specification. Each argument can either be a formula,
#'   a named vector, data frames, or lists.
#'
#'   Formula arguments provide new sampling statements to replace their
#'   counterparts in the original Stan model. All such formulas must be of the
#'   form \code{variable ~ distribution(parameters)}, where \code{variable} and
#'   \code{parameters} are Stan data variables or parameters, or are provided by
#'   other arguments to this function (see below), and where \code{distribution}
#'   matches one of the univariate
#'   \href{https://mc-stan.org/docs/2_22/functions-reference/conventions-for-probability-functions.html}{Stan distributions}.
#'   Arithmetic expressions of parameters are also allowed, but care must be
#'   taken with multivariate parameter arguments.  Since specifications are
#'   passed as formulas, R's arithmetic operators are used, not Stan's. As a
#'   result, matrix and elementwise multipilcation in Stan sampling statements may
#'   not be interpreted correctly. Moving these computations out of sampling
#'   statements and into local variables will ensure correct results.
#'
#'   For named vector arguments, each entry of the vector will be substituted
#'   into the corresponding parameter in the sampling statements. For data
#'   frames, each entry in each column will be substituted into the corresponding
#'   parameter in the sampling statements.
#'
#'   List arguments are coerced to data frames. They can either be lists of named
#'   vectors, or lists of lists of single-element named vectors.
#'
#'   The lengths of all parameter arguments must be consistent.  Named vectors
#'   can have length 1 or must have length equal to the number of rows in all
#'   data frame arguments and the length of list arguments.
#'
#' @return An object of class \code{adjustr_spec}, which is essentially a list
#' with two elements: \code{samp}, which is a list of sampling formulas, and
#' \code{params}, which is a list of lists of parameters. Core
#' \link[=filter.adjustr_spec]{dplyr verbs} which don't involve grouping
#' (\code{\link[dplyr]{filter}}, \code{\link[dplyr]{arrange}},
#' \code{\link[dplyr]{mutate}}, \code{\link[dplyr]{select}},
#' \code{\link[dplyr]{rename}}, and \code{\link[dplyr]{slice}}) are
#' supported and operate on the underlying table of specification parameters.
#'
#' @param x An \code{adjustr_spec} object (for \code{print} and \code{length} methods).
#'
#' @seealso \code{\link{adjust_weights}}, \code{\link{summarize.adjustr_weighted}}, \code{\link{spec_plot}}
#'
#' @examples
#' make_spec(eta ~ cauchy(0, 1))
#'
#' make_spec(eta ~ student_t(df, 0, 1), df=1:10)
#'
#' params = tidyr::crossing(df=1:10, infl=c(1, 1.5, 2))
#' make_spec(eta ~ student_t(df, 0, 1),
#'           y ~ normal(theta, infl*sigma),
#'           params)
#'
#' @export
make_spec = function(...) {
    args = dots_list(..., .check_assign=TRUE)

    spec_samp = Filter(is_formula, args)
    if (length(spec_samp) == 0) warning("No sampling statements provided.")
    nms = names(args)
    spec_params = Map(function(value, name) {
        if (!is.numeric(name) && name != "") { # named arguments are preserved as is
            list2(!!name := value)
        } else if (is.data.frame(value)) {
            as.list(value)
        } else if (is_list(value)) {
            if (is_list(value[[1]])) {
                tryCatch(as.list(do.call(bind_rows, value)), error = function(e) {
                    stop("List-of-list arguments must be coercible to data frames.")
                })
            } else if (is_vector(value[[1]])) {
                tryCatch(do.call(bind_cols, value), error = function(e) {
                    stop("List-of-vector arguments must be coercible to data frames.")
                })
                value
            } else {
                stop("List arguments must be lists of lists or lists of ",
                    "vectors, and coercible to data frames.")
            }
        } else if (is_formula(value)) {
            NULL
        } else {
            stop("Arguments must be formulas, named vectors, data frames, ",
                "or lists. Use ?make_spec to see documentations.")
        }
    }, args, nms)
    spec_params = Filter(Negate(is.null), spec_params) # remove NULLs
    spec_params = do.call(c, unname(spec_params)) # flatten one level without name mangling
    spec_params = as_tibble(spec_params)

    if (any(is.na(spec_params)))
        stop("NAs found. Check input parameters and format.")

    spec_obj = structure(list(
        samp = spec_samp,
        params = if (nrow(spec_params) > 0)
                list_transpose(as.list(spec_params))
            else
                list(list())
    ),  class="adjustr_spec")
    spec_obj
}

# GENERIC FUNCTIONS for `adjustr_spec`
is.adjustr_spec = function(x) inherits(x, "adjustr_spec")
#' @rdname make_spec
#' @export
print.adjustr_spec = function(x, ...) {
    cat("Sampling specifications:\n")
    for (s in x$samp) print(s)
    if (length(x$params[[1]]) > 0) {
        cat("\nSpecification parameters:\n")
        df = as.data.frame(do.call(rbind, x$params))
        print(df, row.names=FALSE, max=15*ncol(df))
    }
    invisible(x)
}
#' @rdname make_spec
#' @export
length.adjustr_spec = function(x) length(x$params)

#' Convert an \code{adjustr_spec} Object Into a Data Frame
#'
#' Returns the data frame of specification parameters, with added columns of
#' the form \code{.samp_1}, \code{.samp_2}, ... for each sampling statement
#' (or just \code{.samp} if there is only one sampling statement).
#'
#' @param x the \code{adjustr_spec} object
#' @param ... additional arguments to underlying method
#'
#' @return A data frame with one row per specification and columns for each
#'   parameter, plus \code{.samp} (or \code{.samp_1}, \code{.samp_2}, ...)
#'   columns containing the sampling statement formulas.
#'
#' @export
as.data.frame.adjustr_spec = function(x, ...) {
    if (length(x$params[[1]]) == 0) {
        params_df = as.data.frame(matrix(nrow=1, ncol=0))
    } else {
        params_df = do.call(bind_rows, x$params) %>%
            as_tibble %>%
            as.data.frame
    }

    n_samp = length(x$samp)
    if (n_samp == 1) {
        params_df$.samp = format(x$samp[[1]])
    } else {
        for (i in 1:n_samp) {
            colname = paste0(".samp_", i)
            params_df[colname] = format(x$samp[[i]])
        }
    }

    params_df
}


#' \code{dplyr} Methods for \code{adjustr_spec} Objects
#'
#' Core \code{\link[dplyr]{dplyr}} verbs which don't involve grouping
#'  (\code{\link[dplyr]{filter}}, \code{\link[dplyr]{arrange}},
#'   \code{\link[dplyr]{mutate}}, \code{\link[dplyr]{select}},
#'   \code{\link[dplyr]{rename}}, and \code{\link[dplyr]{slice}}) are
#' implemented and operate on the underlying table of specification parameters.
#'
#' @param .data the \code{adjustr_spec} object
#' @param ... additional arguments to underlying method
#' @param .preserve as in \code{filter} and \code{slice}
#' @name dplyr.adjustr_spec
#'
#' @return A modified \code{adjustr_spec} object.
#'
#' @examples
#' spec = make_spec(eta ~ student_t(df, 0, 1), df=1:10)
#'
#' arrange(spec, desc(df))
#' slice(spec, 4:7)
#'
NULL
# dplyr generics
dplyr_handler = function(dplyr_func, x, ...) {
    if (length(x$params[[1]]) == 0) return(x)
    x$params = do.call(bind_rows, x$params) %>%
        as_tibble %>%
        dplyr_func(...) %>%
        as.list %>%
        list_transpose()
    x
}

#' @exportS3Method dplyr::filter
#' @rdname dplyr.adjustr_spec
filter.adjustr_spec = function(.data, ..., .preserve=FALSE) {
    dplyr_handler(dplyr::filter, .data, ..., .preserve=.preserve)
}
#' @rdname dplyr.adjustr_spec
#' @export
arrange.adjustr_spec = function(.data, ...) {
    dplyr_handler(dplyr::arrange, .data, ...)
}
#' @rdname dplyr.adjustr_spec
#' @export
rename.adjustr_spec = function(.data, ...) {
    dplyr_handler(dplyr::rename, .data, ...)
}
#' @rdname dplyr.adjustr_spec
#' @export
select.adjustr_spec = function(.data, ...) {
    dplyr_handler(dplyr::select, .data, ...)
}
#' @rdname dplyr.adjustr_spec
#' @export
slice.adjustr_spec = function(.data, ..., .preserve=FALSE) {
    dplyr_handler(dplyr::slice, .data, ..., .preserve=.preserve)
}

