export obj, grad, grad!, objgrad, objgrad!, objcons, objcons!
export cons, cons!, cons_lin, cons_lin!, cons_nln, cons_nln!
export jth_con, jth_congrad, jth_congrad!, jth_sparse_congrad
export jac_structure!, jac_structure, jac_coord!, jac_coord
export jac, jprod, jprod!, jtprod, jtprod!, jac_op, jac_op!
export jac_lin_structure!, jac_lin_structure, jac_lin_coord!, jac_lin_coord
export jac_lin, jprod_lin, jprod_lin!, jtprod_lin, jtprod_lin!, jac_lin_op, jac_lin_op!
export jac_nln_structure!, jac_nln_structure, jac_nln_coord!, jac_nln_coord
export jac_nln, jprod_nln, jprod_nln!, jtprod_nln, jtprod_nln!, jac_nln_op, jac_nln_op!
export jth_hess_coord, jth_hess_coord!, jth_hess
export jth_hprod, jth_hprod!, ghjvprod, ghjvprod!
export hess_structure!, hess_structure, hess_coord!, hess_coord
export hess, hprod, hprod!, hess_op, hess_op!
export varscale, lagscale, conscale
export jac_dense!, hess_dense!

"""
    f = obj(nlp, x)

Evaluate ``f(x)``, the objective function of `nlp` at `x`.
"""
function obj end

"""
    g = grad(nlp, x)

Evaluate ``∇f(x)``, the gradient of the objective function at `x`.
This function is only available if `nlp.meta.grad_available` is set to `true`.
"""
function grad(nlp::AbstractNLPModel{T, S}, x::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  g = S(undef, get_nvar(nlp.meta))
  return grad!(nlp, x, g)
end

"""
    g = grad!(nlp, x, g)

Evaluate ``∇f(x)``, the gradient of the objective function at `x` in place.
This function is only available if `nlp.meta.grad_available` is set to `true`.
"""
function grad! end

"""
    c = cons(nlp, x)

Evaluate ``c(x)``, the constraints at `x`.
"""
function cons(nlp::AbstractNLPModel{T, S}, x::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  c = S(undef, get_ncon(nlp.meta))
  return cons!(nlp, x, c)
end

"""
    c = cons!(nlp, x, c)

Evaluate ``c(x)``, the constraints at `x` in place.
"""
function cons!(nlp::AbstractNLPModel, x::AbstractVector, cx::AbstractVector)
  @lencheck get_nvar(nlp.meta) x
  @lencheck get_ncon(nlp.meta) cx
  increment!(nlp, :neval_cons)
  if get_nlin(nlp.meta) > 0
    if get_nnln(nlp.meta) == 0
      cons_lin!(nlp, x, cx)
    else
      cons_lin!(nlp, x, view(cx, get_lin(nlp.meta)))
    end
  end
  if get_nnln(nlp.meta) > 0
    if get_nlin(nlp.meta) == 0
      cons_nln!(nlp, x, cx)
    else
      cons_nln!(nlp, x, view(cx, get_nln(nlp.meta)))
    end
  end
  return cx
end

"""
    c = cons_lin(nlp, x)

Evaluate the linear constraints at `x`.
"""
function cons_lin(nlp::AbstractNLPModel{T, S}, x::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  c = S(undef, get_nlin(nlp.meta))
  return cons_lin!(nlp, x, c)
end

"""
    c = cons_lin!(nlp, x, c)

Evaluate the linear constraints at `x` in place.
"""
function cons_lin! end

"""
    c = cons_nln(nlp, x)

Evaluate the nonlinear constraints at `x`.
"""
function cons_nln(nlp::AbstractNLPModel{T, S}, x::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  c = S(undef, get_nnln(nlp.meta))
  return cons_nln!(nlp, x, c)
end

"""
    c = cons_nln!(nlp, x, c)

Evaluate the nonlinear constraints at `x` in place.
"""
function cons_nln! end

function jth_con end

function jth_congrad(nlp::AbstractNLPModel{T, S}, x::AbstractVector, j::Integer) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  g = S(undef, get_nvar(nlp.meta))
  return jth_congrad!(nlp, x, j, g)
end

function jth_congrad! end

function jth_sparse_congrad end

"""
    f, c = objcons(nlp, x)

Evaluate ``f(x)`` and ``c(x)`` at `x`.
"""
function objcons(nlp::AbstractNLPModel{T, S}, x::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  c = S(undef, get_ncon(nlp.meta))
  return objcons!(nlp, x, c)
end

"""
    f, c = objcons!(nlp, x, c)

Evaluate ``f(x)`` and ``c(x)`` at `x`. `c` is overwritten with the value of ``c(x)``.
"""
function objcons!(nlp::AbstractNLPModel, x::AbstractVector, c::AbstractVector)
  @lencheck get_nvar(nlp.meta) x
  @lencheck get_ncon(nlp.meta) c
  f = obj(nlp, x)
  cons!(nlp, x, c)
  return f, c
end

"""
    f, g = objgrad(nlp, x)

Evaluate ``f(x)`` and ``∇f(x)`` at `x`.
This function is only available if `nlp.meta.grad_available` is set to `true`.
"""
function objgrad(nlp::AbstractNLPModel{T, S}, x::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  g = S(undef, get_nvar(nlp.meta))
  return objgrad!(nlp, x, g)
end

"""
    f, g = objgrad!(nlp, x, g)

Evaluate ``f(x)`` and ``∇f(x)`` at `x`. `g` is overwritten with the value of ``∇f(x)``.
This function is only available if `nlp.meta.grad_available` is set to `true`.
"""
function objgrad!(nlp::AbstractNLPModel, x::AbstractVector, g::AbstractVector)
  @lencheck get_nvar(nlp.meta) x g
  f = obj(nlp, x)
  grad!(nlp, x, g)
  return f, g
end

"""
    (rows,cols) = jac_structure(nlp.meta)

Return the structure of the constraints Jacobian in sparse coordinate format.
This function is only available when both `nlp.meta.jac_available` and `nlp.meta.sparse_jacobian` are set to `true`.
"""
function jac_structure(nlp::AbstractNLPModel)
  rows = Vector{Int}(undef, get_nnzj(nlp.meta))
  cols = Vector{Int}(undef, get_nnzj(nlp.meta))
  jac_structure!(nlp, rows, cols)
end

"""
    jac_structure!(nlp, rows, cols)

Return the structure of the constraints Jacobian in sparse coordinate format in place.
This function is only available when both `nlp.meta.jac_available` and `nlp.meta.sparse_jacobian` are set to `true`.
"""
function jac_structure!(
  nlp::AbstractNLPModel,
  rows::AbstractVector{T},
  cols::AbstractVector{T},
) where {T}
  @lencheck get_nnzj(nlp.meta) rows cols
  lin_ind = 1:(get_lin_nnzj(nlp.meta))
  if get_nlin(nlp.meta) > 0
    if get_nnln(nlp.meta) == 0
      jac_lin_structure!(nlp, rows, cols)
    else
      jac_lin_structure!(nlp, view(rows, lin_ind), view(cols, lin_ind))
      for i in lin_ind
        rows[i] += count(x < get_lin(nlp.meta)[rows[i]] for x in get_nln(nlp.meta))
      end
    end
  end
  if get_nnln(nlp.meta) > 0
    if get_nlin(nlp.meta) == 0
      jac_nln_structure!(nlp, rows, cols)
    else
      nln_ind = (get_lin_nnzj(nlp.meta) + 1):(get_lin_nnzj(nlp.meta) + get_nln_nnzj(nlp.meta))
      jac_nln_structure!(nlp, view(rows, nln_ind), view(cols, nln_ind))
      for i in nln_ind
        rows[i] += count(x < get_nln(nlp.meta)[rows[i]] for x in get_lin(nlp.meta))
      end
    end
  end
  return rows, cols
end

"""
    (rows,cols) = jac_lin_structure(nlp.meta)

Return the structure of the linear constraints Jacobian in sparse coordinate format.
This function is only available when both `nlp.meta.jac_available` and `nlp.meta.sparse_jacobian` are set to `true`.
"""
function jac_lin_structure(nlp::AbstractNLPModel)
  rows = Vector{Int}(undef, get_lin_nnzj(nlp.meta))
  cols = Vector{Int}(undef, get_lin_nnzj(nlp.meta))
  jac_lin_structure!(nlp, rows, cols)
end

"""
    jac_lin_structure!(nlp, rows, cols)

Return the structure of the linear constraints Jacobian in sparse coordinate format in place.
This function is only available when both `nlp.meta.jac_available` and `nlp.meta.sparse_jacobian` are set to `true`.
"""
function jac_lin_structure! end

"""
    (rows,cols) = jac_nln_structure(nlp.meta)

Return the structure of the nonlinear constraints Jacobian in sparse coordinate format.
This function is only available when both `nlp.meta.jac_available` and `nlp.meta.sparse_jacobian` are set to `true`.
"""
function jac_nln_structure(nlp::AbstractNLPModel)
  rows = Vector{Int}(undef, get_nln_nnzj(nlp.meta))
  cols = Vector{Int}(undef, get_nln_nnzj(nlp.meta))
  jac_nln_structure!(nlp, rows, cols)
end

"""
    jac_nln_structure!(nlp, rows, cols)

Return the structure of the nonlinear constraints Jacobian in sparse coordinate format in place.
This function is only available when both `nlp.meta.jac_available` and `nlp.meta.sparse_jacobian` are set to `true`.
"""
function jac_nln_structure! end

"""
    vals = jac_coord!(nlp, x, vals)

Evaluate ``J(x)``, the constraints Jacobian at `x` in sparse coordinate format, overwriting `vals`.
This function is only available when both `nlp.meta.jac_available` and `nlp.meta.sparse_jacobian` are set to `true`.
"""
function jac_coord!(nlp::AbstractNLPModel, x::AbstractVector, vals::AbstractVector)
  @lencheck get_nvar(nlp.meta) x
  @lencheck get_nnzj(nlp.meta) vals
  increment!(nlp, :neval_jac)
  if get_nlin(nlp.meta) > 0
    if get_nnln(nlp.meta) == 0
      jac_lin_coord!(nlp, x, vals)
    else
      lin_ind = 1:(get_lin_nnzj(nlp.meta))
      jac_lin_coord!(nlp, x, view(vals, lin_ind))
    end
  end
  if get_nnln(nlp.meta) > 0
    if get_nlin(nlp.meta) == 0
      jac_nln_coord!(nlp, x, vals)
    else
      nln_ind = (get_lin_nnzj(nlp.meta) + 1):(get_lin_nnzj(nlp.meta) + get_nln_nnzj(nlp.meta))
      jac_nln_coord!(nlp, x, view(vals, nln_ind))
    end
  end
  return vals
end

"""
    vals = jac_coord(nlp, x)

Evaluate ``J(x)``, the constraints Jacobian at `x` in sparse coordinate format.
This function is only available when both `nlp.meta.jac_available` and `nlp.meta.sparse_jacobian` are set to `true`.
"""
function jac_coord(nlp::AbstractNLPModel{T, S}, x::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  vals = S(undef, get_nnzj(nlp.meta))
  return jac_coord!(nlp, x, vals)
end

"""
    Jx = jac_dense!(nlp, x, Jx)

Evaluate ``J(x)``, the constraints Jacobian at `x` in dense format, overwriting `Jx`.
This function is only available when `nlp.meta.jac_available` is set to `true` and `nlp.meta.sparse_jacobian` is set to `false`.
"""
function jac_dense! end

"""
    Jx = jac(nlp, x)

Evaluate ``J(x)``, the constraints Jacobian at `x` as a sparse matrix.
This function is only available when both `nlp.meta.jac_available` and `nlp.meta.sparse_jacobian` are set to `true`.
"""
function jac(nlp::AbstractNLPModel, x::AbstractVector)
  @lencheck get_nvar(nlp.meta) x
  rows, cols = jac_structure(nlp)
  vals = jac_coord(nlp, x)
  sparse(rows, cols, vals, get_ncon(nlp.meta), get_nvar(nlp.meta))
end

"""
    vals = jac_lin_coord!(nlp, x, vals)

Evaluate ``J(x)``, the linear constraints Jacobian at `x` in sparse coordinate format, overwriting `vals`.
This function is only available when both `nlp.meta.jac_available` and `nlp.meta.sparse_jacobian` are set to `true`.
"""
function jac_lin_coord! end

"""
    vals = jac_lin_coord(nlp, x)

Evaluate ``J(x)``, the linear constraints Jacobian at `x` in sparse coordinate format.
This function is only available when both `nlp.meta.jac_available` and `nlp.meta.sparse_jacobian` are set to `true`.
"""
function jac_lin_coord(nlp::AbstractNLPModel{T, S}, x::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  vals = S(undef, get_lin_nnzj(nlp.meta))
  return jac_lin_coord!(nlp, x, vals)
end

"""
    Jx = jac_lin(nlp, x)

Evaluate ``J(x)``, the linear constraints Jacobian at `x` as a sparse matrix.
This function is only available when both `nlp.meta.jac_available` and `nlp.meta.sparse_jacobian` are set to `true`.
"""
function jac_lin(nlp::AbstractNLPModel, x::AbstractVector)
  @lencheck get_nvar(nlp.meta) x
  rows, cols = jac_lin_structure(nlp)
  vals = jac_lin_coord(nlp, x)
  sparse(rows, cols, vals, get_nlin(nlp.meta), get_nvar(nlp.meta))
end

"""
    vals = jac_nln_coord!(nlp, x, vals)

Evaluate ``J(x)``, the nonlinear constraints Jacobian at `x` in sparse coordinate format, overwriting `vals`.
This function is only available when both `nlp.meta.jac_available` and `nlp.meta.sparse_jacobian` are set to `true`.
"""
function jac_nln_coord! end

"""
    vals = jac_nln_coord(nlp, x)

Evaluate ``J(x)``, the nonlinear constraints Jacobian at `x` in sparse coordinate format.
This function is only available when both `nlp.meta.jac_available` and `nlp.meta.sparse_jacobian` are set to `true`.
"""
function jac_nln_coord(nlp::AbstractNLPModel{T, S}, x::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  vals = S(undef, get_nln_nnzj(nlp.meta))
  return jac_nln_coord!(nlp, x, vals)
end

"""
    Jx = jac_nln(nlp, x)

Evaluate ``J(x)``, the nonlinear constraints Jacobian at `x` as a sparse matrix.
This function is only available when both `nlp.meta.jac_available` and `nlp.meta.sparse_jacobian` are set to `true`.
"""
function jac_nln(nlp::AbstractNLPModel, x::AbstractVector)
  @lencheck get_nvar(nlp.meta) x
  rows, cols = jac_nln_structure(nlp)
  vals = jac_nln_coord(nlp, x)
  sparse(rows, cols, vals, get_nnln(nlp.meta), get_nvar(nlp.meta))
end

"""
    Jv = jprod(nlp, x, v)

Evaluate ``J(x)v``, the Jacobian-vector product at `x`.
This function is only available if `nlp.meta.jprod_available` is set to `true`.
"""
function jprod(nlp::AbstractNLPModel{T, S}, x::AbstractVector, v::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x v
  Jv = S(undef, get_ncon(nlp.meta))
  return jprod!(nlp, x, v, Jv)
end

"""
    Jv = jprod!(nlp, x, v, Jv)

Evaluate ``J(x)v``, the Jacobian-vector product at `x` in place.
This function is only available if `nlp.meta.jprod_available` is set to `true`.
"""
function jprod!(nlp::AbstractNLPModel, x::AbstractVector, v::AbstractVector, Jv::AbstractVector)
  @lencheck get_nvar(nlp.meta) x v
  @lencheck get_ncon(nlp.meta) Jv
  increment!(nlp, :neval_jprod)
  if get_nlin(nlp.meta) > 0
    if get_nnln(nlp.meta) == 0
      jprod_lin!(nlp, x, v, Jv)
    else
      jprod_lin!(nlp, x, v, view(Jv, get_lin(nlp.meta)))
    end
  end
  if get_nnln(nlp.meta) > 0
    if get_nlin(nlp.meta) == 0
      jprod_nln!(nlp, x, v, Jv)
    else
      jprod_nln!(nlp, x, v, view(Jv, get_nln(nlp.meta)))
    end
  end
  return Jv
end

"""
    Jv = jprod!(nlp, rows, cols, vals, v, Jv)

Evaluate ``J(x)v``, the Jacobian-vector product, where the Jacobian is given by `(rows, cols, vals)` in triplet format.
This function is only available if `nlp.meta.jprod_available` is set to `true`.
"""
function jprod!(
  nlp::AbstractNLPModel,
  rows::AbstractVector{<:Integer},
  cols::AbstractVector{<:Integer},
  vals::AbstractVector,
  v::AbstractVector,
  Jv::AbstractVector,
)
  @lencheck get_nnzj(nlp.meta) rows cols vals
  @lencheck get_nvar(nlp.meta) v
  @lencheck get_ncon(nlp.meta) Jv
  increment!(nlp, :neval_jprod)
  coo_prod!(rows, cols, vals, v, Jv)
end

"""
    Jv = jprod_lin(nlp, x, v)

Evaluate ``J(x)v``, the linear Jacobian-vector product at `x`.
This function is only available if `nlp.meta.jprod_available` is set to `true`.
"""
function jprod_lin(nlp::AbstractNLPModel{T, S}, x::AbstractVector, v::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x v
  Jv = S(undef, get_nlin(nlp.meta))
  return jprod_lin!(nlp, x, v, Jv)
end

"""
    Jv = jprod_lin!(nlp, x, v, Jv)

Evaluate ``J(x)v``, the linear Jacobian-vector product at `x` in place.
This function is only available if `nlp.meta.jprod_available` is set to `true`.
"""
function jprod_lin! end

"""
    Jv = jprod_lin!(nlp, rows, cols, vals, v, Jv)

Evaluate ``J(x)v``, the linear Jacobian-vector product, where the Jacobian is given by `(rows, cols, vals)` in triplet format.
This function is only available if `nlp.meta.jprod_available` is set to `true`.
"""
function jprod_lin!(
  nlp::AbstractNLPModel,
  rows::AbstractVector{<:Integer},
  cols::AbstractVector{<:Integer},
  vals::AbstractVector,
  v::AbstractVector,
  Jv::AbstractVector,
)
  @lencheck get_lin_nnzj(nlp.meta) rows cols vals
  @lencheck get_nvar(nlp.meta) v
  @lencheck get_nlin(nlp.meta) Jv
  increment!(nlp, :neval_jprod_lin)
  coo_prod!(rows, cols, vals, v, Jv)
end

"""
    Jv = jprod_nln(nlp, x, v)

Evaluate ``J(x)v``, the nonlinear Jacobian-vector product at `x`.
This function is only available if `nlp.meta.jprod_available` is set to `true`.
"""
function jprod_nln(nlp::AbstractNLPModel{T, S}, x::AbstractVector, v::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x v
  Jv = S(undef, get_nnln(nlp.meta))
  return jprod_nln!(nlp, x, v, Jv)
end

"""
    Jv = jprod_nln!(nlp, x, v, Jv)

Evaluate ``J(x)v``, the nonlinear Jacobian-vector product at `x` in place.
This function is only available if `nlp.meta.jprod_available` is set to `true`.
"""
function jprod_nln! end

"""
    Jv = jprod_nln!(nlp, rows, cols, vals, v, Jv)

Evaluate ``J(x)v``, the nonlinear Jacobian-vector product, where the Jacobian is given by `(rows, cols, vals)` in triplet format.
This function is only available if `nlp.meta.jprod_available` is set to `true`.
"""
function jprod_nln!(
  nlp::AbstractNLPModel,
  rows::AbstractVector{<:Integer},
  cols::AbstractVector{<:Integer},
  vals::AbstractVector,
  v::AbstractVector,
  Jv::AbstractVector,
)
  @lencheck get_nln_nnzj(nlp.meta) rows cols vals
  @lencheck get_nvar(nlp.meta) v
  @lencheck get_nnln(nlp.meta) Jv
  increment!(nlp, :neval_jprod_nln)
  coo_prod!(rows, cols, vals, v, Jv)
end

"""
    Jtv = jtprod(nlp, x, v)

Evaluate ``J(x)^Tv``, the transposed-Jacobian-vector product at `x`.
This function is only available if `nlp.meta.jtprod_available` is set to `true`.
"""
function jtprod(nlp::AbstractNLPModel{T, S}, x::AbstractVector, v::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  @lencheck get_ncon(nlp.meta) v
  Jtv = S(undef, get_nvar(nlp.meta))
  return jtprod!(nlp, x, v, Jtv)
end

"""
    Jtv = jtprod!(nlp, x, v, Jtv)

Evaluate ``J(x)^Tv``, the transposed-Jacobian-vector product at `x` in place.
If the problem has linear and nonlinear constraints, this function allocates.
This function is only available if `nlp.meta.jtprod_available` is set to `true`.
"""
function jtprod!(nlp::AbstractNLPModel, x::AbstractVector, v::AbstractVector, Jtv::AbstractVector)
  @lencheck get_nvar(nlp.meta) x Jtv
  @lencheck get_ncon(nlp.meta) v
  increment!(nlp, :neval_jtprod)
  if get_nnln(nlp.meta) == 0
    (get_nlin(nlp.meta) > 0) && jtprod_lin!(nlp, x, v, Jtv)
  elseif get_nlin(nlp.meta) == 0
    (get_nnln(nlp.meta) > 0) && jtprod_nln!(nlp, x, v, Jtv)
  elseif get_nlin(nlp.meta) >= get_nnln(nlp.meta)
    jtprod_lin!(nlp, x, view(v, get_lin(nlp.meta)), Jtv)
    if get_nnln(nlp.meta) > 0
      Jtv .+= jtprod_nln(nlp, x, view(v, get_nln(nlp.meta)))
    end
  else
    jtprod_nln!(nlp, x, view(v, get_nln(nlp.meta)), Jtv)
    if get_nlin(nlp.meta) > 0
      Jtv .+= jtprod_lin(nlp, x, view(v, get_lin(nlp.meta)))
    end
  end
  return Jtv
end

"""
    Jtv = jtprod!(nlp, rows, cols, vals, v, Jtv)

Evaluate ``J(x)^Tv``, the transposed-Jacobian-vector product, where the
Jacobian is given by `(rows, cols, vals)` in triplet format.
This function is only available if `nlp.meta.jtprod_available` is set to `true`.
"""
function jtprod!(
  nlp::AbstractNLPModel,
  rows::AbstractVector{<:Integer},
  cols::AbstractVector{<:Integer},
  vals::AbstractVector,
  v::AbstractVector,
  Jtv::AbstractVector,
)
  @lencheck get_nnzj(nlp.meta) rows cols vals
  @lencheck get_ncon(nlp.meta) v
  @lencheck get_nvar(nlp.meta) Jtv
  increment!(nlp, :neval_jtprod)
  coo_prod!(cols, rows, vals, v, Jtv)
end

"""
    Jtv = jtprod_lin(nlp, x, v)

Evaluate ``J(x)^Tv``, the linear transposed-Jacobian-vector product at `x`.
This function is only available if `nlp.meta.jtprod_available` is set to `true`.
"""
function jtprod_lin(nlp::AbstractNLPModel{T, S}, x::AbstractVector, v::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  @lencheck get_nlin(nlp.meta) v
  Jtv = S(undef, get_nvar(nlp.meta))
  return jtprod_lin!(nlp, x, v, Jtv)
end

"""
    Jtv = jtprod_lin!(nlp, x, v, Jtv)

Evaluate ``J(x)^Tv``, the linear transposed-Jacobian-vector product at `x` in place.
This function is only available if `nlp.meta.jtprod_available` is set to `true`.
"""
function jtprod_lin! end

"""
    Jtv = jtprod_lin!(nlp, rows, cols, vals, v, Jtv)

Evaluate ``J(x)^Tv``, the linear transposed-Jacobian-vector product, where the
Jacobian is given by `(rows, cols, vals)` in triplet format.
This function is only available if `nlp.meta.jtprod_available` is set to `true`.
"""
function jtprod_lin!(
  nlp::AbstractNLPModel,
  rows::AbstractVector{<:Integer},
  cols::AbstractVector{<:Integer},
  vals::AbstractVector,
  v::AbstractVector,
  Jtv::AbstractVector,
)
  @lencheck get_lin_nnzj(nlp.meta) rows cols vals
  @lencheck get_nlin(nlp.meta) v
  @lencheck get_nvar(nlp.meta) Jtv
  increment!(nlp, :neval_jtprod_lin)
  coo_prod!(cols, rows, vals, v, Jtv)
end

"""
    Jtv = jtprod_nln(nlp, x, v)

Evaluate ``J(x)^Tv``, the nonlinear transposed-Jacobian-vector product at `x`.
This function is only available if `nlp.meta.jtprod_available` is set to `true`.
"""
function jtprod_nln(nlp::AbstractNLPModel{T, S}, x::AbstractVector, v::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  @lencheck get_nnln(nlp.meta) v
  Jtv = S(undef, get_nvar(nlp.meta))
  return jtprod_nln!(nlp, x, v, Jtv)
end

"""
    Jtv = jtprod_nln!(nlp, x, v, Jtv)

Evaluate ``J(x)^Tv``, the nonlinear transposed-Jacobian-vector product at `x` in place.
This function is only available if `nlp.meta.jtprod_available` is set to `true`.
"""
function jtprod_nln! end

"""
    Jtv = jtprod_nln!(nlp, rows, cols, vals, v, Jtv)

Evaluate ``J(x)^Tv``, the nonlinear transposed-Jacobian-vector product, where the
Jacobian is given by `(rows, cols, vals)` in triplet format.
This function is only available if `nlp.meta.jtprod_available` is set to `true`.
"""
function jtprod_nln!(
  nlp::AbstractNLPModel,
  rows::AbstractVector{<:Integer},
  cols::AbstractVector{<:Integer},
  vals::AbstractVector,
  v::AbstractVector,
  Jtv::AbstractVector,
)
  @lencheck get_nln_nnzj(nlp.meta) rows cols vals
  @lencheck get_nnln(nlp.meta) v
  @lencheck get_nvar(nlp.meta) Jtv
  increment!(nlp, :neval_jtprod_nln)
  coo_prod!(cols, rows, vals, v, Jtv)
end

"""
    J = jac_op(nlp, x)

Return the Jacobian at `x` as a linear operator.
The resulting object may be used as if it were a matrix, e.g., `J * v` or  `J' * v`.
This function is only available if both `nlp.meta.jprod_available` and `nlp.meta.jtprod_available` are set to `true`.
"""
function jac_op(nlp::AbstractNLPModel{T, S}, x::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  Jv = S(undef, get_ncon(nlp.meta))
  Jtv = S(undef, get_nvar(nlp.meta))
  return jac_op!(nlp, x, Jv, Jtv)
end

"""
    J = jac_op!(nlp, x, Jv, Jtv)

Return the Jacobian at `x` as a linear operator.
The resulting object may be used as if it were a matrix, e.g., `J * v` or `J' * v`.
The values `Jv` and `Jtv` are used as preallocated storage for the operations.
This function is only available if both `nlp.meta.jprod_available` and `nlp.meta.jtprod_available` are set to `true`.
"""
function jac_op!(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector{T},
  Jv::AbstractVector,
  Jtv::AbstractVector,
) where {T, S}
  @lencheck get_nvar(nlp.meta) x Jtv
  @lencheck get_ncon(nlp.meta) Jv
  prod! = @closure (res, v, α, β) -> begin # res = α * J * v + β * res
    jprod!(nlp, x, v, Jv)
    if β == 0
      res .= α .* Jv
    else
      res .= α .* Jv .+ β .* res
    end
    return res
  end
  ctprod! = @closure (res, v, α, β) -> begin
    jtprod!(nlp, x, v, Jtv)
    if β == 0
      res .= α .* Jtv
    else
      res .= α .* Jtv .+ β .* res
    end
    return res
  end
  return LinearOperator{T, S}(
    get_ncon(nlp.meta),
    get_nvar(nlp.meta),
    false,
    false,
    prod!,
    ctprod!,
    ctprod!,
  )
end

"""
    J = jac_op!(nlp, rows, cols, vals, Jv, Jtv)

Return the Jacobian given by `(rows, cols, vals)` as a linear operator.
The resulting object may be used as if it were a matrix, e.g., `J * v` or `J' * v`.
The values `Jv` and `Jtv` are used as preallocated storage for the operations.
This function is only available if both `nlp.meta.jprod_available` and `nlp.meta.jtprod_available` are set to `true`.
"""
function jac_op!(
  nlp::AbstractNLPModel{T, S},
  rows::AbstractVector{<:Integer},
  cols::AbstractVector{<:Integer},
  vals::AbstractVector{T},
  Jv::AbstractVector,
  Jtv::AbstractVector,
) where {T, S}
  @lencheck get_nnzj(nlp.meta) rows cols vals
  @lencheck get_ncon(nlp.meta) Jv
  @lencheck get_nvar(nlp.meta) Jtv
  prod! = @closure (res, v, α, β) -> begin # res = α * J * v + β * res
    jprod!(nlp, rows, cols, vals, v, Jv)
    if β == 0
      res .= α .* Jv
    else
      res .= α .* Jv .+ β .* res
    end
    return res
  end
  ctprod! = @closure (res, v, α, β) -> begin
    jtprod!(nlp, rows, cols, vals, v, Jtv)
    if β == 0
      res .= α .* Jtv
    else
      res .= α .* Jtv .+ β .* res
    end
    return res
  end
  return LinearOperator{T, S}(
    get_ncon(nlp.meta),
    get_nvar(nlp.meta),
    false,
    false,
    prod!,
    ctprod!,
    ctprod!,
  )
end

"""
    J = jac_lin_op(nlp, x)

Return the linear Jacobian at `x` as a linear operator.
The resulting object may be used as if it were a matrix, e.g., `J * v` or `J' * v`.
This function is only available if both `nlp.meta.jprod_available` and `nlp.meta.jtprod_available` are set to `true`.
"""
function jac_lin_op(nlp::AbstractNLPModel{T, S}, x::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  Jv = S(undef, get_nlin(nlp.meta))
  Jtv = S(undef, get_nvar(nlp.meta))
  return jac_lin_op!(nlp, x, Jv, Jtv)
end

"""
    J = jac_lin_op!(nlp, x, Jv, Jtv)

Return the linear Jacobian at `x` as a linear operator.
The resulting object may be used as if it were a matrix, e.g., `J * v` or `J' * v`.
The values `Jv` and `Jtv` are used as preallocated storage for the operations.
This function is only available if both `nlp.meta.jprod_available` and `nlp.meta.jtprod_available` are set to `true`.
"""
function jac_lin_op!(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector{T},
  Jv::AbstractVector,
  Jtv::AbstractVector,
) where {T, S}
  @lencheck get_nvar(nlp.meta) x Jtv
  @lencheck get_nlin(nlp.meta) Jv
  prod! = @closure (res, v, α, β) -> begin # res = α * J * v + β * res
    jprod_lin!(nlp, x, v, Jv)
    if β == 0
      res .= α .* Jv
    else
      res .= α .* Jv .+ β .* res
    end
    return res
  end
  ctprod! = @closure (res, v, α, β) -> begin
    jtprod_lin!(nlp, x, v, Jtv)
    if β == 0
      res .= α .* Jtv
    else
      res .= α .* Jtv .+ β .* res
    end
    return res
  end
  return LinearOperator{T, S}(
    get_nlin(nlp.meta),
    get_nvar(nlp.meta),
    false,
    false,
    prod!,
    ctprod!,
    ctprod!,
  )
end

"""
    J = jac_lin_op!(nlp, rows, cols, vals, Jv, Jtv)

Return the linear Jacobian given by `(rows, cols, vals)` as a linear operator.
The resulting object may be used as if it were a matrix, e.g., `J * v` or `J' * v`.
The values `Jv` and `Jtv` are used as preallocated storage for the operations.
This function is only available if both `nlp.meta.jprod_available` and `nlp.meta.jtprod_available` are set to `true`.
"""
function jac_lin_op!(
  nlp::AbstractNLPModel{T, S},
  rows::AbstractVector{<:Integer},
  cols::AbstractVector{<:Integer},
  vals::AbstractVector{T},
  Jv::AbstractVector,
  Jtv::AbstractVector,
) where {T, S}
  @lencheck get_lin_nnzj(nlp.meta) rows cols vals
  @lencheck get_nlin(nlp.meta) Jv
  @lencheck get_nvar(nlp.meta) Jtv
  prod! = @closure (res, v, α, β) -> begin # res = α * J * v + β * res
    jprod_lin!(nlp, rows, cols, vals, v, Jv)
    if β == 0
      res .= α .* Jv
    else
      res .= α .* Jv .+ β .* res
    end
    return res
  end
  ctprod! = @closure (res, v, α, β) -> begin
    jtprod_lin!(nlp, rows, cols, vals, v, Jtv)
    if β == 0
      res .= α .* Jtv
    else
      res .= α .* Jtv .+ β .* res
    end
    return res
  end
  return LinearOperator{T, S}(
    get_nlin(nlp.meta),
    get_nvar(nlp.meta),
    false,
    false,
    prod!,
    ctprod!,
    ctprod!,
  )
end

"""
    J = jac_nln_op(nlp, x)

Return the nonlinear Jacobian at `x` as a linear operator.
The resulting object may be used as if it were a matrix, e.g., `J * v` or `J' * v`.
This function is only available if both `nlp.meta.jprod_available` and `nlp.meta.jtprod_available` are set to `true`.
"""
function jac_nln_op(nlp::AbstractNLPModel{T, S}, x::AbstractVector) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  Jv = S(undef, get_nnln(nlp.meta))
  Jtv = S(undef, get_nvar(nlp.meta))
  return jac_nln_op!(nlp, x, Jv, Jtv)
end

"""
    J = jac_nln_op!(nlp, x, Jv, Jtv)

Return the nonlinear Jacobian at `x` as a linear operator.
The resulting object may be used as if it were a matrix, e.g., `J * v` or `J' * v`.
The values `Jv` and `Jtv` are used as preallocated storage for the operations.
This function is only available if both `nlp.meta.jprod_available` and `nlp.meta.jtprod_available` are set to `true`.
"""
function jac_nln_op!(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector{T},
  Jv::AbstractVector,
  Jtv::AbstractVector,
) where {T, S}
  @lencheck get_nvar(nlp.meta) x Jtv
  @lencheck get_nnln(nlp.meta) Jv
  prod! = @closure (res, v, α, β) -> begin # res = α * J * v + β * res
    jprod_nln!(nlp, x, v, Jv)
    if β == 0
      res .= α .* Jv
    else
      res .= α .* Jv .+ β .* res
    end
    return res
  end
  ctprod! = @closure (res, v, α, β) -> begin
    jtprod_nln!(nlp, x, v, Jtv)
    if β == 0
      res .= α .* Jtv
    else
      res .= α .* Jtv .+ β .* res
    end
    return res
  end
  return LinearOperator{T, S}(
    get_nnln(nlp.meta),
    get_nvar(nlp.meta),
    false,
    false,
    prod!,
    ctprod!,
    ctprod!,
  )
end

"""
    J = jac_nln_op!(nlp, rows, cols, vals, Jv, Jtv)

Return the nonlinear Jacobian given by `(rows, cols, vals)` as a linear operator.
The resulting object may be used as if it were a matrix, e.g., `J * v` or `J' * v`.
The values `Jv` and `Jtv` are used as preallocated storage for the operations.
This function is only available if both `nlp.meta.jprod_available` and `nlp.meta.jtprod_available` are set to `true`.
"""
function jac_nln_op!(
  nlp::AbstractNLPModel{T, S},
  rows::AbstractVector{<:Integer},
  cols::AbstractVector{<:Integer},
  vals::AbstractVector{T},
  Jv::AbstractVector,
  Jtv::AbstractVector,
) where {T, S}
  @lencheck get_nln_nnzj(nlp.meta) rows cols vals
  @lencheck get_nnln(nlp.meta) Jv
  @lencheck get_nvar(nlp.meta) Jtv
  prod! = @closure (res, v, α, β) -> begin # res = α * J * v + β * res
    jprod_nln!(nlp, rows, cols, vals, v, Jv)
    if β == 0
      res .= α .* Jv
    else
      res .= α .* Jv .+ β .* res
    end
    return res
  end
  ctprod! = @closure (res, v, α, β) -> begin
    jtprod_nln!(nlp, rows, cols, vals, v, Jtv)
    if β == 0
      res .= α .* Jtv
    else
      res .= α .* Jtv .+ β .* res
    end
    return res
  end
  return LinearOperator{T, S}(
    get_nnln(nlp.meta),
    get_nvar(nlp.meta),
    false,
    false,
    prod!,
    ctprod!,
    ctprod!,
  )
end

"""
    vals = jth_hess_coord(nlp, x, j)

Evaluate the Hessian of j-th constraint at `x` in sparse coordinate format.
Only the lower triangle is returned.
This function is only available when `nlp.meta.sparse_hessian` is set to `true`.
"""
function jth_hess_coord(nlp::AbstractNLPModel{T, S}, x::AbstractVector, j::Integer) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  @rangecheck 1 get_ncon(nlp.meta) j
  vals = S(undef, get_nnzh(nlp.meta))
  return jth_hess_coord!(nlp, x, j, vals)
end

"""
    vals = jth_hess_coord!(nlp, x, j, vals)

Evaluate the Hessian of j-th constraint at `x` in sparse coordinate format, with `vals` of length `nlp.meta.nnzh`, in place.
Only the lower triangle is returned.
This function is only available when `nlp.meta.sparse_hessian` is set to `true`.
"""
function jth_hess_coord! end

"""
    Hx = jth_hess(nlp, x, j)

Evaluate the Hessian of j-th constraint at `x` as a sparse matrix with
the same sparsity pattern as the Lagrangian Hessian.
A `Symmetric` object wrapping the lower triangle is returned.
This function is only available when `nlp.meta.sparse_hessian` is set to `true`.
"""
function jth_hess(nlp::AbstractNLPModel, x::AbstractVector, j::Integer)
  @lencheck get_nvar(nlp.meta) x
  @rangecheck 1 get_ncon(nlp.meta) j
  rows, cols = hess_structure(nlp)
  vals = jth_hess_coord(nlp, x, j)
  return Symmetric(sparse(rows, cols, vals, get_nvar(nlp.meta), get_nvar(nlp.meta)), :L)
end

"""
    Hv = jth_hprod(nlp, x, v, j)

Evaluate the product of the Hessian of j-th constraint at `x` with the vector `v`.
"""
function jth_hprod(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector,
  v::AbstractVector,
  j::Integer,
) where {T, S}
  @lencheck get_nvar(nlp.meta) x v
  @rangecheck 1 get_ncon(nlp.meta) j
  Hv = S(undef, get_nvar(nlp.meta))
  return jth_hprod!(nlp, x, v, j, Hv)
end

"""
    Hv = jth_hprod!(nlp, x, v, j, Hv)

Evaluate the product of the Hessian of j-th constraint at `x` with the vector `v`
in place.
"""
function jth_hprod! end

"""
    gHv = ghjvprod(nlp, x, g, v)

Return the vector whose i-th component is gᵀ ∇²cᵢ(x) v.
"""
function ghjvprod(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector,
  g::AbstractVector,
  v::AbstractVector,
) where {T, S}
  @lencheck get_nvar(nlp.meta) x g v
  gHv = S(undef, get_ncon(nlp.meta))
  return ghjvprod!(nlp, x, g, v, gHv)
end

"""
    ghjvprod!(nlp, x, g, v, gHv)

Return the vector whose i-th component is gᵀ ∇²cᵢ(x) v in place.
"""
function ghjvprod! end

"""
    (rows,cols) = hess_structure(nlp.meta)

Return the structure of the Lagrangian Hessian in sparse coordinate format.
This function is only available when both `nlp.meta.hess_available` and `nlp.meta.sparse_hessian` are set to `true`.
"""
function hess_structure(nlp::AbstractNLPModel)
  rows = Vector{Int}(undef, get_nnzh(nlp.meta))
  cols = Vector{Int}(undef, get_nnzh(nlp.meta))
  hess_structure!(nlp, rows, cols)
end

"""
    hess_structure!(nlp, rows, cols)

Return the structure of the Lagrangian Hessian in sparse coordinate format in place.
This function is only available when both `nlp.meta.hess_available` and `nlp.meta.sparse_hessian` are set to `true`.
"""
function hess_structure! end

"""
    vals = hess_coord!(nlp, x, vals; obj_weight=1.0)

Evaluate the objective Hessian at `x` in sparse coordinate format,
with objective function scaled by `obj_weight`, i.e.,
$(OBJECTIVE_HESSIAN), overwriting `vals`.
Only the lower triangle is returned.
This function is only available when both `nlp.meta.hess_available` and `nlp.meta.sparse_hessian` are set to `true`.
"""
function hess_coord!(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector{T},
  vals::AbstractVector;
  obj_weight::Real = one(T),
) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  @lencheck get_nnzh(nlp.meta) vals
  y = fill!(S(undef, get_ncon(nlp.meta)), 0)
  hess_coord!(nlp, x, y, vals, obj_weight = obj_weight)
end

"""
    vals = hess_coord!(nlp, x, y, vals; obj_weight=1.0)

Evaluate the Lagrangian Hessian at `(x,y)` in sparse coordinate format,
with objective function scaled by `obj_weight`, i.e.,
$(LAGRANGIAN_HESSIAN), overwriting `vals`.
Only the lower triangle is returned.
This function is only available when both `nlp.meta.hess_available` and `nlp.meta.sparse_hessian` are set to `true`.
"""
function hess_coord! end

"""
    vals = hess_coord(nlp, x; obj_weight=1.0)

Evaluate the objective Hessian at `x` in sparse coordinate format,
with objective function scaled by `obj_weight`, i.e.,
$(OBJECTIVE_HESSIAN).
Only the lower triangle is returned.
This function is only available when both `nlp.meta.hess_available` and `nlp.meta.sparse_hessian` are set to `true`.
"""
function hess_coord(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector;
  obj_weight::Real = one(T),
) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  vals = S(undef, get_nnzh(nlp.meta))
  return hess_coord!(nlp, x, vals; obj_weight = obj_weight)
end

"""
    vals = hess_coord(nlp, x, y; obj_weight=1.0)

Evaluate the Lagrangian Hessian at `(x,y)` in sparse coordinate format,
with objective function scaled by `obj_weight`, i.e.,
$(LAGRANGIAN_HESSIAN).
Only the lower triangle is returned.
This function is only available when both `nlp.meta.hess_available` and `nlp.meta.sparse_hessian` are set to `true`.
"""
function hess_coord(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector,
  y::AbstractVector;
  obj_weight::Real = one(T),
) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  @lencheck get_ncon(nlp.meta) y
  vals = S(undef, get_nnzh(nlp.meta))
  return hess_coord!(nlp, x, y, vals; obj_weight = obj_weight)
end

"""
    Hx = hess(nlp, x; obj_weight=1.0)

Evaluate the objective Hessian at `x` as a sparse matrix,
with objective function scaled by `obj_weight`, i.e.,
$(OBJECTIVE_HESSIAN).
A `Symmetric` object wrapping the lower triangle is returned.
This function is only available when both `nlp.meta.hess_available` and `nlp.meta.sparse_hessian` are set to `true`.
"""
function hess(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector;
  obj_weight::Real = one(T),
) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  rows, cols = hess_structure(nlp)
  vals = hess_coord(nlp, x, obj_weight = obj_weight)
  Symmetric(sparse(rows, cols, vals, get_nvar(nlp.meta), get_nvar(nlp.meta)), :L)
end

"""
    Hx = hess_dense!(nlp, x, Hx; obj_weight=1.0)
    Hx = hess_dense!(nlp, x, y, Hx; obj_weight=1.0)

The first method evaluates ``H(x)``, the Hessian of the objective at `x` in dense format, overwriting `Hx`.
The second method evaluates ``H(x,y)``, the Hessian of the Lagrangian at `(x,y)` in dense format, overwriting `Hx`.
Only the lower triangular part of `Hx` needs to be filled.
This function is only available when `nlp.meta.hess_available` is set to `true` and `nlp.meta.sparse_hessian` is set to `false`.
"""
function hess_dense! end

"""
    Hx = hess(nlp, x, y; obj_weight=1.0)

Evaluate the Lagrangian Hessian at `(x,y)` as a sparse matrix,
with objective function scaled by `obj_weight`, i.e.,
$(LAGRANGIAN_HESSIAN).
A `Symmetric` object wrapping the lower triangle is returned.
This function is only available when both `nlp.meta.hess_available` and `nlp.meta.sparse_hessian` are set to `true`.
"""
function hess(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector,
  y::AbstractVector;
  obj_weight::Real = one(T),
) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  @lencheck get_ncon(nlp.meta) y
  rows, cols = hess_structure(nlp)
  vals = hess_coord(nlp, x, y, obj_weight = obj_weight)
  Symmetric(sparse(rows, cols, vals, get_nvar(nlp.meta), get_nvar(nlp.meta)), :L)
end

"""
    Hv = hprod(nlp, x, v; obj_weight=1.0)

Evaluate the product of the objective Hessian at `x` with the vector `v`,
with objective function scaled by `obj_weight`, where the objective Hessian is
$(OBJECTIVE_HESSIAN).
This function is only available if `nlp.meta.hprod_available` is set to `true`.
"""
function hprod(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector,
  v::AbstractVector;
  obj_weight::Real = one(T),
) where {T, S}
  @lencheck get_nvar(nlp.meta) x v
  Hv = S(undef, get_nvar(nlp.meta))
  return hprod!(nlp, x, v, Hv; obj_weight = obj_weight)
end

"""
    Hv = hprod(nlp, x, y, v; obj_weight=1.0)

Evaluate the product of the Lagrangian Hessian at `(x,y)` with the vector `v`,
with objective function scaled by `obj_weight`, where the Lagrangian Hessian is
$(LAGRANGIAN_HESSIAN).
This function is only available if `nlp.meta.hprod_available` is set to `true`.
"""
function hprod(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector,
  y::AbstractVector,
  v::AbstractVector;
  obj_weight::Real = one(T),
) where {T, S}
  @lencheck get_nvar(nlp.meta) x v
  @lencheck get_ncon(nlp.meta) y
  Hv = S(undef, get_nvar(nlp.meta))
  return hprod!(nlp, x, y, v, Hv; obj_weight = obj_weight)
end

"""
    Hv = hprod!(nlp, x, v, Hv; obj_weight=1.0)

Evaluate the product of the objective Hessian at `x` with the vector `v` in
place, with objective function scaled by `obj_weight`, where the objective Hessian is
$(OBJECTIVE_HESSIAN).
This function is only available if `nlp.meta.hprod_available` is set to `true`.
"""
function hprod!(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector,
  v::AbstractVector,
  Hv::AbstractVector;
  obj_weight::Real = one(T),
) where {T, S}
  @lencheck get_nvar(nlp.meta) x v Hv
  y = fill!(S(undef, get_ncon(nlp.meta)), 0)
  hprod!(nlp, x, y, v, Hv, obj_weight = obj_weight)
end

"""
    Hv = hprod!(nlp, rows, cols, vals, v, Hv)

Evaluate the product of the objective or Lagrangian Hessian given by `(rows, cols, vals)` in
triplet format with the vector `v` in place. Only one triangle of the Hessian should be given.
This function is only available if `nlp.meta.hprod_available` is set to `true`.
"""
function hprod!(
  nlp::AbstractNLPModel,
  rows::AbstractVector{<:Integer},
  cols::AbstractVector{<:Integer},
  vals::AbstractVector,
  v::AbstractVector,
  Hv::AbstractVector,
)
  @lencheck get_nnzh(nlp.meta) rows cols vals
  @lencheck get_nvar(nlp.meta) v Hv
  increment!(nlp, :neval_hprod)
  coo_sym_prod!(cols, rows, vals, v, Hv)
end

"""
    Hv = hprod!(nlp, x, y, v, Hv; obj_weight=1.0)

Evaluate the product of the Lagrangian Hessian at `(x,y)` with the vector `v` in
place, with objective function scaled by `obj_weight`, where the Lagrangian Hessian is
$(LAGRANGIAN_HESSIAN).
This function is only available if `nlp.meta.hprod_available` is set to `true`.
"""
function hprod! end

"""
    H = hess_op(nlp, x; obj_weight=1.0)

Return the objective Hessian at `x` with objective function scaled by
`obj_weight` as a linear operator. The resulting object may be used as if it were a
matrix, e.g., `H * v`. The linear operator H represents
$(OBJECTIVE_HESSIAN).
This function is only available if `nlp.meta.hprod_available` is set to `true`.
"""
function hess_op(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector;
  obj_weight::Real = one(T),
) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  Hv = S(undef, get_nvar(nlp.meta))
  return hess_op!(nlp, x, Hv, obj_weight = obj_weight)
end

"""
    H = hess_op(nlp, x, y; obj_weight=1.0)

Return the Lagrangian Hessian at `(x,y)` with objective function scaled by
`obj_weight` as a linear operator. The resulting object may be used as if it were a
matrix, e.g., `H * v`. The linear operator H represents
$(LAGRANGIAN_HESSIAN).
This function is only available if `nlp.meta.hprod_available` is set to `true`.
"""
function hess_op(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector,
  y::AbstractVector;
  obj_weight::Real = one(T),
) where {T, S}
  @lencheck get_nvar(nlp.meta) x
  @lencheck get_ncon(nlp.meta) y
  Hv = S(undef, get_nvar(nlp.meta))
  return hess_op!(nlp, x, y, Hv, obj_weight = obj_weight)
end

"""
    H = hess_op!(nlp, x, Hv; obj_weight=1.0)

Return the objective Hessian at `x` with objective function scaled by
`obj_weight` as a linear operator, and storing the result on `Hv`. The resulting
object may be used as if it were a matrix, e.g., `w = H * v`. The vector `Hv` is
used as preallocated storage for the operation.  The linear operator H
represents
$(OBJECTIVE_HESSIAN).
This function is only available if `nlp.meta.hprod_available` is set to `true`.
"""
function hess_op!(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector,
  Hv::AbstractVector;
  obj_weight::Real = one(T),
) where {T, S}
  @lencheck get_nvar(nlp.meta) x Hv
  prod! = @closure (res, v, α, β) -> begin
    hprod!(nlp, x, v, Hv; obj_weight = obj_weight)
    if β == 0
      res .= α .* Hv
    else
      res .= α .* Hv .+ β .* res
    end
    return res
  end
  return LinearOperator{T, S}(
    get_nvar(nlp.meta),
    get_nvar(nlp.meta),
    true,
    true,
    prod!,
    prod!,
    prod!,
  )
end

"""
    H = hess_op!(nlp, rows, cols, vals, Hv)

Return the Hessian given by `(rows, cols, vals)` as a linear operator,
and storing the result on `Hv`. The resulting
object may be used as if it were a matrix, e.g., `w = H * v`.
The vector `Hv` is used as preallocated storage for the operation.
The linear operator H represents
$(OBJECTIVE_HESSIAN).
This function is only available if `nlp.meta.hprod_available` is set to `true`.
"""
function hess_op!(
  nlp::AbstractNLPModel{T, S},
  rows::AbstractVector{<:Integer},
  cols::AbstractVector{<:Integer},
  vals::AbstractVector,
  Hv::AbstractVector,
) where {T, S}
  @lencheck get_nnzh(nlp.meta) rows cols vals
  @lencheck get_nvar(nlp.meta) Hv
  prod! = @closure (res, v, α, β) -> begin
    hprod!(nlp, rows, cols, vals, v, Hv)
    if β == 0
      res .= α .* Hv
    else
      res .= α .* Hv .+ β .* res
    end
    return res
  end
  return LinearOperator{T, S}(
    get_nvar(nlp.meta),
    get_nvar(nlp.meta),
    true,
    true,
    prod!,
    prod!,
    prod!,
  )
end

"""
    H = hess_op!(nlp, x, y, Hv; obj_weight=1.0)

Return the Lagrangian Hessian at `(x,y)` with objective function scaled by
`obj_weight` as a linear operator, and storing the result on `Hv`. The resulting
object may be used as if it were a matrix, e.g., `w = H * v`. The vector `Hv` is
used as preallocated storage for the operation.  The linear operator H
represents
$(LAGRANGIAN_HESSIAN).
This function is only available if `nlp.meta.hprod_available` is set to `true`.
"""
function hess_op!(
  nlp::AbstractNLPModel{T, S},
  x::AbstractVector,
  y::AbstractVector,
  Hv::AbstractVector;
  obj_weight::Real = one(T),
) where {T, S}
  @lencheck get_nvar(nlp.meta) x Hv
  @lencheck get_ncon(nlp.meta) y
  prod! = @closure (res, v, α, β) -> begin
    hprod!(nlp, x, y, v, Hv; obj_weight = obj_weight)
    if β == 0
      res .= α .* Hv
    else
      res .= α .* Hv .+ β .* res
    end
    return res
  end
  return LinearOperator{T, S}(
    get_nvar(nlp.meta),
    get_nvar(nlp.meta),
    true,
    true,
    prod!,
    prod!,
    prod!,
  )
end

"""
    varscale(model::AbstractNLPModel)

Return a vector containing the scaling factors for each variable in the model.
This is typically used to normalize variables for numerical stability in solvers.

By default, the scaling is model-dependent. If not overridden by the model, a vector of ones 
is returned. Inspired by the AMPL scaling conventions.
"""
function varscale end

"""
    lagscale(model::AbstractNLPModel)

Return a vector of scaling factors for the Lagrange multipliers associated with constraints.
This can be used to improve numerical stability or condition number when solving KKT systems.
"""
function lagscale end

"""
    conscale(model::AbstractNLPModel)

Return a vector of constraint scaling factors for the model.
These are typically used to normalize constraints to have similar magnitudes and improve 
convergence behavior in nonlinear solvers.
"""
function conscale end
