#pragma once

#include <optional>

#include "drake/common/symbolic/expression.h"
#include "drake/math/autodiff.h"
#include "drake/math/autodiff_gradient.h"

namespace drake {
namespace math {
namespace internal {

template <typename T>
using is_symbolic = std::is_same<T, symbolic::Expression>;

template <typename T>
inline constexpr bool is_symbolic_v = is_symbolic<T>::value;

template <typename T>
inline constexpr bool is_double_or_symbolic_v =
    std::disjunction<std::is_same<T, double>, is_symbolic<T>>::value;

template <typename T>
struct is_autodiff : std::false_type {};

template <int N>
struct is_autodiff<drake::AutoDiffd<N>> : std::true_type {};

template <typename T>
inline constexpr bool is_autodiff_v = is_autodiff<T>::value;
}  // namespace internal

/**
 * @anchor linear_solve_given_solver
 * @name solve linear system of equations with a given solver.
 * Solve linear system of equations A * x = b. Where A is an Eigen matrix of
 * double/AutoDiffScalar/symbolic::Expression, and b is an Eigen matrix of
 * double/AutoDiffScalar/symbolic::Expression.
 * Notice that if either A or b contains symbolic::Expression, then the other
 * has to contain symbolic::Expression.
 * This 3-argument version allows the user to re-use @p linear_solver when
 * @p b changes or the gradient of @p A changes. When either A or b contains
 * AutoDiffScalar, we use implicit function
 * theorem to find the gradient in x as ∂x/∂zᵢ = A⁻¹(∂b/∂zᵢ - ∂A/∂zᵢ * x) where
 * z is the variable we take gradient with.
 *
 * @note When both A and b are Eigen matrix of double, this function is almost
 * as fast as calling linear_solver.solve(b) directly. When either A or b
 * contains AutoDiffScalar, this function is a lot faster than first
 * instantiating the linear solver of AutoDiffScalar, and then solving the
 * equation with this autodiffable linear solver.
 * @tparam LinearSolver The type of linear solver, for example
 * Eigen::LLT<Eigen::Matrix2d>
 * @tparam DerivedA An Eigen Matrix.
 * @tparam DerivedB An Eigen Vector.
 * @param linear_solver The linear solver constructed with the double-version of
 * A.
 * @param A The matrix A.
 * @param b The vector b.
 *
 * Here is an example code.
 * @code{cc}
 * Eigen::Matrix<AutoDiffd<3>, 2, 2> A_ad;
 * // Set the value and gradient in A_ad with arbitrary values;
 * Eigen::Matrix2d A_val;
 * A_val << 1, 2, 3, 4;
 * // Gradient of A.col(0).
 * Eigen::Matrix<double, 2, 3> A0_gradient;
 * A0_gradient << 1, 2, 3, 4, 5, 6;
 * A_ad.col(0) = InitializeAutoDiff(A_val.col(0), A0_gradient);
 * // Gradient of A.col(1)
 * Eigen::Matrix<double, 2, 3> A1_gradient;
 * A1_gradient << 7, 8, 9, 10, 11, 12;
 * A_ad.col(1) = InitializeAutoDiff(A_val.col(1), A1_gradient);
 * // Set the value and gradient of b to arbitrary value.
 * const Eigen::Vector2d b_val(2, 3);
 * Eigen::Matrix<double, 2, 3> b_gradient;
 * b_gradient << 1, 3, 5, 7, 9, 11;
 * const auto b_ad = InitializeAutoDiff(b_val, b_gradient);
 * // Solve the linear system A_val * x_val = b_val.
 * Eigen::PartialPivLU<Eigen::Matrix2d> linear_solver(A_val);
 * const auto x_val = SolveLinearSystem(linear_solver, A_val, b_val);
 * // Solve the linear system A*x=b, together with the gradient.
 * // x_ad contains both the value of the solution A*x=b, together with its
 * // gradient.
 * const auto x_ad = SolveLinearSystem(linear_solver, A_ad, b_ad);
 * @endcode
 */

//@{
/**
 * Specialized when A and b are both double or symbolic::Expression matrices.
 * See @ref linear_solve_given_solver for more details.
 * Note that @p A is unused, as we already compute its factorization in @p
 * linear_solver. But we keep it here for consistency with the overloaded
 * function, where A is a matrix of AutoDiffScalar.
 */
template <typename LinearSolver, typename DerivedA, typename DerivedB>
typename std::enable_if<
    internal::is_double_or_symbolic_v<typename DerivedA::Scalar> &&
        internal::is_double_or_symbolic_v<typename DerivedB::Scalar> &&
        std::is_same_v<typename DerivedA::Scalar, typename DerivedB::Scalar>,
    Eigen::Matrix<typename DerivedA::Scalar, DerivedA::RowsAtCompileTime,
                  DerivedB::ColsAtCompileTime>>::type
SolveLinearSystem(const LinearSolver& linear_solver,
                  const Eigen::MatrixBase<DerivedA>& A,
                  const Eigen::MatrixBase<DerivedB>& b) {
  unused(A);
  return linear_solver.solve(b);
}

/**
 * Specialized when the matrix in linear_solver and b are both double or
 * symbolic::Expression matrices.
 * See @ref linear_solve_given_solver for more details.
 */
template <typename LinearSolver, typename DerivedB>
typename std::enable_if<
    internal::is_double_or_symbolic_v<
        typename LinearSolver::MatrixType::Scalar> &&
        internal::is_double_or_symbolic_v<typename DerivedB::Scalar> &&
        std::is_same_v<typename LinearSolver::MatrixType::Scalar,
                       typename DerivedB::Scalar>,
    Eigen::Matrix<typename LinearSolver::MatrixType::Scalar,
                  DerivedB::RowsAtCompileTime,
                  DerivedB::ColsAtCompileTime>>::type
SolveLinearSystem(const LinearSolver& linear_solver,
                  const Eigen::MatrixBase<DerivedB>& b) {
  return linear_solver.solve(b);
}

/**
 * Specialized the matrix in linear_solver is a double-valued matrix and b is
 * an AutoDiffScalar-valued matrix. See @ref linear_solve_given_solver for more
 * details.
 */
template <typename LinearSolver, typename DerivedB>
typename std::enable_if<
    std::is_same_v<typename LinearSolver::MatrixType::Scalar, double> &&
        internal::is_autodiff_v<typename DerivedB::Scalar>,
    Eigen::Matrix<typename DerivedB::Scalar, DerivedB::RowsAtCompileTime,
                  DerivedB::ColsAtCompileTime>>::type
SolveLinearSystem(const LinearSolver& linear_solver,
                  const Eigen::MatrixBase<DerivedB>& b) {
  const int num_z_b = GetDerivativeSize(b);
  if (num_z_b == 0) {
    return linear_solver.solve(ExtractValue(b))
        .template cast<typename DerivedB::Scalar>();
  }
  const Eigen::Matrix<double, DerivedB::RowsAtCompileTime,
                      DerivedB::ColsAtCompileTime>
      x_val = linear_solver.solve(ExtractValue(b));
  Eigen::Matrix<double, DerivedB::RowsAtCompileTime,
                DerivedB::Scalar::DerType::RowsAtCompileTime>
      grad(b.rows(), num_z_b);
  Eigen::Matrix<typename DerivedB::Scalar, DerivedB::RowsAtCompileTime,
                DerivedB::ColsAtCompileTime>
      x_ad(b.rows(), b.cols());
  for (int i = 0; i < b.cols(); ++i) {
    grad = ExtractGradient(b.col(i));
    // ∂x/∂zᵢ = A⁻¹*∂b/∂zᵢ
    // Before calling linear_solver.solve(grad.eval()), grad contains the
    // gradient ∂b/∂zᵢ. After calling this function grad contains ∂x/∂zᵢ. I call
    // grad.eval() to avoid aliasing issue.
    grad = linear_solver.solve(grad.eval());
    x_ad.col(i) = InitializeAutoDiff(x_val.col(i), grad);
  }
  return x_ad;
}

/**
 * Specialized when A is a double-valued matrix and b is an
 * AutoDiffScalar-valued matrix. See @ref linear_solve_given_solver for more
 * details. Note that @p A is unused, as we already compute its factorization in
 * @p linear_solver. But we keep it here for consistency with the overloaded
 * function, where A is a matrix of AutoDiffScalar.
 */
template <typename LinearSolver, typename DerivedA, typename DerivedB>
typename std::enable_if<
    std::is_same_v<typename DerivedA::Scalar, double> &&
        internal::is_autodiff_v<typename DerivedB::Scalar>,
    Eigen::Matrix<typename DerivedB::Scalar, DerivedA::RowsAtCompileTime,
                  DerivedB::ColsAtCompileTime>>::type
SolveLinearSystem(const LinearSolver& linear_solver,
                  const Eigen::MatrixBase<DerivedA>& A,
                  const Eigen::MatrixBase<DerivedB>& b) {
  unused(A);
  return SolveLinearSystem(linear_solver, b);
}

/**
 * Specialized when A is an AutoDiffScalar-valued matrix, and b can contain
 * either AutoDiffScalar or double. See @ref linear_solve_given_solver for more
 * details.
 */
template <typename LinearSolver, typename DerivedA, typename DerivedB>
typename std::enable_if<
    internal::is_autodiff_v<typename DerivedA::Scalar>,
    Eigen::Matrix<typename DerivedA::Scalar, DerivedA::RowsAtCompileTime,
                  DerivedB::ColsAtCompileTime>>::type
SolveLinearSystem(const LinearSolver& linear_solver,
                  const Eigen::MatrixBase<DerivedA>& A,
                  const Eigen::MatrixBase<DerivedB>& b) {
  // We differentiate A * x = b directly
  // A*∂x/∂zᵢ + ∂A/∂zᵢ * x = ∂b/∂zᵢ
  // So ∂x/∂zᵢ = A⁻¹(∂b/∂zᵢ - ∂A/∂zᵢ * x)
  // where I use z to denote the variable we take derivatives.

  // The size of the derivatives stored in A and b.
  const int num_z_A = GetDerivativeSize(A);
  int num_z_b = 0;
  if constexpr (!std::is_same_v<typename DerivedB::Scalar, double>) {
    num_z_b = GetDerivativeSize(b);
  }
  if (num_z_A == 0 && num_z_b == 0) {
    if constexpr (std::is_same_v<typename DerivedB::Scalar, double>) {
      return SolveLinearSystem(linear_solver, ExtractValue(A), b)
          .template cast<typename DerivedA::Scalar>();
    } else {
      return SolveLinearSystem(linear_solver, ExtractValue(A), ExtractValue(b))
          .template cast<typename DerivedA::Scalar>();
    }
  } else if (num_z_A == 0 && num_z_b != 0) {
    return SolveLinearSystem(linear_solver, ExtractValue(A), b);
  }
  // First compute the value of x.
  Eigen::Matrix<double, DerivedA::RowsAtCompileTime,
                DerivedB::ColsAtCompileTime>
      x_val;
  if constexpr (std::is_same_v<typename DerivedB::Scalar, double>) {
    x_val = linear_solver.solve(b);
  } else {
    const auto b_val = ExtractValue(b);
    x_val = linear_solver.solve(b_val);
  }

  // num_z_A != 0
  if (num_z_A != 0 && num_z_b != 0 && num_z_A != num_z_b) {
    throw std::runtime_error(
        fmt::format("SolveLinearSystem(): A contains derivatives for {} "
                    "variables, while b contains derivatives for {} variables",
                    num_z_A, num_z_b));
  }
  Eigen::Matrix<typename DerivedA::Scalar, DerivedA::RowsAtCompileTime,
                DerivedB::ColsAtCompileTime>
      x_ad(A.rows(), b.cols());
  // First sets the value of x_ad, and allocates the memory for the derivatives.
  // Note that I don't call InitializeAutoDiff since I have the
  // gradient of matrix x w.r.t scalar zᵢ, not the gradient of a vector x.col(j)
  // w.r.t a vector z.
  for (int i = 0; i < A.rows(); ++i) {
    for (int j = 0; j < b.cols(); ++j) {
      x_ad(i, j).value() = x_val(i, j);
      x_ad(i, j).derivatives() =
          Eigen::Matrix<double, DerivedA::Scalar::DerType::RowsAtCompileTime,
                        1>::Zero(num_z_A);
    }
  }

  Eigen::Matrix<double, DerivedA::RowsAtCompileTime,
                DerivedA::ColsAtCompileTime>
      dAdzi(A.rows(), A.cols());
  // This stores ∂b/∂zᵢ
  Eigen::Matrix<double, DerivedB::RowsAtCompileTime,
                DerivedB::ColsAtCompileTime>
      dbdzi(A.rows(), b.cols());
  // This stores ∂x/∂zᵢ
  Eigen::Matrix<double, DerivedA::RowsAtCompileTime,
                DerivedB::ColsAtCompileTime>
      dxdzi(A.rows(), b.cols());
  for (int i = 0; i < num_z_A; ++i) {
    dAdzi.setZero();
    dbdzi.setZero();
    // So ∂x/∂zᵢ = A⁻¹(∂b/∂zᵢ - ∂A/∂zᵢ * x)
    // First we need to store the matrix ∂A/∂zᵢ
    for (int j = 0; j < A.rows(); ++j) {
      for (int k = 0; k < A.cols(); ++k) {
        if (A(j, k).derivatives().rows() != 0) {
          dAdzi(j, k) = A(j, k).derivatives()(i);
        }
      }
    }
    if constexpr (!std::is_same_v<typename DerivedB::Scalar, double>) {
      for (int j = 0; j < b.rows(); ++j) {
        for (int k = 0; k < b.cols(); ++k) {
          if (b(j, k).derivatives().rows() != 0) {
            dbdzi(j, k) = b(j, k).derivatives()(i);
          }
        }
      }
    }
    dxdzi = linear_solver.solve(dbdzi - dAdzi * x_val);
    for (int j = 0; j < A.rows(); ++j) {
      for (int k = 0; k < b.cols(); ++k) {
        x_ad(j, k).derivatives()(i) = dxdzi(j, k);
      }
    }
  }
  return x_ad;
}

//@}

/**
 * @anchor get_linear_solver
 * @name Get linear solver
 *
 * Create the linear solver for a given matrix A, which will be used to solve
 * the linear system of equations A * x = b.
 *
 * The following table indicate the scalar type of the matrix in the returned
 * linear solver, depending on the scalar type in matrix A
 *
 * | A    | double |  ADS  | Expr |
 * |------|--------|-------|----- |
 * |solver| double | double| Expr |
 *
 * where ADS stands for Eigen::AutoDiffScalar, and Expr stands for
 * symbolic::Expression.
 * Here is the example code
 * @code{cc}
 * Eigen::Matrix2d A_val;
 * A_val << 1, 2, 2, 5;
 * Eigen::Vector2d b_val(3, 4);
 * const Eigen::Vector2d x_val =
 *   SolveLinearSystem(GetLinearSolver<Eigen::LLT>(A_val), A_val, b_val);
 * Eigen::Matrix<AutoDiffXd, 2, 2> A_ad;
 * A_ad(0, 0).value() = A_val(0, 0);
 * A_ad(0, 0).derivatives() = Eigen::Vector3d(1, 2, 3);
 * A_ad(0, 1).value() = A_val(0, 1);
 * A_ad(0, 1).derivatives() = Eigen::Vector3d(2, 3, 4);
 * A_ad(1, 0).value() = A_val(1, 0);
 * A_ad(1, 0).derivatives() = Eigen::Vector3d(3, 4, 5);
 * A_ad(1, 1).value() = A_val(1, 1);
 * A_ad(1, 1).derivatives() = Eigen::Vector3d(4, 5, 6);
 * // Solve A * x = b with A containing gradient.
 * const Eigen::Matrix<AutoDiffXd, 2, 1> x_ad1 =
 *   SolveLinearSystem(GetLinearSolver<Eigen::LLT>(A_ad), A_ad, b_val);
 * Eigen::Matrix<AutoDiffXd, 2, 1> b_ad;
 * b_ad(0).value() = b_val(0);
 * b_ad(0).derivatives() = Eigen::Vector3d(5, 6, 7);
 * b_ad(1).value() = b_val(1);
 * b_ad(1).derivatives() = Eigen::Vector3d(6, 7, 8);
 * // Solve A * x = b with b containing gradient.
 * const Eigen::Matrix<AutoDiffXd, 2, 1> x_ad2 =
 *   SolveLinearSystem(GetLinearSolver<Eigen::LLT>(A_val), A_val, b_ad);
 * // Solve A * x = b with both A and b containing gradient.
 * const Eigen::Matrix<AutoDiffXd, 2, 1> x_ad3 =
 *   SolveLinearSystem(GetLinearSolver<Eigen::LLT>(A_ad), A_ad, b_ad);
 * @endcode{cc}
 */

//@{

namespace internal {
/*
 * The return type of GetLinearSolver function. It is the type of the linear
 * solver. For example
 * LinearSolver<Eigen::LLT, Eigen::Matrix3d>::type is
 * Eigen::LLT<Eigen::Matrix3d>.
 * See @ref get_linear_solver for more details. When DerivedA::Scalar is
 * double or symbolic::Expression, the solver scalar type is the same as
 * DerivedA::Scalar; when DerivedA::Scalar is Eigen::AutoDiffScalar, the
 * solver scalar type is double.
 */
template <template <typename, int...> typename LinearSolverType,
          typename DerivedA>
using EigenLinearSolver = LinearSolverType<Eigen::Matrix<
    std::conditional_t<
        internal::is_double_or_symbolic_v<typename DerivedA::Scalar>,
        typename DerivedA::Scalar, double>,
    DerivedA::RowsAtCompileTime, DerivedA::ColsAtCompileTime, Eigen::ColMajor,
    DerivedA::MaxRowsAtCompileTime, DerivedA::MaxColsAtCompileTime>>;

/*
 * The most "promoted" of two scalar types; if they are the same, then that
 * type, if they are different and one is 'double', then whichever one is not
 * 'double'. Otherwise, the resulting type is 'void', indicating an error.
 */
template <typename ScalarA, typename ScalarB>
using Promoted = typename std::conditional_t<
    std::is_same_v<ScalarA, ScalarB>, ScalarA,
    std::conditional_t<
        std::is_same_v<double, ScalarA>, ScalarB,
        std::conditional_t<std::is_same_v<double, ScalarB>, ScalarA, void>>>;

/*
 * The type of the solution vector 'x' given DerivedA and DerivedB.
 */
template <typename DerivedA, typename DerivedB>
using Solution = typename Eigen::Matrix<
    Promoted<typename DerivedA::Scalar, typename DerivedB::Scalar>,
    DerivedA::RowsAtCompileTime, DerivedB::ColsAtCompileTime>;
}  // namespace internal

/**
 * Get the linear solver for a matrix A.
 * If A has scalar type of double or symbolic::Expressions, then the returned
 * linear solver will have the same scalar type. If A has scalar type of
 * Eigen::AutoDiffScalar, then the returned linear solver will have scalar type
 * of double. See @ref get_linear_solver for more details.
 */
template <template <typename, int...> typename LinearSolverType,
          typename DerivedA>
internal::EigenLinearSolver<LinearSolverType, DerivedA> GetLinearSolver(
    const Eigen::MatrixBase<DerivedA>& A) {
  if constexpr (internal::is_double_or_symbolic_v<typename DerivedA::Scalar>) {
    const internal::EigenLinearSolver<LinearSolverType, DerivedA> linear_solver(
        A);
    return linear_solver;
  } else {
    const auto A_val = ExtractValue(A);
    const internal::EigenLinearSolver<LinearSolverType, DerivedA> linear_solver(
        A_val);
    return linear_solver;
  }
}
//@}

/**
 * @anchor linear_solve
 * @name solve linear system of equations
 * Solve linear system of equations A * x = b. Where A is an Eigen matrix of
 * double/AutoDiffScalar/symbolic::Expression, and b is an Eigen matrix of
 * double/AutoDiffScalar/symbolic::Expression.
 * Note that when either A or b contains symbolic::Expression, the other has
 * to contain symbolic::Expression as well.
 * When either A or b contains AutoDiffScalar, we use implicit function theorem
 * to find the gradient in x as ∂x/∂zᵢ = A⁻¹(∂b/∂zᵢ - ∂A/∂zᵢ * x) where z is the
 * variable we take gradient with.
 *
 * The following table indicate the scalar type of x with A/b containing the
 * specified scalar type. The entries with NA are not supported.
 *
 * | b ＼ A | double | ADS | Expr |
 * |--------|--------|-----|----- |
 * | double | double | ADS |  NA  |
 * |   ADS  |   ADS  | ADS |  NA  |
 * |  Expr  |   NA   | NA  | Expr |
 *
 * where ADS stands for Eigen::AutoDiffScalar, and Expr stands for
 * symbolic::Expression.
 *
 * TODO(hongkai.dai): support one of A/b being a double matrix and the other
 * being a symbolic::Expression matrix.
 *
 * @note When both A and b are Eigen matrix of double, this function is almost
 * as fast as calling linear_solver.solve(b) directly; when either A or b
 * contains AutoDiffScalar, this function is a lot faster than first
 * instantiating the linear solver of AutoDiffScalar, and then solving the
 * equation with this autodiffable linear solver.
 * @tparam LinearSolverType The type of linear solver, for example
 * Eigen::LLT. Notice that this is just specifies the solver type (such as
 * Eigen::LLT), not the matrix type (like Eigen::LLT<Eigen::Matrix2d>).
 * All Eigen solvers we care about are templated on the matrix type. Some are
 * further templated on configuration ints. The int... will account for zero or
 * more of these ints, providing a common interface for both types of solvers.
 * @tparam DerivedA An Eigen Matrix.
 * @tparam DerivedB An Eigen Vector.
 * @param A The matrix A.
 * @param b The vector b.
 *
 * Here is an example code.
 * @code{cc}
 * Eigen::Matrix<AutoDiffd<3>, 2, 2> A_ad;
 * // Set the value and gradient in A_ad with arbitrary values;
 * Eigen::Matrix2d A_val;
 * A_val << 1, 2, 3, 4;
 * // Gradient of A.col(0).
 * Eigen::Matrix<double, 2, 3> A0_gradient;
 * A0_gradient << 1, 2, 3, 4, 5, 6;
 * A_ad.col(0) = InitializeAutoDiff(A_val.col(0), A0_gradient);
 * // Gradient of A.col(1)
 * Eigen::Matrix<double, 2, 3> A1_gradient;
 * A1_gradient << 7, 8, 9, 10, 11, 12;
 * A_ad.col(1) = InitializeAutoDiff(A_val.col(1), A1_gradient);
 * // Set the value and gradient of b to arbitrary value.
 * const Eigen::Vector2d b_val(2, 3);
 * Eigen::Matrix<double, 2, 3> b_gradient;
 * b_gradient << 1, 3, 5, 7, 9, 11;
 * const auto b_ad = InitializeAutoDiff(b_val, b_gradient);
 * // Solve the linear system A*x=b without the gradient.
 * const auto x_val = SolveLinearSystem<Eigen::PartialPivLU>(A_val, b_val);
 * // Solve the linear system A*x=b, together with the gradient.
 * // x_ad contains both the value of the solution A*x=b, together with its
 * // gradient.
 * const auto x_ad = SolveLinearSystem<Eigen::PartialPivLU>(A_ad, b_ad);
 * @endcode
 */

//@{
/**
 * Solves system A*x=b.  The supported combinations of scalar types are
 * summarized in the table above.  See @ref linear_solve for more details.
 */
template <template <typename, int...> typename LinearSolverType,
          typename DerivedA, typename DerivedB>
internal::Solution<DerivedA, DerivedB> SolveLinearSystem(
    const Eigen::MatrixBase<DerivedA>& A,
    const Eigen::MatrixBase<DerivedB>& b) {
  using ScalarA = typename DerivedA::Scalar;
  using ScalarB = typename DerivedB::Scalar;
  static_assert(
      std::is_same_v<ScalarA, ScalarB> || (!internal::is_symbolic_v<ScalarA> &&
                                           !internal::is_symbolic_v<ScalarB>),
      "Mixing symbolic and other types is not supported.");

  const auto linear_solver = GetLinearSolver<LinearSolverType>(A);
  return SolveLinearSystem(linear_solver, A, b);
}

//@}

/**
 * Solves a linear system of equations A*x=b.
 * Depending on the scalar types of A and b, the scalar type of x is summarized
 * in this table.
 * | b ＼ A | double | ADS | Expr |
 * |--------|--------|-----|----- |
 * | double | double | ADS |  NA  |
 * |   ADS  |   ADS  | ADS |  NA  |
 * |  Expr  |   NA   | NA  | Expr |
 *
 * where ADS stands for Eigen::AutoDiffScalar, and Expr stands for
 * symbolic::Expression.
 *
 * Using LinearSolver class is as fast as using Eigen's linear solver directly
 * when neither A nor b contains AutoDiffScalar. When either A or b contains
 * AutoDiffScalar, using LinearSolver is much faster than using Eigen's
 * autodiffable linear solver (for example
 * Eigen::LDLT<Eigen::Matrix<Eigen::AutoDiffScalar, 3, 3>>).
 *
 * Here is the example code
 * @code{cc}
 * Eigen::Matrix<AutoDiffXd, 2, 2> A;
 * A(0, 0).value() = 1;
 * A(0, 0).derivatives() = Eigen::Vector3d(1, 2, 3);
 * A(0, 1).value() = 2;
 * A(0, 1).derivatives() = Eigen::Vector3d(2, 3, 4);
 * A(1, 0).value() = 2;
 * A(1, 0).derivatives() = Eigen::Vector3d(3, 4, 5);
 * A(1, 1).value() = 5;
 * A(1, 1).derivatives() = Eigen::Vector3d(4, 5, 6);
 * LinearSolver<Eigen::LLT, Eigen::Matrix<AutoDiffXd, 2, 2>> solver(A);
 * Eigen::Matrix<AutoDiffXd, 2, 1> b;
 * b(0).value() = 2;
 * b(0).derivatives() = Eigen::Vector3d(1, 2, 3);
 * b(1).value() = 3;
 * b(1).derivatives() = Eigen::Vector3d(4, 5, 6);
 * Eigen::Matrix<AutoDiffXd, 2, 1> x = solver.Solve(b);
 * @endcode
 *
 */
template <template <typename, int...> typename LinearSolverType,
          typename DerivedA>
class LinearSolver {
 public:
  using SolverType = internal::EigenLinearSolver<LinearSolverType, DerivedA>;

  /**
   * The return type of Solve() function.
   * When both A and B contain the same scalar, and that scalar type
   * is double or symbolic::Expression, then the return type is
   * Eigen::Solve<Decomposition, DerivedB>, same as the return type of
   * Decomposition::solve() function in Eigen. This avoids unnecessary copies
   * and heap memory allocations if we were to evaluate
   * Eigen::Solve<Decomposition, DerivedB> to a concrete Eigen::Matrix type.
   * Otherwise we return an Eigen::Matrix with the same size as DerivedB
   * and proper scalar type.
   */
  template <typename DerivedB>
  using SolutionType = std::conditional_t<
      std::is_same_v<typename DerivedA::Scalar, typename DerivedB::Scalar> &&
          internal::is_double_or_symbolic_v<typename DerivedA::Scalar>,
      Eigen::Solve<SolverType, DerivedB>,
      internal::Solution<DerivedA, DerivedB>>;

  /** Default constructor. Constructs an empty linear solver. */
  LinearSolver() : eigen_linear_solver_() {}

  explicit LinearSolver(const Eigen::MatrixBase<DerivedA>& A)
      : eigen_linear_solver_{GetLinearSolver<LinearSolverType>(A)} {
    if constexpr (internal::is_autodiff_v<typename DerivedA::Scalar>) {
      A_.emplace(A);
    }
  }

  /**
   * Solves system A*x = b.
   * Return type is as described in the table above.
   */
  template <typename DerivedB>
  SolutionType<DerivedB> Solve(const Eigen::MatrixBase<DerivedB>& b) const {
    using ScalarA = typename DerivedA::Scalar;
    using ScalarB = typename DerivedB::Scalar;
    static_assert(std::is_same_v<ScalarA, ScalarB> ||
                      (!internal::is_symbolic_v<ScalarA> &&
                       !internal::is_symbolic_v<ScalarB>),
                  "Mixing symbolic and other types is not supported.");

    if constexpr (std::is_same_v<ScalarA, ScalarB> &&
                  !internal::is_autodiff_v<ScalarA>) {
      return eigen_linear_solver_.solve(b);
      // NOLINTNEXTLINE(readability/braces)
    } else if constexpr (std::is_same_v<ScalarA, double> &&
                         internal::is_autodiff_v<ScalarB>) {
      return SolveLinearSystem(eigen_linear_solver_, b);
      // NOLINTNEXTLINE(readability/braces)
    } else if constexpr (internal::is_autodiff_v<ScalarA>) {
      return SolveLinearSystem(eigen_linear_solver_, *A_, b);
    }
    DRAKE_UNREACHABLE();
  }

  /** Getter for the Eigen linear solver.
   * The scalar type in the Eigen linear solver depends on the scalar type in A
   * matrix, as shown in this table
   * |      A       | double |  ADS   | Expr |
   * |--------------|--------|--------|----- |
   * |linear_solver | double | double | Expr |
   *
   * where ADS stands for Eigen::AutoDiffScalar, Expr stands for
   * symbolic::Expression.
   *
   * Note that when A contains autodiffscalar, we only use the double version of
   * Eigen linear solver. By using implicit-function theorem with the
   * double-valued Eigen linear solver, we can compute the gradient of the
   * solution much faster than directly autodiffing the Eigen linear solver.
   *
   */
  const SolverType& eigen_linear_solver() const { return eigen_linear_solver_; }

 private:
  SolverType eigen_linear_solver_;
  std::optional<Eigen::Matrix<
      typename DerivedA::Scalar, DerivedA::RowsAtCompileTime,
      DerivedA::ColsAtCompileTime, Eigen::ColMajor,
      DerivedA::MaxRowsAtCompileTime, DerivedA::MaxColsAtCompileTime>>
      A_;
};

}  // namespace math
}  // namespace drake
