# Note: we define type aliases outside of the @muladd block to avoid Revise breaking when code
# inside the @muladd block is edited. See https://github.com/trixi-framework/Trixi.jl/issues/801
# for more details.

# `DGMulti` refers to both multiple DG types (polynomial/SBP, simplices/quads/hexes) as well as
# the use of multi-dimensional operators in the solver.
const DGMulti{NDIMS, ElemType, ApproxType, SurfaceIntegral, VolumeIntegral} =
  DG{<:RefElemData{NDIMS, ElemType, ApproxType}, Mortar, SurfaceIntegral, VolumeIntegral} where {Mortar}

# Type aliases. The first parameter is `ApproxType` since it is more commonly used for dispatch.
const DGMultiWeakForm{ApproxType, ElemType} =
  DGMulti{NDIMS, ElemType, ApproxType, <:SurfaceIntegralWeakForm, <:VolumeIntegralWeakForm} where {NDIMS}

const DGMultiFluxDiff{ApproxType, ElemType} =
  DGMulti{NDIMS, ElemType, ApproxType, <:SurfaceIntegralWeakForm, <:VolumeIntegralFluxDifferencing} where {NDIMS}

const DGMultiFluxDiffSBP{ApproxType, ElemType} =
  DGMulti{NDIMS, ElemType, ApproxType, <:SurfaceIntegralWeakForm, <:VolumeIntegralFluxDifferencing} where {NDIMS, ApproxType<:Union{SBP, AbstractDerivativeOperator}}

const DGMultiSBP{ApproxType, ElemType} =
  DGMulti{NDIMS, ElemType, ApproxType, SurfaceIntegral, VolumeIntegral} where {NDIMS, ElemType, ApproxType<:Union{SBP, AbstractDerivativeOperator}, SurfaceIntegral, VolumeIntegral}

# By default, Julia/LLVM does not use fused multiply-add operations (FMAs).
# Since these FMAs can increase the performance of many numerical algorithms,
# we need to opt-in explicitly.
# See https://ranocha.de/blog/Optimizing_EC_Trixi for further details.
@muladd begin

# these are necessary for pretty printing
polydeg(dg::DGMulti) = dg.basis.N
Base.summary(io::IO, dg::DG) where {DG <: DGMulti} = print(io, "DGMulti(polydeg=$(polydeg(dg)))")
Base.real(rd::RefElemData{NDIMS, Elem, ApproxType, Nfaces, RealT}) where {NDIMS, Elem, ApproxType, Nfaces, RealT} = RealT

"""
    DGMulti(; polydeg::Integer,
              element_type::AbstractElemShape,
              approximation_type=Polynomial(),
              surface_flux=flux_central,
              surface_integral=SurfaceIntegralWeakForm(surface_flux),
              volume_integral=VolumeIntegralWeakForm(),
              RefElemData_kwargs...)

Create a discontinuous Galerkin method which uses
- approximations of polynomial degree `polydeg`
- element type `element_type` (`Tri()`, `Quad()`, `Tet()`, and `Hex()` currently supported)

Optional:
- `approximation_type` (default is `Polynomial()`; `SBP()` also supported for `Tri()`, `Quad()`,
  and `Hex()` element types).
- `RefElemData_kwargs` are additional keyword arguments for `RefElemData`, such as `quad_rule_vol`.
  For more info, see the [StartUpDG.jl docs](https://jlchan.github.io/StartUpDG.jl/dev/).
"""
function DGMulti(; polydeg=nothing,
                   element_type::AbstractElemShape,
                   approximation_type=Polynomial(),
                   surface_flux=flux_central,
                   surface_integral=SurfaceIntegralWeakForm(surface_flux),
                   volume_integral=VolumeIntegralWeakForm(),
                   kwargs...)

  # call dispatchable constructor
  DGMulti(element_type, approximation_type, volume_integral, surface_integral;
          polydeg=polydeg, kwargs...)
end

# dispatchable constructor for DGMulti to allow for specialization
function DGMulti(element_type::AbstractElemShape,
                 approximation_type,
                 volume_integral,
                 surface_integral;
                 polydeg::Integer,
                 kwargs...)

  rd = RefElemData(element_type, approximation_type, polydeg; kwargs...)
  return DG(rd, nothing #= mortar =#, surface_integral, volume_integral)
end

########################################
#            DGMultiMesh
########################################

# now that `DGMulti` is defined, we can define constructors for `DGMultiMesh` which use `dg::DGMulti`

function DGMultiMesh(dg::DGMulti, geometric_term_type, md::MeshData{NDIMS}, boundary_faces) where {NDIMS}
  return DGMultiMesh{NDIMS, geometric_term_type, typeof(md), typeof(boundary_faces)}(md, boundary_faces)
end

# TODO: DGMulti, v0.5. These constructors which use `rd::RefElemData` are now redundant and can be removed.
function DGMultiMesh(vertex_coordinates::NTuple{NDIMS, Vector{Tv}}, EToV::Array{Ti,2}, rd::RefElemData;
                     is_on_boundary = nothing,
                     periodicity=ntuple(_->false, NDIMS), kwargs...) where {NDIMS, Tv, Ti}

  Base.depwarn("`DGMultiMesh` constructor with `rd::RefElemData` is deprecated. Use the constructor with `dg::DGMulti` instead.",
               :DGMultiMesh)
  if haskey(kwargs, :is_periodic)
    # TODO: DGMulti, v0.5. Remove deprecated keyword
    Base.depwarn("keyword argument `is_periodic` is now `periodicity`.", :DGMultiMesh)
    periodicity=kwargs[:is_periodic]
  end

  md = MeshData(vertex_coordinates, EToV, rd)
  if NDIMS==1
    md = StartUpDG.make_periodic(md, periodicity...)
  else
    md = StartUpDG.make_periodic(md, periodicity)
  end
  boundary_faces = StartUpDG.tag_boundary_faces(md, is_on_boundary)
  return DGMultiMesh{NDIMS, typeof(rd.elementType), typeof(md), typeof(boundary_faces)}(md, boundary_faces)
end

function DGMultiMesh(triangulateIO, rd::RefElemData{2, Tri}, boundary_dict::Dict{Symbol, Int})

  vertex_coordinates, EToV = StartUpDG.triangulateIO_to_VXYEToV(triangulateIO)
  md = MeshData(vertex_coordinates, EToV, rd)
  boundary_faces = StartUpDG.tag_boundary_faces(triangulateIO, rd, md, boundary_dict)
  return DGMultiMesh{2, typeof(rd.elementType), typeof(md), typeof(boundary_faces)}(md, boundary_faces)
end

# TODO: DGMulti, v0.5. Remove deprecated constructor
@deprecate VertexMappedMesh(args...; kwargs...) DGMultiMesh(args...; kwargs...)

# Mesh types used internally for trait dispatch
struct Cartesian end
struct VertexMapped end # where element geometry is determined by vertices.
struct Curved end

# type parameters for dispatch using `DGMultiMesh`
abstract type GeometricTermsType end
struct Affine <: GeometricTermsType end # mesh produces constant geometric terms
struct NonAffine <: GeometricTermsType end # mesh produces non-constant geometric terms

# choose MeshType based on the constructor and element type
GeometricTermsType(mesh_type, dg::DGMulti) = GeometricTermsType(mesh_type, dg.basis.elementType)
GeometricTermsType(mesh_type::Cartesian, element_type::AbstractElemShape) = Affine()
GeometricTermsType(mesh_type::TriangulateIO, element_type::Tri) = Affine()
GeometricTermsType(mesh_type::VertexMapped, element_type::Union{Tri, Tet}) = Affine()
GeometricTermsType(mesh_type::VertexMapped, element_type::Union{Quad, Hex}) = NonAffine()
GeometricTermsType(mesh_type::Curved, element_type::AbstractElemShape) = NonAffine()

# other potential constructor types to add later: Bilinear, Isoparametric{polydeg_geo}, Rational/Exact?
# other potential mesh types to add later: Polynomial{polydeg_geo}?

"""
  DGMultiMesh(vertex_coordinates, EToV, dg::DGMulti{NDIMS};
              is_on_boundary=nothing,
              periodicity=ntuple(_->false, NDIMS)) where {NDIMS, Tv}

- `vertex_coordinates` is a tuple of vectors containing x,y,... components of the vertex coordinates
- `EToV` is a 2D array containing element-to-vertex connectivities for each element
- `dg::DGMulti` contains information associated with to the reference element (e.g., quadrature,
  basis evaluation, differentiation, etc).
- `is_on_boundary` specifies boundary using a `Dict{Symbol, <:Function}`
- `periodicity` is a tuple of booleans specifying if the domain is periodic `true`/`false` in the
  (x,y,z) direction.
"""
# TODO: DGMulti v0.5. Standardize order of arguments, pass in `dg` first
function DGMultiMesh(vertex_coordinates, EToV, dg::DGMulti{NDIMS};
                     is_on_boundary=nothing,
                     periodicity=ntuple(_->false, NDIMS), kwargs...) where {NDIMS}
  if haskey(kwargs, :is_periodic)
    # TODO: DGMulti, v0.5. Remove deprecated keyword
    Base.depwarn("keyword argument `is_periodic` is now `periodicity`.", :DGMultiMesh)
    periodicity=kwargs[:is_periodic]
  end

  md = MeshData(vertex_coordinates, EToV, dg.basis)
  if NDIMS == 1
    md = StartUpDG.make_periodic(md, periodicity...)
  else
    md = StartUpDG.make_periodic(md, periodicity)
  end
  boundary_faces = StartUpDG.tag_boundary_faces(md, is_on_boundary)
  return DGMultiMesh(dg, GeometricTermsType(VertexMapped(), dg), md, boundary_faces)
end

"""
    DGMultiMesh(triangulateIO, dg::DGMulti{2, Tri}, boundary_dict::Dict{Symbol, Int})

- `triangulateIO` is a `TriangulateIO` mesh representation
- `dg::DGMulti` contains information associated with to the reference element (e.g., quadrature,
  basis evaluation, differentiation, etc).
- `boundary_dict` is a `Dict{Symbol, Int}` which associates each integer `TriangulateIO` boundary
  tag with a `Symbol`.
"""
# TODO: DGMulti v0.5, standardize order of arguments (`dg` first)
function DGMultiMesh(triangulateIO, dg::DGMulti{2, Tri}, boundary_dict::Dict{Symbol, Int};
                     periodicity=(false, false))
  vertex_coordinates, EToV = StartUpDG.triangulateIO_to_VXYEToV(triangulateIO)
  md = MeshData(vertex_coordinates, EToV, dg.basis)
  md = StartUpDG.make_periodic(md, periodicity)
  boundary_faces = StartUpDG.tag_boundary_faces(triangulateIO, dg.basis, md, boundary_dict)
  return DGMultiMesh(dg, GeometricTermsType(TriangulateIO(), dg), md, boundary_faces)
end

# TODO: DGMulti. Make `cells_per_dimension` a non-keyword argument for easier dispatch.
"""
    DGMultiMesh(dg::DGMulti; cells_per_dimension,
                coordinates_min=(-1.0, -1.0), coordinates_max=(1.0, 1.0),
                is_on_boundary=nothing,
                periodicity=ntuple(_ -> false, NDIMS))

Constructs a Cartesian [`DGMultiMesh`](@ref) with element type `dg.basis.elementType`. The domain is
the tensor product of the intervals `[coordinates_min[i], coordinates_max[i]]`.
- `is_on_boundary` specifies boundary using a `Dict{Symbol, <:Function}`
- `periodicity` is a tuple of `Bool`s specifying periodicity = `true`/`false` in the (x,y,z) direction.
"""
function DGMultiMesh(dg::DGMulti{NDIMS}; cells_per_dimension,
                     coordinates_min=ntuple(_ -> -one(real(dg)), NDIMS),
                     coordinates_max=ntuple(_ -> one(real(dg)), NDIMS),
                     is_on_boundary=nothing,
                     periodicity=ntuple(_ -> false, NDIMS), kwargs...) where {NDIMS}

  if haskey(kwargs, :is_periodic)
    # TODO: DGMulti. Deprecate `is_periodic` in version 0.5
    Base.depwarn("keyword argument `is_periodic` is now `periodicity`.", :DGMultiMesh)
    periodicity=kwargs[:is_periodic]
  end

  vertex_coordinates, EToV = StartUpDG.uniform_mesh(dg.basis.elementType, cells_per_dimension...)
  domain_lengths = coordinates_max .- coordinates_min
  for i in 1:NDIMS
    @. vertex_coordinates[i] = 0.5 * (vertex_coordinates[i] + 1) * domain_lengths[i] + coordinates_min[i]
  end

  md = MeshData(vertex_coordinates, EToV, dg.basis)
  if NDIMS == 1
    md = StartUpDG.make_periodic(md, periodicity...)
  else
    md = StartUpDG.make_periodic(md, periodicity)
  end
  boundary_faces = StartUpDG.tag_boundary_faces(md, is_on_boundary)
  return DGMultiMesh(dg, GeometricTermsType(Cartesian(), dg), md, boundary_faces)
end

"""
    DGMultiMesh(dg::DGMulti{NDIMS}, cells_per_dimension, mapping;
                is_on_boundary=nothing,
                periodicity=ntuple(_ -> false, NDIMS), kwargs...) where {NDIMS}

Constructs a `Curved()` [`DGMultiMesh`](@ref) with element type `dg.basis.elementType`.
- `mapping` is a function which maps from a reference [-1, 1]^NDIMS domain to a mapped domain,
   e.g., `xy = mapping(x, y)` in 2D.
- `is_on_boundary` specifies boundary using a `Dict{Symbol, <:Function}`
- `periodicity` is a tuple of `Bool`s specifying periodicity = `true`/`false` in the (x,y,z) direction.
"""
function DGMultiMesh(dg::DGMulti{NDIMS}, cells_per_dimension, mapping;
                     is_on_boundary=nothing,
                     periodicity=ntuple(_ -> false, NDIMS), kwargs...) where {NDIMS}

  vertex_coordinates, EToV = StartUpDG.uniform_mesh(dg.basis.elementType, cells_per_dimension...)
  md = MeshData(vertex_coordinates, EToV, dg.basis)
  md = NDIMS==1 ? StartUpDG.make_periodic(md, periodicity...) : StartUpDG.make_periodic(md, periodicity)

  @unpack xyz = md
  for i in eachindex(xyz[1])
    new_xyz = mapping(getindex.(xyz, i)...)
    setindex!.(xyz, new_xyz, i)
  end
  md_curved = MeshData(dg.basis, md, xyz...)

  # interpolate geometric terms to both volume and face cubature points
  @unpack rstxyzJ = md_curved
  @unpack Vq, Vf = dg.basis
  rstxyzJ_interpolated = map(x -> [Vq; Vf] * x, rstxyzJ)
  md_curved = @set md_curved.rstxyzJ = rstxyzJ_interpolated

  boundary_faces = StartUpDG.tag_boundary_faces(md_curved, is_on_boundary)
  return DGMultiMesh(dg, GeometricTermsType(Curved(), dg), md_curved, boundary_faces)
end

# Todo: DGMulti. Add traits for dispatch on affine/curved meshes here.

# Matrix type for lazy construction of physical differentiation matrices
# Constructs a lazy linear combination of B = ∑_i coeffs[i] * A[i]
struct LazyMatrixLinearCombo{Tcoeffs, N, Tv, TA <: AbstractMatrix{Tv}} <: AbstractMatrix{Tv}
  matrices::NTuple{N, TA}
  coeffs::NTuple{N, Tcoeffs}
  function LazyMatrixLinearCombo(matrices, coeffs)
    @assert all(matrix -> size(matrix) == size(first(matrices)), matrices)
    new{typeof(first(coeffs)), length(matrices), eltype(first(matrices)), typeof(first(matrices))}(matrices, coeffs)
  end
end
Base.eltype(A::LazyMatrixLinearCombo) = eltype(first(A.matrices))
Base.IndexStyle(A::LazyMatrixLinearCombo) = IndexCartesian()
Base.size(A::LazyMatrixLinearCombo) = size(first(A.matrices))

@inline function Base.getindex(A::LazyMatrixLinearCombo{<:Real, N}, i, j) where {N}
  val = zero(eltype(A))
  for k in Base.OneTo(N)
    val = val + A.coeffs[k] * getindex(A.matrices[k], i, j)
  end
  return val
end

# `SimpleKronecker` lazily stores a Kronecker product `kron(ntuple(A, NDIMS)...)`.
# This object also allocates some temporary storage to enable the fast computation
# of matrix-vector products.
struct SimpleKronecker{NDIMS, TA, Ttmp}
  A::TA
  tmp_storage::Ttmp # temporary array used for Kronecker multiplication
end

# constructor for SimpleKronecker which requires specifying only `NDIMS` and
# the 1D matrix `A`.
function SimpleKronecker(NDIMS, A, eltype_A=eltype(A))
  @assert size(A, 1) == size(A, 2) # check if square
  tmp_storage=[zeros(eltype_A, ntuple(_ -> size(A, 2), NDIMS)...) for _ in 1:Threads.nthreads()]
  return SimpleKronecker{NDIMS, typeof(A), typeof(tmp_storage)}(A, tmp_storage)
end

# Computes `b = kron(A, A) * x` in an optimized fashion
function LinearAlgebra.mul!(b_in, A_kronecker::SimpleKronecker{2}, x_in)

  @unpack A = A_kronecker
  tmp_storage = A_kronecker.tmp_storage[Threads.threadid()]
  n = size(A, 2)

  # copy `x_in` to `tmp_storage` to avoid mutating the input
  @assert length(tmp_storage) == length(x_in)
  for i in eachindex(tmp_storage)
    tmp_storage[i] = x_in[i]
  end
  x = reshape(tmp_storage, n, n)
  b = reshape(b_in, n, n)

  @turbo for j in 1:n, i in 1:n
    tmp = zero(eltype(x))
    for ii in 1:n
      tmp = tmp + A[i, ii] * x[ii, j]
    end
    b[i, j] = tmp
  end

  @turbo for j in 1:n, i in 1:n
    tmp = zero(eltype(x))
    for jj in 1:n
      tmp = tmp + A[j, jj] * b[i, jj]
    end
    x[i, j] = tmp
  end

  @turbo for i in eachindex(b_in)
    b_in[i] = x[i]
  end

  return nothing
end

# Computes `b = kron(A, A, A) * x` in an optimized fashion
function LinearAlgebra.mul!(b_in, A_kronecker::SimpleKronecker{3}, x_in)

  @unpack A = A_kronecker
  tmp_storage = A_kronecker.tmp_storage[Threads.threadid()]
  n = size(A, 2)

  # copy `x_in` to `tmp_storage` to avoid mutating the input
  for i in eachindex(tmp_storage)
    tmp_storage[i] = x_in[i]
  end
  x = reshape(tmp_storage, n, n, n)
  b = reshape(b_in, n, n, n)

  @turbo for k in 1:n, j in 1:n, i in 1:n
    tmp = zero(eltype(x))
    for ii in 1:n
      tmp = tmp + A[i, ii] * x[ii, j, k]
    end
    b[i, j, k] = tmp
  end

  @turbo for k in 1:n, j in 1:n, i in 1:n
    tmp = zero(eltype(x))
    for jj in 1:n
      tmp = tmp + A[j, jj] * b[i, jj, k]
    end
    x[i, j, k] = tmp
  end

  @turbo for k in 1:n, j in 1:n, i in 1:n
    tmp = zero(eltype(x))
    for kk in 1:n
      tmp = tmp + A[k, kk] * x[i, j, kk]
    end
    b[i, j, k] = tmp
  end

  return nothing
end

end # @muladd


mutable struct ExaTronProblem{VI, VD, TM <: AbstractTronMatrix}
    n::Cint                 # number of variables
    nnz::Integer            # number of Hessian entries
    nnz_a::Integer          # number of Hessian entries in the strict lower
    A::TM
    B::TM
    L::TM
    indfree::VI   # a working array of dimension n
    iwa::VI       # a working array of dimension 3*n
    g::VD         # gradient
    xc::VD        # a working array of dimension n
    s::VD         # a working array of dimension n
    wa::VD        # a working array of dimension 7*n

    x::VD
    x_l::VD
    x_u::VD
    rows::VI
    cols::VI
    values::VD

    eval_f::Function
    eval_grad_f::Function
    eval_h::Function

    options::Dict{String, Any}

    # Statistics
    delta::Cdouble
    f::Cdouble
    gnorm_two::Float64
    gnorm_inf::Float64
    nfev::Integer
    ngev::Integer
    nhev::Integer
    minor_iter::Integer
    status::Symbol

    ExaTronProblem{VI, VD, TM}() where {VI, VD, TM} = new{VI, VD, TM}()
end

function gpnorm(n, x, x_l, x_u, g)
    two_norm = 0.0
    inf_norm = 0.0

    for i=1:n
        if x_l[i] != x_u[i]
            if x[i] == x_l[i]
                val = (min(g[i], 0.0))^2
            elseif x[i] == x_u[i]
                val = (max(g[i], 0.0))^2
            else
                val = g[i]^2
            end

            two_norm += val
            val = sqrt(val)
            if inf_norm < val
                inf_norm = val
            end
        end
    end

    two_norm = sqrt(two_norm)

    return two_norm, inf_norm
end

function set_default_options!(prob::ExaTronProblem)
    prob.options = Dict{String,Any}()
    set_default_options!(prob.options)
    return
end

function set_default_options!(options::Dict{String, Any})
    options["max_feval"] = 500
    options["max_minor"] = 200
    options["p"] = 5
    options["verbose"] = 0
    options["tol"] = 1e-6
    options["fatol"] = 0.0
    options["frtol"] = 1e-12
    options["fmin"] = -1e32
    options["cgtol"] = 0.1
    options["tron_code"] = :Julia
    options["matrix_type"] = :Sparse

    return
end

function getOption(prob::ExaTronProblem, keyword::String)
    if !haskey(prob.options, keyword)
        error("ExaTron does not have option with name $(keyword).")
    end
    return prob.options[keyword]
end

function setOption(prob::ExaTronProblem, keyword::String, value::Symbol)
    if !haskey(prob.options, keyword)
        error("ExaTron does not have option with name $(keyword).")
    end
    prob.options[keyword] = value
end

function setOption(prob::ExaTronProblem, keyword::String, value::Integer)
    if !haskey(prob.options, keyword)
        error("ExaTron does not have option with name $(keyword).")
    end
    prob.options[keyword] = value
    return
end

function setOption(prob::ExaTronProblem, keyword::String, value::Float64)
    if !haskey(prob.options, keyword)
        error("ExaTron does not have option with name $(keyword).")
    end
    prob.options[keyword] = value
    return
end

function instantiate_memory!(tron::ExaTronProblem{VI,VD}, n, nele_hess) where {VI,VD}
    tron.n = convert(Cint, n)
    tron.nnz = nele_hess
    tron.indfree = VI(undef, n)
    tron.iwa = VI(undef, 3*n)

    tron.g = tron_zeros(VD, n)
    tron.xc = tron_zeros(VD, n)
    tron.s = tron_zeros(VD, n)
    tron.wa = tron_zeros(VD, 7*n)

    tron.x = tron_zeros(VD, n)
    tron.x_l = tron_zeros(VD, n)
    tron.x_u = tron_zeros(VD, n)
    tron.rows = VI(undef, nele_hess)
    tron.cols = VI(undef, nele_hess)
    tron.values = tron_zeros(VD, nele_hess)
end

function createProblem(n::Integer, x_l::AbstractVector{Float64}, x_u::AbstractVector{Float64},
                       nele_hess::Integer, eval_f_cb, eval_grad_f_cb, eval_h_cb; options...)
    @assert n == length(x_l) == length(x_u)
    @assert typeof(x_l) == typeof(x_u)

    # Load options first, as we need to know :matrix_type for type inference
    options_dict = Dict{String, Any}()
    set_default_options!(options_dict)
    for (name, value) in options
        options_dict[string(name)] = value
    end

    VI = Vector{Cint}
    VD = typeof(x_l)

    if options_dict["matrix_type"] == :Sparse
        TM = TronSparseMatrixCSC{VI, VD}
    else
        AT = isa(x_l, Array) ? Array{Float64, 2} : CuArray{Float64, 2}
        TM = TronDenseMatrix{AT}
    end

    tron = ExaTronProblem{VI, VD, TM}()
    tron.options = options_dict
    instantiate_memory!(tron, n, Int64(nele_hess))
    copyto!(tron.x_l, 1, x_l, 1, n)
    copyto!(tron.x_u, 1, x_u, 1, n)

    tron.eval_f = eval_f_cb
    tron.eval_grad_f = eval_grad_f_cb
    tron.eval_h = eval_h_cb

    tron.eval_h(tron.x, :Structure, tron.rows, tron.cols, 1.0, Float64[], Float64[])
    # Instantiate sparse matrix
    p = tron.options["p"]

    if tron.options["matrix_type"] == :Sparse
        tron.A = TronSparseMatrixCSC(tron.rows, tron.cols, tron.values, n)
        tron.B = TronSparseMatrixCSC{VI, VD}(n, Int64(nele_hess))
        tron.L = TronSparseMatrixCSC{VI, VD}(n, Int64(nele_hess + n*p))
        tron.nnz_a = tron.A.nnz
    else
        tron.A = TronDenseMatrix(tron.rows, tron.cols, tron.values, n)
        if isa(x_l, Array)
            tron.B = TronDenseMatrix{Array{Float64, 2}}(n)
            tron.L = TronDenseMatrix{Array{Float64, 2}}(n)
        else
            tron.B = TronDenseMatrix{CuArray{Float64, 2}}(n)
            tron.L = TronDenseMatrix{CuArray{Float64, 2}}(n)
        end
        tron.nnz_a = n*n
    end
    tron.status = :NotSolved

    return tron
end

function solveProblem(tron::ExaTronProblem)
    task = Vector{UInt8}(undef, 60)
    for (i,s) in enumerate("START")
        task[i] = UInt8(s)
    end

    VI = Vector{Cint}
    VD = typeof(tron.x)

    isave = VI(undef, 3)
    dsave = tron_zeros(VD, 3)
    fatol = tron.options["fatol"]::Float64
    frtol = tron.options["frtol"]::Float64
    fmin = tron.options["fmin"]::Float64
    cgtol = tron.options["cgtol"]::Float64
    gtol = tron.options["tol"]::Float64
    max_feval = tron.options["max_feval"]::Int
    tcode = tron.options["tron_code"]::Symbol
    max_minor = tron.options["max_minor"]::Int

    # Project x into its bound. Tron requires x to be feasible.
    tron.x .= max.(tron.x_l, min.(tron.x_u, tron.x))

    itermax = tron.n
    tron.minor_iter = 0
    tron.nfev = tron.ngev = tron.nhev = 0
    tron.status = :NotSolved
    search = true

    while (search)

        # Evaluate the function.

        if Char(task[1]) == 'F' || unsafe_string(pointer(task), 5) == "START"
            tron.f = tron.eval_f(tron.x)
            tron.nfev += 1
            if tron.nfev >= max_feval
                tron.status = :Maximum_Iterations_Exceeded
                search = false
            end
        end

        # Evaluate the gradient and Hessian.

        if (Char(task[1]) == 'G' && Char(task[2]) == 'H') || unsafe_string(pointer(task), 5) == "START"
            tron.eval_grad_f(tron.x, tron.g)
            tron.eval_h(tron.x, :Values, Int[], Int[], 1.0, Float64[], tron.values)
            tron.ngev += 1
            tron.nhev += 1
            tron.minor_iter += 1

            # Copy values in the CSC matrix.
            fill!(tron.A, 0.0)
            transfer!(tron.A, tron.rows, tron.cols, tron.values, tron.nnz)
        end

        # Initialize the trust region bound.

        if unsafe_string(pointer(task), 5) == "START"
            gnorm0 = norm(tron.g)
            tron.delta = gnorm0
        end

        # Call Tron.

        if search
            tron.delta = ExaTronKernels.dtron(tron.n, tron.x, tron.x_l, tron.x_u,
                            tron.f, tron.g, tron.A,
                            frtol, fatol,
                            fmin, cgtol, itermax, tron.delta,
                            task, tron.B, tron.L,
                            tron.xc, tron.s, tron.indfree,
                            isave, dsave, tron.wa, tron.iwa)
        end

        if unsafe_string(pointer(task), 4) == "NEWX"
            tron.gnorm_two, tron.gnorm_inf = gpnorm(tron.n, tron.x, tron.x_l, tron.x_u, tron.g)
            if tron.gnorm_inf <= gtol
                for (i,s) in enumerate("CONVERGENCE: GTOL TEST SATISFIED")
                    task[i] = UInt8(s)
                end
            end

            if tron.minor_iter >= max_minor
                tron.status = :Maximum_Iterations_Exceeded
                search = false
            end
        end

        if unsafe_string(pointer(task), 4) == "CONV"
            tron.status = :Solve_Succeeded
            search = false
        end

        if unsafe_string(pointer(task), 4) == "WARN"
            tron.status = :TooSmall
            search = false
        end
    end

    return tron.status
end


using MathOptInterface
const MOI = MathOptInterface

include("permuted_exponential_cone.jl")

MOI.Utilities.@product_of_sets(Zeros, MOI.Zeros)

MOI.Utilities.@product_of_sets(
    Cones,
    MOI.Nonnegatives,
    MOI.SecondOrderCone,
    PermutedExponentialCone
)

MOI.Utilities.@struct_of_constraints_by_set_types(
    ZerosOrNot,
    MOI.Zeros,
    Union{MOI.Nonnegatives,MOI.SecondOrderCone,PermutedExponentialCone},
)

# !!! danger
#     Make sure to use ECOS's definition of pfloat for double and idxint for
#     int. These are _not_ the same across platforms, differ from Julia's
#     Cdouble and Clong, and even differ across patch releases of ECOS :(.

const OptimizerCache = MOI.Utilities.GenericModel{
    pfloat,
    MOI.Utilities.ObjectiveContainer{pfloat},
    MOI.Utilities.VariablesContainer{pfloat},
    ZerosOrNot{pfloat}{
        MOI.Utilities.MatrixOfConstraints{
            pfloat,
            MOI.Utilities.MutableSparseMatrixCSC{
                pfloat,
                idxint,
                MOI.Utilities.ZeroBasedIndexing,
            },
            Vector{pfloat},
            Zeros{pfloat},
        },
        MOI.Utilities.MatrixOfConstraints{
            pfloat,
            MOI.Utilities.MutableSparseMatrixCSC{
                pfloat,
                idxint,
                MOI.Utilities.ZeroBasedIndexing,
            },
            Vector{pfloat},
            Cones{pfloat},
        },
    },
}

mutable struct _Solution
    ret_val::Union{Nothing,idxint}
    primal::Vector{pfloat}
    dual_eq::Vector{pfloat}
    dual_ineq::Vector{pfloat}
    slack::Vector{pfloat}
    slack_eq::Vector{pfloat}
    objective_value::pfloat
    dual_objective_value::pfloat
    solve_time::pfloat
    iter::idxint
end

# The values used by ECOS are from -7 to 10 so -10 should be safe
function _Solution()
    return _Solution(
        nothing,
        pfloat[],
        pfloat[],
        pfloat[],
        pfloat[],
        pfloat[],
        NaN,
        NaN,
        NaN,
        0,
    )
end

mutable struct Optimizer <: MOI.AbstractOptimizer
    zeros::Union{Nothing,Zeros{pfloat}}
    cones::Union{Nothing,Cones{pfloat}}
    sol::_Solution
    silent::Bool
    options::Dict{Symbol,Any}
    function Optimizer(; kwargs...)
        return new(nothing, nothing, _Solution(), false, Dict{Symbol,Any}())
    end
end

function MOI.is_empty(optimizer::Optimizer)
    return optimizer.zeros === nothing && optimizer.cones === nothing
end

function MOI.empty!(optimizer::Optimizer)
    optimizer.zeros = nothing
    optimizer.cones = nothing
    optimizer.sol = _Solution()
    return
end

function MOI.get(::Optimizer, ::MOI.Bridges.ListOfNonstandardBridges)
    return [PermutedExponentialBridge{pfloat}]
end

function _rows(
    optimizer::Optimizer,
    ci::MOI.ConstraintIndex{MOI.VectorAffineFunction{pfloat},MOI.Zeros},
)
    return MOI.Utilities.rows(optimizer.zeros, ci)
end

function _rows(
    optimizer::Optimizer,
    ci::MOI.ConstraintIndex{MOI.VectorAffineFunction{pfloat}},
)
    return MOI.Utilities.rows(optimizer.cones, ci)
end

MOI.get(::Optimizer, ::MOI.SolverName) = "ECOS"

MOI.get(::Optimizer, ::MOI.SolverVersion) = unsafe_string(ECOS_ver())

# MOI.RawOptimizerAttribute

function MOI.supports(::Optimizer, param::MOI.RawOptimizerAttribute)
    return hasfield(settings, Symbol(param.name))
end

function MOI.set(optimizer::Optimizer, param::MOI.RawOptimizerAttribute, value)
    optimizer.options[Symbol(param.name)] = value
    return
end

function MOI.get(optimizer::Optimizer, param::MOI.RawOptimizerAttribute)
    # TODO(odow): This gives a poor error message if the name of the parameter
    # is invalid.
    return optimizer.options[Symbol(param.name)]
end

# MOI.Silent

MOI.supports(::Optimizer, ::MOI.Silent) = true

function MOI.set(optimizer::Optimizer, ::MOI.Silent, value::Bool)
    optimizer.silent = value
    return
end

MOI.get(optimizer::Optimizer, ::MOI.Silent) = optimizer.silent

# MOI.supports

function MOI.supports(
    ::Optimizer,
    ::Union{
        MOI.ObjectiveSense,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{pfloat}},
    },
)
    return true
end

function MOI.supports_constraint(
    ::Optimizer,
    ::Type{MOI.VectorAffineFunction{pfloat}},
    ::Type{
        <:Union{
            MOI.Zeros,
            MOI.Nonnegatives,
            MOI.SecondOrderCone,
            PermutedExponentialCone,
        },
    },
)
    return true
end

function _optimize!(dest::Optimizer, src::OptimizerCache)
    MOI.empty!(dest)
    Ab = MOI.Utilities.constraints(
        src.constraints,
        MOI.VectorAffineFunction{pfloat},
        MOI.Zeros,
    )
    A = Ab.coefficients
    Gh = MOI.Utilities.constraints(
        src.constraints,
        MOI.VectorAffineFunction{pfloat},
        MOI.Nonnegatives,
    )
    G = Gh.coefficients
    soc_indices = MOI.get(
        Gh,
        MOI.ListOfConstraintIndices{
            MOI.VectorAffineFunction{pfloat},
            MOI.SecondOrderCone,
        }(),
    )
    q = MOI.dimension.(MOI.get.(Gh, MOI.ConstraintSet(), soc_indices))
    @assert A.n == G.n
    max_sense = MOI.get(src, MOI.ObjectiveSense()) == MOI.MAX_SENSE
    obj =
        MOI.get(src, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{pfloat}}())
    objective_constant = MOI.constant(obj)
    c0 = zeros(A.n)
    for term in obj.terms
        c0[term.variable.value] += term.coefficient
    end
    dest.zeros = deepcopy(Ab.sets) # TODO copy(Ab.sets)
    dest.cones = deepcopy(Gh.sets) # TODO copy(Gh.sets)
    options = copy(dest.options)
    if dest.silent
        options[:verbose] = false
    end
    num_exponential = MOI.get(
        Gh,
        MOI.NumberOfConstraints{
            MOI.VectorAffineFunction{pfloat},
            PermutedExponentialCone,
        }(),
    )
    # This is before ECOS_setup because ECOS modifies the constant vectors
    # in-place!
    slack_eq = copy(Ab.constants)
    inner = ECOS_setup(
        A.n,
        G.m,
        A.m,
        Gh.sets.num_rows[1],
        length(q),
        q,
        num_exponential,
        # Negated because ECOS uses `h - Gx ∈ K`.
        -G.nzval,
        G.colptr,
        G.rowval,
        # Negated because ECOS uses `b -  Ax = 0`.
        -A.nzval,
        A.colptr,
        A.rowval,
        max_sense ? -c0 : c0,
        Gh.constants,
        Ab.constants,
    )
    if inner == C_NULL
        error("ECOS failed to construct problem.")
    end
    unsafe_add_settings(inner, options)
    ret_val = ECOS_solve(inner)
    ecos_prob = unsafe_load(inner)::pwork
    stat = unsafe_load(ecos_prob.info)::stats
    x = copy(unsafe_wrap(Array, ecos_prob.x, ecos_prob.n))
    for col in 1:A.n
        for i in A.colptr[col]:(A.colptr[col+1]-1)
            slack_eq[A.rowval[i+1]+1] += A.nzval[i+1] * x[col]
        end
    end
    dest.sol = _Solution(
        ret_val,
        x,
        copy(unsafe_wrap(Array, ecos_prob.y, ecos_prob.p)),
        copy(unsafe_wrap(Array, ecos_prob.z, ecos_prob.m)),
        copy(unsafe_wrap(Array, ecos_prob.s, ecos_prob.m)),
        slack_eq,
        max_sense ? -stat.pcost : stat.pcost,
        max_sense ? -stat.dcost : stat.dcost,
        stat.tsetup + stat.tsolve,
        stat.iter,
    )
    ECOS_cleanup(inner, 0)
    if !MOI.Utilities.is_ray(MOI.get(dest, MOI.PrimalStatus()))
        dest.sol.objective_value += objective_constant
    end
    if MOI.Utilities.is_ray(MOI.get(dest, MOI.DualStatus()))
        # ECOS can return very large rays here! Without this rescaling it fails
        # many of the MOI tests due to rounding error in floating-point.
        len = sqrt(sum(dest.sol.dual_ineq .^ 2) + sum(dest.sol.dual_eq .^ 2))
        dest.sol.dual_ineq ./= len
        dest.sol.dual_eq ./= len
        dest.sol.dual_objective_value /= len
    else
        dest.sol.dual_objective_value += objective_constant
    end
    return
end

function MOI.optimize!(dest::Optimizer, src::OptimizerCache)
    _optimize!(dest, src)
    return MOI.Utilities.identity_index_map(src), false
end

function MOI.optimize!(dest::Optimizer, src::MOI.ModelLike)
    cache = OptimizerCache()
    index_map = MOI.copy_to(cache, src)
    _optimize!(dest, cache)
    return index_map, false
end

MOI.get(optimizer::Optimizer, ::MOI.SolveTimeSec) = optimizer.sol.solve_time

function MOI.get(optimizer::Optimizer, ::MOI.BarrierIterations)
    return Int64(optimizer.sol.iter)
end

function MOI.get(optimizer::Optimizer, ::MOI.RawStatusString)
    # Strings from https://github.com/ifa-ethz/ecos/blob/master/include/ecos.h
    flag = optimizer.sol.ret_val
    if flag === nothing
        return "Optimize not called"
    elseif flag == ECOS_OPTIMAL
        return "Problem solved to optimality"
    elseif flag == ECOS_OPTIMAL + ECOS_INACC_OFFSET
        return "Problem solved to inaccurate optimality"
    elseif flag == ECOS_PINF
        return "Found certificate of primal infeasibility"
    elseif flag == ECOS_PINF + ECOS_INACC_OFFSET
        return "Found inaccurate certificate of primal infeasibility"
    elseif flag == ECOS_DINF
        return "Found certificate of dual infeasibility"
    elseif flag == ECOS_DINF + ECOS_INACC_OFFSET
        return "Found inaccurate certificate of dual infeasibility"
    elseif flag == ECOS_MAXIT
        return "Maximum number of iterations reached"
    elseif flag == ECOS_NUMERICS
        return "Search direction unreliable"
    elseif flag == ECOS_OUTCONE
        return "s or z got outside the cone, numerics?"
    elseif flag == ECOS_SIGINT
        return "solver interrupted by a signal/ctrl-c"
    else
        @assert flag == ECOS_FATAL
        return "Unknown problem in solver"
    end
end

# Implements getter for result value and statuses
function MOI.get(optimizer::Optimizer, ::MOI.TerminationStatus)
    flag = optimizer.sol.ret_val
    if flag === nothing
        return MOI.OPTIMIZE_NOT_CALLED
    elseif flag == ECOS_OPTIMAL
        return MOI.OPTIMAL
    elseif flag == ECOS_PINF
        return MOI.INFEASIBLE
    elseif flag == ECOS_DINF
        return MOI.DUAL_INFEASIBLE
    elseif flag == ECOS_MAXIT
        return MOI.ITERATION_LIMIT
    elseif flag == ECOS_NUMERICS || flag == ECOS_OUTCONE
        return MOI.NUMERICAL_ERROR
    elseif flag == ECOS_SIGINT
        return MOI.INTERRUPTED
    elseif flag == ECOS_FATAL
        return MOI.OTHER_ERROR
    elseif flag == ECOS_OPTIMAL + ECOS_INACC_OFFSET
        return MOI.ALMOST_OPTIMAL
    elseif flag == ECOS_PINF + ECOS_INACC_OFFSET
        return MOI.ALMOST_INFEASIBLE
    else
        @assert flag == ECOS_DINF + ECOS_INACC_OFFSET
        return MOI.ALMOST_DUAL_INFEASIBLE
    end
end

function MOI.get(optimizer::Optimizer, attr::MOI.ObjectiveValue)
    MOI.check_result_index_bounds(optimizer, attr)
    return optimizer.sol.objective_value
end

function MOI.get(optimizer::Optimizer, attr::MOI.DualObjectiveValue)
    MOI.check_result_index_bounds(optimizer, attr)
    return optimizer.sol.dual_objective_value
end

function MOI.get(optimizer::Optimizer, attr::MOI.PrimalStatus)
    if attr.result_index > MOI.get(optimizer, MOI.ResultCount())
        return MOI.NO_SOLUTION
    end
    flag = optimizer.sol.ret_val
    if flag == ECOS_OPTIMAL
        return MOI.FEASIBLE_POINT
    elseif flag == ECOS_PINF
        return MOI.INFEASIBLE_POINT
    elseif flag == ECOS_DINF
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif flag == ECOS_MAXIT
        return MOI.UNKNOWN_RESULT_STATUS
    elseif flag == ECOS_OPTIMAL + ECOS_INACC_OFFSET
        return MOI.NEARLY_FEASIBLE_POINT
    elseif flag == ECOS_PINF + ECOS_INACC_OFFSET
        return MOI.INFEASIBLE_POINT
    elseif flag == ECOS_DINF + ECOS_INACC_OFFSET
        return MOI.NEARLY_INFEASIBILITY_CERTIFICATE
    else
        return MOI.OTHER_RESULT_STATUS
    end
end

function MOI.get(
    optimizer::Optimizer,
    attr::MOI.VariablePrimal,
    vi::MOI.VariableIndex,
)
    MOI.check_result_index_bounds(optimizer, attr)
    return optimizer.sol.primal[vi.value]
end

function MOI.get(
    optimizer::Optimizer,
    attr::MOI.ConstraintPrimal,
    ci::MOI.ConstraintIndex{MOI.VectorAffineFunction{pfloat},MOI.Zeros},
)
    MOI.check_result_index_bounds(optimizer, attr)
    return optimizer.sol.slack_eq[_rows(optimizer, ci)]
end

function MOI.get(
    optimizer::Optimizer,
    attr::MOI.ConstraintPrimal,
    ci::MOI.ConstraintIndex,
)
    MOI.check_result_index_bounds(optimizer, attr)
    return optimizer.sol.slack[_rows(optimizer, ci)]
end

function MOI.get(optimizer::Optimizer, attr::MOI.DualStatus)
    if attr.result_index > MOI.get(optimizer, MOI.ResultCount())
        return MOI.NO_SOLUTION
    end
    flag = optimizer.sol.ret_val
    if flag == ECOS_OPTIMAL
        return MOI.FEASIBLE_POINT
    elseif flag == ECOS_PINF
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif flag == ECOS_DINF
        return MOI.INFEASIBLE_POINT
    elseif flag == ECOS_MAXIT
        return MOI.UNKNOWN_RESULT_STATUS
    elseif flag == ECOS_OPTIMAL + ECOS_INACC_OFFSET
        return MOI.NEARLY_FEASIBLE_POINT
    elseif flag == ECOS_PINF + ECOS_INACC_OFFSET
        return MOI.NEARLY_INFEASIBILITY_CERTIFICATE
    elseif flag == ECOS_DINF + ECOS_INACC_OFFSET
        return MOI.INFEASIBLE_POINT
    end
    return MOI.OTHER_RESULT_STATUS
end

function MOI.get(
    optimizer::Optimizer,
    attr::MOI.ConstraintDual,
    ci::MOI.ConstraintIndex{MOI.VectorAffineFunction{pfloat},MOI.Zeros},
)
    MOI.check_result_index_bounds(optimizer, attr)
    return optimizer.sol.dual_eq[_rows(optimizer, ci)]
end

function MOI.get(
    optimizer::Optimizer,
    attr::MOI.ConstraintDual,
    ci::MOI.ConstraintIndex{MOI.VectorAffineFunction{pfloat}},
)
    MOI.check_result_index_bounds(optimizer, attr)
    return optimizer.sol.dual_ineq[_rows(optimizer, ci)]
end

MOI.get(optimizer::Optimizer, ::MOI.ResultCount) = 1


# Optimization and solution query
#
#
# OBS: in xpress all functions with LP in name
#      actually refer to CONTINUOUS optimization
#      LP, QP, QCQP, SOCP
#      as opposed to Integer programming

"""
    optimize(model::Model)

Solve model
"""
function optimize(model::Model)
    @assert model.ptr_model != C_NULL
    if is_mip(model)
        mipoptimize(model)
    else
        lpoptimize(model)
    end
    nothing
end

"""
    lpoptimize(model::Model)

Solve models ignoring integrality.
Quadratic terms are NOT ignored.
"""
function lpoptimize(model::Model, flags::String="")
    @assert model.ptr_model != C_NULL
    start_time = time()
    ret = @xprs_ccall(lpoptimize, Cint, (Ptr{Nothing},Ptr{UInt8}),
        model.ptr_model, flags)
    model.time = time() - start_time
    if ret != 0
        throw(XpressError(model))
    end
    nothing
end

function mipoptimize(model::Model, flags::String="")
    @assert model.ptr_model != C_NULL
    start_time = time()
    ret = @xprs_ccall(mipoptimize, Cint, (Ptr{Nothing},Ptr{UInt8}),
        model.ptr_model, flags)
        model.time = time() - start_time
    if ret != 0
        throw(XpressError(model))
    end
    nothing
end

"""
    computeIIS(model::Model)

compute all ISS
"""
function computeIIS(model::Model)
    @assert model.ptr_model != C_NULL
    ret = @xprs_ccall(iisall, Cint, (Ptr{Nothing},), model.ptr_model)
    if ret != 0
        throw(XpressError(model))
    end
    nothing
end

function loaddelayedrows(model::Model, mrows::Array{I}) where I<:Integer
    nrows = length(mrows)
    @assert model.ptr_model != C_NULL
    ret = @xprs_ccall(loaddelayedrows, Cint, (Ptr{Nothing},Cint,Ptr{Cint}),
        model.ptr_model,nrows,convert(Vector{Cint},mrows-1))
    if ret != 0
        throw(XpressError(model))
    end
    nothing
end

#################################################
#
#  solution status and optimization info
#
#################################################

# Unstarted (XPRS_LP_UNSTARTED).
# Optimal (XPRS_LP_OPTIMAL).
# Infeasible (XPRS_LP_INFEAS).
# Objective worse than cutoff (XPRS_LP_CUTOFF).
# Unfinished (XPRS_LP_UNFINISHED).
# Unbounded (XPRS_LP_UNBOUNDED).
# Cutoff in dual (XPRS_LP_CUTOFF_IN_DUAL).
# Problem is unsolved (XPRS_LP_UNSOLVED).
# Problem contains quadratic data, which is not convex (XPRS_LP_NONCONVEX).

@enum LP_Status LP_Unstarted LP_Optimal LP_Infeasible LP_CutOff LP_Unfinished LP_Unbounded LP_CutOffInDual LP_Unsolved LP_NonConvex LP_Unknown

function get_lp_status2(model::Model)
    code = get_lp_status_code(model)
    if code == XPRS_LP_UNSTARTED
        return LP_Unstarted
    elseif code == XPRS_LP_OPTIMAL
        return LP_Optimal
    elseif code == XPRS_LP_INFEAS
        return LP_Infeasible
    elseif code == XPRS_LP_CUTOFF
        return LP_CutOff
    elseif code == XPRS_LP_UNFINISHED
        return LP_Unfinished
    elseif code == XPRS_LP_UNBOUNDED
        return LP_Unbounded
    elseif code == XPRS_LP_CUTOFF_IN_DUAL
        return LP_CutOffInDual
    elseif code == XPRS_LP_UNSOLVED
        return LP_Unsolved
    elseif code == XPRS_LP_NONCONVEX
        return LP_NonConvex
    end
    return LP_Unknown
end

# XPRS_MIP_NOT_LOADED Problem has not been loaded.
# XPRS_MIP_LP_NOT_OPTIMAL Global search incomplete - the initial continuous relaxation has not been solved and no integer solution has been found.
# XPRS_MIP_LP_OPTIMAL Global search incomplete - the initial continuous relaxation has been solved and no integer solution has been found.
# XPRS_MIP_NO_SOL_FOUND Global search incomplete - no integer solution found.
# XPRS_MIP_SOLUTION Global search incomplete - an integer solution has been found.
# XPRS_MIP_INFEAS Global search complete - no integer solution found.
# XPRS_MIP_OPTIMAL Global search complete - integer solution found.
# XPRS_MIP_UNBOUNDED Global search incomplete - the initial continuous relaxation was found to be unbounded. A solution may have been found.

@enum MIP_Status MIP_NotLoaded MIP_LPNotOptimal MIP_LPOptimal MIP_NoSolFound MIP_Solution MIP_Infeasible MIP_Optimal MIP_Unbounded MIP_Unknown

function get_mip_status2(model::Model)
    code = get_mip_status_code(model)
    if code == XPRS_MIP_NOT_LOADED
        return MIP_NotLoaded
    elseif code == XPRS_MIP_LP_NOT_OPTIMAL
        return MIP_LPNotOptimal
    elseif code == XPRS_MIP_LP_OPTIMAL
        return MIP_LPOptimal
    elseif code == XPRS_MIP_NO_SOL_FOUND
        return MIP_NoSolFound
    elseif code == XPRS_MIP_SOLUTION
        return MIP_Solution
    elseif code == XPRS_MIP_INFEAS
        return MIP_Infeasible
    elseif code == XPRS_MIP_OPTIMAL
        return MIP_Optimal
    elseif code == XPRS_MIP_UNBOUNDED
        return MIP_Unbounded
    end
    return MIP_Unknown
end

const status_symbols_lp = Dict(
    XPRS_LP_UNSTARTED         => :unstarted,
    XPRS_LP_OPTIMAL           => :optimal,
    XPRS_LP_INFEAS            => :infeasible,
    XPRS_LP_CUTOFF            => :cutoff,
    XPRS_LP_UNFINISHED        => :unfinished,
    XPRS_LP_UNBOUNDED         => :unbounded,
    XPRS_LP_CUTOFF_IN_DUAL    => :cutoff_in_dual,
    XPRS_LP_UNSOLVED          => :unsolved,
    XPRS_LP_NONCONVEX         => :nonconvex
)

const status_symbols_mip = Dict(
    XPRS_MIP_NOT_LOADED       => :mip_not_loaded,
    XPRS_MIP_LP_NOT_OPTIMAL   => :mip_lp_not_optimal,
    XPRS_MIP_LP_OPTIMAL       => :mip_lp_optimal,
    XPRS_MIP_NO_SOL_FOUND     => :mip_no_sol_found,
    XPRS_MIP_SOLUTION         => :mip_suboptimal,
    XPRS_MIP_INFEAS           => :mip_infeasible,
    XPRS_MIP_OPTIMAL          => :mip_optimal,
    XPRS_MIP_UNBOUNDED        => :mip_unbounded
)

@xprs_int_attr get_mip_status_code XPRS_MIPSTATUS
@xprs_int_attr get_lp_status_code XPRS_LPSTATUS

get_mip_status(model::Model) = status_symbols_mip[get_mip_status_code(model)]::Symbol
get_lp_status(model::Model) = status_symbols_lp[get_lp_status_code(model)]::Symbol

@xprs_int_attr get_sol_count XPRS_MIPSOLS
@xprs_int_attr get_node_count XPRS_NODES

@xprs_int_attr get_simplex_iter_count XPRS_SIMPLEXITER
@xprs_int_attr get_barrier_iter_count XPRS_BARITER

# objective
@xprs_dbl_attr get_lp_objval XPRS_LPOBJVAL
@xprs_dbl_attr get_mip_objval XPRS_MIPBESTOBJVAL
@xprs_dbl_attr get_mip_objval_last XPRS_MIPOBJVAL

@xprs_dbl_attr get_bardualobj XPRS_BARDUALOBJ
@xprs_dbl_attr get_barprimalobj XPRS_BARPRIMALOBJ

@xprs_dbl_attr get_bestbound XPRS_BESTBOUND

@xprs_dbl_attr get_objrhs XPRS_OBJRHS

#iis
@xprs_int_attr num_iis XPRS_NUMIIS

function get_objval(model::Model)
    if num_intents(model)+num_sos(model) > 0
        return get_mip_objval(model)
    else
        return get_lp_objval(model)
    end
end

mutable struct OptimInfo
    status_lp::Symbol
    status_mip::Symbol

    #time ??

    sol_count::Int
    node_count::Int

    simplex_iter_count::Int
    barrier_iter_count::Int

end

function get_optiminfo(model::Model)
    OptimInfo(
        get_lp_status(model),
        get_mip_status(model),

        #get_runtime(model),

        get_sol_count(model),
        get_node_count(model),

        get_simplex_iter_count(model),
        get_barrier_iter_count(model)
    )
end

function show(io::IO, s::OptimInfo)
    println(io, "Xpress Optimization Info")
    println(io, "    lp status    = $(s.status_lp)")
    println(io, "    mip status   = $(s.status_mip)")
    #println(io, "    runtime  = $(s.runtime)")
    println(io, "    # solutions     = $(s.sol_count)")
    println(io, "    # nodes         = $(s.node_count)")

    println(io, "    # simplex iters = $(s.simplex_iter_count)")
    println(io, "    # barrier iters = $(s.barrier_iter_count)")
end

#################################################
#
#  solution query
#
#################################################

function get_complete_lp_solution(model::Model)
    cols = num_vars(model)
    rows = num_constrs(model)

    x = Vector{Float64}(undef, cols)
    slack = Vector{Float64}(undef, rows)
    dual = Vector{Float64}(undef, rows)
    red = Vector{Float64}(undef, cols)

    ret = @xprs_ccall(getlpsol, Cint,
        (Ptr{Nothing},
         Ptr{Float64},
         Ptr{Float64},
         Ptr{Float64},
         Ptr{Float64}
            ), model.ptr_model, x, slack, dual, red)
    if ret != 0
        throw(XpressError(model))
    end
    return x, slack, dual, red
end

"""
    get_lp_solution(model::Model)

return a vector with primal variable solutions
"""
function get_lp_solution(model::Model)
    cols = num_vars(model)
    x = Vector{Float64}(undef, cols)
    get_lp_solution!(model, x)
    return x
end

"""
    get_lp_solution!(model::Model, x::Vector{Float64})

return a vector with primal variable solutions - inplace
"""
function get_lp_solution!(model::Model, x::Vector{Float64})
    _chklen(x, num_vars(model))
    ret = @xprs_ccall(getlpsol, Cint,
        (Ptr{Nothing},
         Ptr{Float64},
         Ptr{Float64},
         Ptr{Float64},
         Ptr{Float64}
            ), model.ptr_model, x, C_NULL, C_NULL, C_NULL)
    if ret != 0
        throw(XpressError(model))
    end
    return nothing
end

"""
    get_lp_slack(model::Model)

return a vector of slack values for rows (some srt of constraint primal solution)
"""
function get_lp_slack(model::Model)
    rows = num_constrs(model)
    slack = Vector{Float64}(undef, rows)
    get_lp_slack!(model, slack)
    return slack
end

"""
    get_lp_slack!(model::Model, slack::Vector{Float64})

return a vector of slack values for rows (some srt of constraint primal solution) - inplace
"""
function get_lp_slack!(model::Model, slack::Vector{Float64})
    _chklen(slack, num_constrs(model))
    ret = @xprs_ccall(getlpsol, Cint,
        (Ptr{Nothing},
         Ptr{Float64},
         Ptr{Float64},
         Ptr{Float64},
         Ptr{Float64}
            ), model.ptr_model, C_NULL, slack, C_NULL, C_NULL)
    if ret != 0
        throw(XpressError(model))
    end
    return nothing
end
function get_lp_slack_lin!(model::Model, place::Vector{Float64})
    if num_qconstrs(model) > 0
        _chklen(place, num_linconstrs(model))

        slack = zeros(num_constrs(model))
        get_lp_slack!(model, slack)

        lrows = get_lrows(model)
        _chklen(lrows, num_linconstrs(model))

        for i in eachindex(lrows)
            place[i] = slack[lrows[i]]
        end
    else
        get_lp_slack!(model, place)
    end

    return nothing
end

"""
    get_lp_dual(model::Model)

Return a vector of constraint dual solution
"""
function get_lp_dual(model::Model)
    rows = num_constrs(model)

    dual = Vector{Float64}(undef, rows)

    get_lp_dual!(model, dual)
    return dual
end

"""
    get_lp_dual!(model::Model, dual::Vector{Float64})

Return a vector of constraint dual solution - inplace
"""
function get_lp_dual!(model::Model, dual::Vector{Float64})
    rows = num_constrs(model)
    _chklen(dual, rows)
    ret = @xprs_ccall(getlpsol, Cint,
        (Ptr{Nothing},
         Ptr{Float64},
         Ptr{Float64},
         Ptr{Float64},
         Ptr{Float64}
            ), model.ptr_model, C_NULL, C_NULL, dual, C_NULL)
    if ret != 0
        throw(XpressError(model))
    end
    return nothing
end
function get_lp_dual_lin!(model::Model, place::Vector{Float64})
    if num_qconstrs(model) > 0
        _chklen(place, num_linconstrs(model))

        dual = zeros(num_constrs(model))
        get_lp_dual!(model, dual)

        lrows = get_lrows(model)
        _chklen(lrows, num_linconstrs(model))

        for i in eachindex(lrows)
            place[i] = dual[lrows[i]]
        end
    else
        get_lp_dual!(model, place)
    end

    return nothing
end

"""
    get_lp_reducedcost(model::Model)

Return a vector of variable reduced cost (dual solution)
"""
function get_lp_reducedcost(model::Model)
    cols = num_vars(model)

    red = Vector{Float64}(undef, cols)

    get_lp_reducedcost!(model, red)
    return red
end

"""
    get_lp_reducedcost!(model::Model, red::Vector{Float64})

Return a vector of variable reduced cost (dual solution) - inplace
"""
function get_lp_reducedcost!(model::Model, red::Vector{Float64})
    cols = num_vars(model)

    _chklen(red, cols)

    ret = @xprs_ccall(getlpsol, Cint,
        (Ptr{Nothing},
         Ptr{Float64},
         Ptr{Float64},
         Ptr{Float64},
         Ptr{Float64}
            ), model.ptr_model, C_NULL, C_NULL, C_NULL, red)
    if ret != 0
        throw(XpressError(model))
    end
    return nothing
end

## mip sol
"""
    get_mip_solution(model::Model)

Return a vector of variable primal solutions
"""
function get_mip_solution(model::Model)
    cols = num_vars(model)

    x = Vector{Float64}(undef, cols)

    get_mip_solution!(model, x)
    return x
end

"""
    get_mip_solution!(model::Model, x::Vector{Float64})

Return a vector of variable primal solutions - inplace
"""
function get_mip_solution!(model::Model, x::Vector{Float64})
    cols = num_vars(model)

    _chklen(x, cols)

    ret = @xprs_ccall(getmipsol, Cint,
        (Ptr{Nothing},
         Ptr{Float64},
         Ptr{Float64}
            ), model.ptr_model, x, C_NULL)
    if ret != 0
        throw(XpressError(model))
    end
    return nothing
end

"""
    get_mip_slack(model::Model)

Return a vector of constraint primal solutions (slacks)
"""
function get_mip_slack(model::Model)
    rows = num_constrs(model)

    slack = Vector{Float64}(undef, rows)

    get_mip_slack!(model, slack)
    return slack
end

"""
    get_mip_slack!(model::Model, slack::Vector{Float64})

Return a vector of constraint primal solutions (slacks) - inplace
"""
function get_mip_slack!(model::Model, slack::Vector{Float64})
    rows = num_constrs(model)
    _chklen(slack, rows)
    ret = @xprs_ccall(getmipsol, Cint,
        (Ptr{Nothing},
         Ptr{Float64},
         Ptr{Float64}
            ), model.ptr_model, C_NULL, slack)
    if ret != 0
        throw(XpressError(model))
    end
    return slack
end

function get_mip_slack_lin!(model::Model, place::Vector{Float64})
    if num_qconstrs(model) > 0
        _chklen(place, num_linconstrs(model))

        slack = zeros(num_constrs(model))
        get_mip_slack!(model, slack)

        lrows = get_lrows(model)
        _chklen(lrows, num_linconstrs(model))

        for i in eachindex(lrows)
            place[i] = slack[lrows[i]]
        end
    else
        get_mip_slack!(model, place)
    end
end

"""
    get_solution!(model::Model, x::Vector{Float64})

Return a vector of variable primal solution - inplace
"""
function get_solution!(model::Model, x::Vector{Float64})
    if is_mip(model)
        return get_mip_solution!(model,x)
    else
        return get_lp_solution!(model,x)
    end
end

"""
    get_solution(model::Model)

Return a vector of variable primal solution
"""
function get_solution(model::Model)
    if is_mip(model)
        return get_mip_solution(model)
    else
        return get_lp_solution(model)
    end
end

"""
    get_slack!(model::Model, slack::Vector{Float64})

Return a vector of constraints slacks - inplace

    get_slack(model::Model)

Return a vector of constraints slacks
"""
function get_slack!(model::Model, slack::Vector{Float64})
    if is_mip(model)
        return get_mip_slack!(model, slack)
    else
        return get_lp_slack!(model, slack)
    end
end
function get_slack(model::Model)
    if is_mip(model)
        return get_mip_slack(model)
    else
        return get_lp_slack(model)
    end
end
function get_slack_lin!(model::Model, slack::Vector{Float64})
    if is_mip(model)
        return get_mip_slack_lin!(model, slack)
    else
        return get_lp_slack_lin!(model, slack)
    end
end

"""
    get_dual!(model::Model, dual::Vector{Float64})

Return a vector of constraint dual solutions
"""
function get_dual!(model::Model, dual::Vector{Float64})
    if is_mip(model)
        error("Not possible to get MIP duals")
        return Float64[]
    else
        return get_lp_dual!(model,dual)
    end
end
function get_dual_lin!(model::Model, dual::Vector{Float64})
    if is_mip(model)
        error("Not possible to get MIP duals")
        return Float64[]
    else
        return get_lp_dual_lin!(model,dual)
    end
end

"""
    get_dual(model::Model)

Return a vector of constraint dual solutions
"""
function get_dual(model::Model)
    if is_mip(model)
        error("Not possible to get MIP duals")
        return Float64[]
    else
        return get_lp_dual(model)
    end
end

"""
    get_reducedcost!(model::Model, red::Vector{Float64})

Return a vector of variable dual solution
"""
function get_reducedcost!(model::Model, red::Vector{Float64})
    if is_mip(model)
        error("Not possible to get MIP reduced costs")
        return Float64[]
    else
        return get_lp_reducedcost!(model, red)
    end
end

"""
    get_reducedcost(model::Model)

Return a vector of variable dual solution
"""
function get_reducedcost(model::Model)
    if is_mip(model)
        error("Not possible to get MIP reduced costs")
        return Float64[]
    else
        return get_lp_reducedcost(model)
    end
end

#################################################
#
#  basis query
#
#################################################
const basicmap = Dict(
    0 => :NonbasicAtLower,#_or_SuperbasicAtZero,
    1 => :Basic,
    2 => :NonbasicAtUpper,
    3 => :Superbasic
)
const basicmap_rev = Dict(
    :NonbasicAtLower => Cint(0),#_or_SuperbasicAtZero,
    :Basic => Cint(1),
    :NonbasicAtUpper => Cint(2),
    :Superbasic => Cint(3)
)

"""
    loadbasis(model::Model, x::Vector)

Load basis to a problem in form of primal solutions

    loadbasis(model::Model, rval::Vector{Symbol}, cval::Vector{Symbol})

Load basis to a problem in terms of basicness description
Variables and constraints ca be: `Basic`, `NonBasicAtLower`, `NonBasicAtUpper` and `Superbasic`
"""
function loadbasis(model::Model, x::Vector)#, status::Symbol = :unstarted, isnew::Vector{Bool} = [false])

    ncols = num_vars(model)
    nrows = num_constrs(model)

    length(x) != ncols && error("solution candidate size is different from the number of columns")

    cvals = Array{Cint}(undef,  ncols)
    rvals = Array{Cint}(undef,  nrows)

    # obtain situation of columns

    lb = get_lb(model)
    ub = get_ub(model)

    for i in 1:ncols
        if isapprox(x[i],lb[i])
            cvals[i] = basicmap_rev[:NonbasicAtLower]
        elseif isapprox(x[i],ub[i])
            cvals[i] = basicmap_rev[:NonbasicAtUpper]
        else
            cvals[i] = basicmap_rev[:Basic]
        end
    end

    # obtain situation of rows where: y = Ax

    A = get_constrmatrix(model) #A is sparse

    y = A*x

    senses = get_rowtype(model)
    rhs    = get_rhs(model)

    for j in 1:nrows
        if senses[j] == XPRS_EQ && isapprox(y[j], rhs[j])
            rvals[j] = basicmap_rev[:NonbasicAtLower]
        elseif senses[j] == XPRS_GEQ && isapprox(y[j], rhs[j])
            rvals[j] = basicmap_rev[:NonbasicAtLower]
        elseif senses[j] == XPRS_LEQ && isapprox(y[j], rhs[j])
            rvals[j] = basicmap_rev[:NonbasicAtUpper]
        else
            rvals[j] = basicmap_rev[:Basic]
        end
    end

    loadbasis(model, rvals, cvals)

    return nothing
end
function loadbasis(model::Model, rval::Vector{Symbol}, cval::Vector{Symbol})

    nrval = map(x->basicmap_rev[x], rval)
    ncval = map(x->basicmap_rev[x], cval)

    loadbasis(model, nrval, ncval)

    return nothing
end
function loadbasis(model::Model, rval::Vector{Cint}, cval::Vector{Cint})
# int XPRS_CC XPRSloadbasis(XPRSprob prob, const int rstatus[], const intcstatus[]);

    ret = @xprs_ccall(loadbasis, Cint,
        (Ptr{Nothing},
         Ptr{Cint},
         Ptr{Cint}
            ), model.ptr_model, rval, cval)
    if ret != 0
        throw(XpressError(model))
    end

    return nothing
end

function get_basis(model::Model)
    cval = Array{Cint}(undef,  num_vars(model))
    cbasis = Array{Symbol}( num_vars(model))

    rval = Array{Cint}(undef,  num_constrs(model))
    rbasis = Array{Symbol}( num_constrs(model))

    ret = @xprs_ccall(getbasis, Cint,
        (Ptr{Nothing},
         Ptr{Cint},
         Ptr{Cint}
            ), model.ptr_model, rval, cval)
    if ret != 0
        throw(XpressError(model))
    end

    for it in 1:length(cval)
        cbasis[it] = basicmap[cval[it]]
    end

    for it in 1:length(rval)
        rbasis[it] = basicmap[rval[it]]
    end
    return cbasis, rbasis
end

#################################################
#
#  IIS query
#
#################################################

function get_iisdata(model::Model, num::Int)
# num is the number of THE IIS to be queried

    rows = Array{Cint}(undef, 1)
    cols = Array{Cint}(undef, 1)
    ret = @xprs_ccall(getiisdata, Cint,
        (Ptr{Nothing},
            Cint,# num
         Ptr{Cint},# #row
         Ptr{Cint},# #cols
         Ptr{Cint},# rows set
         Ptr{Cint},# bounds set
         Ptr{Cchar},# cstr type
         Ptr{Cchar},# bnd type
         Ptr{Float64}, # duals
         Ptr{Float64}, # reduced
         Ptr{Cchar}, # isol rows
         Ptr{Cchar} #isol cols
            ), model.ptr_model, Cint(num), rows,
        cols,C_NULL,C_NULL,
        C_NULL,C_NULL,C_NULL,
        C_NULL,C_NULL,C_NULL,)

    if ret != 0
        throw(XpressError(model))
    end

    rows_set = Array{Cint}(undef,  rows[1])
    cols_set = Array{Cint}(undef,  cols[1])

    ret = @xprs_ccall(getiisdata, Cint,
        (Ptr{Nothing},
            Cint,# num
         Ptr{Cint},# #row
         Ptr{Cint},# #cols
         Ptr{Cint},# rows set
         Ptr{Cint},# bounds set
         Ptr{Cchar},# cstr type
         Ptr{Cchar},# bnd type
         Ptr{Float64}, # duals
         Ptr{Float64}, # reduced
         Ptr{Cchar}, # isol rows
         Ptr{Cchar} #isol cols
            ), model.ptr_model, Cint(num), rows,
        cols,rows_set,cols_set,
        C_NULL,C_NULL,C_NULL,
        C_NULL,C_NULL,C_NULL,)

    if ret != 0
        throw(XpressError(model))
    end

    return rows_set + 1, cols_set + 1
end

#################################################
#
#  infeasibility & unboundedness
#
#################################################

function hasdualray(model::Model)::Bool

    hasray = Array{Cint}(undef,  1)

    ret = @xprs_ccall(getdualray, Cint,
        (Ptr{Nothing},
            Ptr{Float64},
            Ptr{Cint}
            ), model.ptr_model, C_NULL, hasray)
    if ret != 0
        throw(XpressError(model))
    end

    if hasray[1] == 0
        return false
    else
        return true
    end
end

function getdualray!(model::Model, ray::Vector{Float64})

    @assert length(ray) == num_constrs(model)

    hasray = Array{Cint}(undef, 1)

    ret = @xprs_ccall(getdualray, Cint,
        (Ptr{Nothing},
            Ptr{Float64},
            Ptr{Cint}
            ), model.ptr_model, ray, hasray)
    if ret != 0
        throw(XpressError(model))
    end
    return hasray[1] != 0
end

function getdualray(model::Model)

    dray = Array{Float64}(undef,  num_constrs(model))

    if !getdualray!(model, dray)
        Compat.@warn("Xpress solver was unable to provide an infeasibility ray")
        return dray
    end

    return dray
end

function hasprimalray(model::Model)::Bool

    hasray = Array{Cint}(undef,  1)

    ret = @xprs_ccall(getprimalray, Cint,
        (Ptr{Nothing},
            Ptr{Float64},
            Ptr{Cint}
            ), model.ptr_model, C_NULL, hasray)
    if ret != 0
        throw(XpressError(model))
    end

    if hasray[1] == 0
        return false
    else
        return true
    end
end

function getprimalray!(model::Model, ray::Vector{Float64})

    @assert length(ray) == num_vars(model)

    hasray = Array{Cint}(undef,  1)

    ret = @xprs_ccall(getprimalray, Cint,
        (Ptr{Nothing},
            Ptr{Float64},
            Ptr{Cint}
            ), model.ptr_model, ray, hasray)
    if ret != 0
        throw(XpressError(model))
    end
    return hasray[1] != 0
end

function getprimalray(model::Model)

    pray = Array{Float64}(undef,  num_vars(model))

    if !getprimalray!(model, pray)
        Compat.@warn("Xpress solver was unable to provide an unboundedness ray")
        return pray
    end

    return pray
end

function repairweightedinfeasibility(model::Model, scode::Vector{Cint}, lrp::Vector{Float64}, grp::Vector{Float64}, lbp::Vector{Float64}, ubp::Vector{Float64}, phase2::Cchar = Cchar('f'), delta::Float64=0.001, flags::String="")
# int XPRS_CC XPRSrepairweightedinfeas(XPRSprob prob, int *scode, const double lrp[], const double grp[], const double lbp[], const double ubp[], char phase2, double delta, const char *optflags)
    ret = @xprs_ccall(repairweightedinfeas, Cint,
        (Ptr{Nothing},
         Ptr{Cint},
         Ptr{Float64},
         Ptr{Float64},
         Ptr{Float64},
         Ptr{Float64},
         Cchar,
         Float64,
         Ptr{UInt8}),
        model.ptr_model, scode, lrp, grp, lbp, ubp, phase2, delta, flags)
    if ret != 0
        throw(XpressError(model))
    end

    return nothing
end

"""
    repairweightedinfeasibility(model::Model, lrp::Vector{Float64}, grp::Vector{Float64}, lbp::Vector{Float64}, ubp::Vector{Float64}; phase2::Cchar = Cchar('f'), delta::Float64=0.001, flags::String="")
    repairweightedinfeasibility(model::Model, phase2::Cchar = Cchar('f'), delta::Float64=0.001, flags::String="")

Repair infeasibility tool
"""
repairweightedinfeasibility(model::Model, lrp::Vector{Float64}, grp::Vector{Float64}, lbp::Vector{Float64}, ubp::Vector{Float64}; phase2::Cchar = Cchar('f'), delta::Float64=0.001, flags::String="") = repairweightedinfeasibility(model::Model, lrp::Vector{Float64}, grp::Vector{Float64}, lbp::Vector{Float64}, ubp::Vector{Float64}, phase2, delta, flags)

function repairweightedinfeasibility(model::Model, lrp::Vector{Float64}, grp::Vector{Float64}, lbp::Vector{Float64}, ubp::Vector{Float64}, phase2::Cchar, delta::Float64, flags::String)

    cols = num_vars(model)
    rows = num_constrs(model)

    _chklen(rows, lrp)
    _chklen(rows, grp)
    _chklen(cols, lbp)
    _chklen(cols, ubp)

    scode = zeros(Cint,1)

    repairweightedinfeasibility(model, scode, lrp, grp, lbp, ubp, phase2, delta, flags)

    return scode[1]
end

function repairweightedinfeasibility(model::Model, phase2::Cchar = Cchar('f'), delta::Float64=0.001, flags::String="")
    cols = num_vars(model)
    rows = num_constrs(model)
    lrp = ones(rows)
    grp = ones(rows)
    lbp = ones(cols)
    ubp = ones(cols)

    return repairweightedinfeasibility(model, lrp, grp, lbp, ubp, phase2, delta, flags)
end

@xprs_int_attr stopstatus XPRS_STOPSTATUS

@enum StopStatus StopNone StopTimeLimit StopControlC StopNodeLimit StopIterLimit StopMIPGap StopSolLimit StopUser StopUnknown

function get_stopstatus(m::Model)
    ss = stopstatus(m)

    if ss == XPRS_STOP_NONE
        return StopNone
    elseif ss == XPRS_STOP_TIMELIMIT
        return StopTimeLimit
    elseif ss == XPRS_MAXTIME
        return StopTimeLimit
    elseif ss == XPRS_STOP_CTRLC
        return StopControlC
    elseif ss == XPRS_STOP_NODELIMIT
        return StopNodeLimit
    elseif ss == XPRS_STOP_ITERLIMIT
        return StopIterLimit
    elseif ss == XPRS_STOP_MIPGAP
        return StopMIPGap
    elseif ss == XPRS_STOP_SOLLIMIT
        return StopSolLimit
    elseif ss == XPRS_STOP_USER
        return StopUser
    end

    return StopUnknown
end


"""
    ArcStandard()

Transition system for for Arc-Standard dependency parsing.

# Transitions

| Transition  | Definition                                         |
|:----------- |:-------------------------------------------------- |
| LeftArc(l)  | (σ\\|s1\\|s0, β, A) → (σ\\|s0, β, A ∪ (s0, l, s1)) |
| RightArc(l) | (σ\\|s, b\\|β, A) → (σ, b\\|β, A ∪ (b, l, s))      |
| Shift	      | (σ,  b\\|β, A) → (σ\\|b, β, A)                     |

# Preconditions

| Transition  | Condition                          |
|:----------- |:---------------------------------- |
| LeftArc(l)  | ¬[s1 = 0], ¬∃k∃l'[(k, l', s1) ϵ A] |
| RightArc(l) | ¬∃k∃l'[(k, l', s0) ϵ A]            |

See [Nivre 2004](https://www.aclweb.org/anthology/W04-0308.pdf).
"""
struct ArcStandard <: AbstractTransitionSystem end

initconfig(s::ArcStandard, graph::DependencyTree) = ArcStandardConfig(graph)
initconfig(s::ArcStandard, words) = ArcStandardConfig(words)

projective_only(::ArcStandard) = true

transition_space(::ArcStandard, labels=[]) =
    isempty(labels) ? [LeftArc(), RightArc(), Shift()] :
    [LeftArc.(labels)..., RightArc.(labels)..., Shift()]

struct ArcStandardConfig <: AbstractParserConfiguration
    stack::Vector{Int}
    buffer::Vector{Int}
    A::Vector{Token}
end

ArcStandardConfig(sentence) = stack_buffer_config(ArcStandardConfig, sentence)

buffer(cfg::ArcStandardConfig) = cfg.buffer
stack(cfg::ArcStandardConfig)  = cfg.stack
tokens(cfg::ArcStandardConfig) = cfg.A

function Base.show(io::IO, cfg::ArcStandardConfig)
    print(io, "$(typeof(cfg))($(stack(cfg)),$(buffer(cfg)))")
    for (i, t) in enumerate(tokens(cfg))
        print("(",join((i, t.form, t.head), ","), ")")
    end
end

function apply_transition(f, cfg::ArcStandardConfig, a...; k...)
    σ, β, A = f(cfg.stack, cfg.buffer, cfg.A, a...; k...)
    return ArcStandardConfig(σ, β, A)
end

leftarc(cfg::ArcStandardConfig, args...; kwargs...) =
    apply_transition(leftarc_reduce2, cfg, args...; kwargs...)

rightarc(cfg::ArcStandardConfig, args...; kwargs...) =
    apply_transition(rightarc_reduce, cfg, args...; kwargs...)

shift(cfg::ArcStandardConfig) =
    apply_transition(shift, cfg)

isfinal(cfg::ArcStandardConfig) =
    length(stack(cfg)) == 1 && stack(cfg)[1] == 0 && length(buffer(cfg)) == 0

"""
    static_oracle(cfg, gold_tree)

Static oracle function for arc-standard dependency parsing.
"""
function static_oracle(cfg::ArcStandardConfig, gold_tree, arc=untyped)
    l = i -> arc(gold_tree[i])
    s = stack(cfg)
    if length(s) >= 2
        s1, s0 = s[end-1], s[end]
        if has_arc(gold_tree, s0, s1)
            return LeftArc(l(s1)...)
        elseif has_arc(gold_tree, s1, s0)
            if !any(k -> (k in s || k in buffer(cfg)), deps(gold_tree, s0))
                return RightArc(l(s0)...)
            end
        end
    end
    return Shift()
end

function is_possible(::LeftArc, cfg::ArcStandardConfig)
    if stacklength(cfg) >= 2
        s, s1, s0 = popstack(cfg, 2)
        return s1 != 0 && !has_head(token(cfg, s1))
    else
        return false
    end
end

is_possible(::RightArc, cfg::ArcStandardConfig) =
    stacklength(cfg) > 1 && !has_head(token(cfg, last(stack(cfg))))

is_possible(::Shift, cfg::ArcStandardConfig) = stacklength(cfg) > 0

function possible_transitions(cfg::ArcStandardConfig, arc=untyped)
    l = i -> arc(token(cfg, i))
    if stacklength(cfg) >= 2
        σ, s1, s0 = popstack(cfg, 2)
        transitions = [LeftArc(l(s1)...), RightArc(l(s0)...), Shift()]
        return filter(t -> is_possible(t, cfg), transitions)
    else
        return filter(t -> is_possible(t, cfg), TransitionOperator[Shift()])
    end
end

possible_transitions(cfg::ArcStandardConfig, gold_tree::DependencyTree, arc=untyped) =
    possible_transitions(cfg, arc)

==(cfg1::ArcStandardConfig, cfg2::ArcStandardConfig) =
    cfg1.stack == cfg2.stack && cfg1.buffer == cfg2.buffer && cfg1.A == cfg2.A


#####
##### Navier-Stokes and tracer advection equations
#####

""" Store previous value of the source term and calculate current source term. """
function calculate_interior_tendency_contributions!(G, arch, grid, coriolis, buoyancy, surface_waves, closure,
                                                    U, C, pHY′, K, F, clock)

    # Manually choose thread-block layout here as it's ~20% faster.
    # See: https://github.com/climate-machine/Oceananigans.jl/pull/308
    Nx, Ny, Nz = grid.Nx, grid.Ny, grid.Nz

    if Nx == 1
        Tx, Ty = 1, min(256, Ny)
        Bx, By, Bz = Tx, floor(Int, Ny/Ty), Nz
    elseif Ny == 1
        Tx, Ty = min(256, Nx), 1
        Bx, By, Bz = floor(Int, Nx/Tx), Ty, Nz
    else
        Tx, Ty = 16, 16
        Bx, By, Bz = floor(Int, Nx/Tx), floor(Int, Ny/Ty), Nz
    end

    @launch(device(arch), threads=(Tx, Ty), blocks=(Bx, By, Bz),
            calculate_Gu!(G.u, grid, coriolis, surface_waves, closure, U, C, K, F, pHY′, clock))

    @launch(device(arch), threads=(Tx, Ty), blocks=(Bx, By, Bz),
            calculate_Gv!(G.v, grid, coriolis, surface_waves, closure, U, C, K, F, pHY′, clock))

    @launch(device(arch), threads=(Tx, Ty), blocks=(Bx, By, Bz),
            calculate_Gw!(G.w, grid, coriolis, surface_waves, closure, U, C, K, F, clock))

    for tracer_index in 1:length(C)
        @inbounds Gc = G[tracer_index+3]
        @inbounds Fc = F[tracer_index+3]
        @inbounds  c = C[tracer_index]

        @launch(device(arch), threads=(Tx, Ty), blocks=(Bx, By, Bz),
                calculate_Gc!(Gc, grid, c, Val(tracer_index), closure, buoyancy, U, C, K, Fc, clock))
    end

    return nothing
end

"""
    calculate_velocity_tendencies_on_boundaries!(tendency_calculation_args...)

Calculate the velocity tendencies *on* east, north, and top boundaries, when the
x-, y-, or z- directions have a `Bounded` topology.
"""
function calculate_velocity_tendencies_on_boundaries!(tendency_calculation_args...)
    calculate_east_boundary_Gu!(tendency_calculation_args...)
    calculate_north_boundary_Gv!(tendency_calculation_args...)
    calculate_top_boundary_Gw!(tendency_calculation_args...)
    return nothing
end

# Fallbacks for non-bounded topologies in x-, y-, and z-directions
@inline calculate_east_boundary_Gu!(args...) = nothing
@inline calculate_north_boundary_Gv!(args...) = nothing
@inline calculate_top_boundary_Gw!(args...) = nothing

"""
    calculate_east_boundary_Gu!(G, arch, grid::AbstractGrid{FT, <:Bounded},
                                coriolis, buoyancy, surface_waves, closure,
                                U, C, pHY′, K, F, clock) where FT

Calculate `Gu` on east boundaries when the x-direction has `Bounded` topology.
"""
function calculate_east_boundary_Gu!(G, arch, grid::AbstractGrid{FT, <:Bounded},
                                     coriolis, buoyancy, surface_waves, closure,
                                     U, C, pHY′, K, F, clock) where FT

    @launch(device(arch), config=launch_config(grid, :yz),
            _calculate_east_boundary_Gu!(G.u, grid, coriolis, surface_waves,
                                         closure, U, C, K, F, pHY′, clock))

    return nothing
end

"""
    calculate_north_boundary_Gu!(G, arch, grid::AbstractGrid{FT, <:Bounded},
                                 coriolis, buoyancy, surface_waves, closure,
                                 U, C, pHY′, K, F, clock) where FT

Calculate `Gv` on north boundaries when the y-direction has `Bounded` topology.
"""
function calculate_north_boundary_Gv!(G, arch, grid::AbstractGrid{FT, TX, <:Bounded},
                                      coriolis, buoyancy, surface_waves, closure,
                                      U, C, pHY′, K, F, clock) where {FT, TX}

    @launch(device(arch), config=launch_config(grid, :xz),
            _calculate_north_boundary_Gv!(G.v, grid, coriolis, surface_waves,
                                          closure, U, C, K, F, pHY′, clock))

    return nothing
end

"""
    calculate_top_boundary_Gw!(G, arch, grid::AbstractGrid{FT, <:Bounded},
                               coriolis, buoyancy, surface_waves, closure,
                               U, C, pHY′, K, F, clock) where FT

Calculate `Gw` on top boundaries when the z-direction has `Bounded` topology.
"""
function calculate_top_boundary_Gw!(G, arch, grid::AbstractGrid{FT, TX, TY, <:Bounded},
                                    coriolis, buoyancy, surface_waves, closure,
                                    U, C, pHY′, K, F, clock) where {FT, TX, TY}

    @launch(device(arch), config=launch_config(grid, :xy),
            _calculate_top_boundary_Gw!(G.w, grid, coriolis, surface_waves,
                                        closure, U, C, K, F, clock))

    return nothing
end

#####
##### Tendency calculators for u-velocity, aka x-velocity
#####

""" Calculate the right-hand-side of the u-velocity equation. """
function calculate_Gu!(Gu, grid, coriolis, surface_waves, closure, U, C, K, F, pHY′, clock)
    @loop_xyz i j k grid begin
        @inbounds Gu[i, j, k] = u_velocity_tendency(i, j, k, grid, coriolis, surface_waves,
                                                    closure, U, C, K, F, pHY′, clock)
    end
    return nothing
end

""" Calculate the right-hand-side of the u-velocity equation on the east boundary. """
function _calculate_east_boundary_Gu!(Gu, grid, coriolis, surface_waves,
                                      closure, U, C, K, F, pHY′, clock)
    i = grid.Nx + 1
    @loop_yz j k grid begin
        @inbounds Gu[i, j, k] = u_velocity_tendency(i, j, k, grid, coriolis, surface_waves,
                                                    closure, U, C, K, F, pHY′, clock)
    end
    return nothing
end

#####
##### Tendency calculators for v-velocity
#####

""" Calculate the right-hand-side of the v-velocity equation. """
function calculate_Gv!(Gv, grid, coriolis, surface_waves, closure, U, C, K, F, pHY′, clock)
    @loop_xyz i j k grid begin
        @inbounds Gv[i, j, k] = v_velocity_tendency(i, j, k, grid, coriolis, surface_waves,
                                                    closure, U, C, K, F, pHY′, clock)
    end
    return nothing
end

""" Calculate the right-hand-side of the v-velocity equation on the north boundary. """
function _calculate_north_boundary_Gv!(Gv, grid, coriolis, surface_waves,
                                       closure, U, C, K, F, pHY′, clock)
    j = grid.Ny + 1
    @loop_xz i k grid begin
        @inbounds Gv[i, j, k] = v_velocity_tendency(i, j, k, grid, coriolis, surface_waves,
                                                    closure, U, C, K, F, pHY′, clock)
    end
    return nothing
end

#####
##### Tendency calculators for w-velocity
#####

""" Calculate the right-hand-side of the w-velocity equation. """
function calculate_Gw!(Gw, grid, coriolis, surface_waves, closure, U, C, K, F, clock)
    @loop_xyz i j k grid begin
        @inbounds Gw[i, j, k] = w_velocity_tendency(i, j, k, grid, coriolis, surface_waves,
                                                    closure, U, C, K, F, clock)
    end
    return nothing
end

""" Calculate the right-hand-side of the w-velocity equation. """
function _calculate_top_boundary_Gw!(Gw, grid, coriolis, surface_waves, closure, U, C, K, F, clock)
    k = grid.Nz + 1
    @loop_xy i j grid begin
        @inbounds Gw[i, j, k] = w_velocity_tendency(i, j, k, grid, coriolis, surface_waves,
                                                    closure, U, C, K, F, clock)
    end
    return nothing
end

#####
##### Tracer(s)
#####

""" Calculate the right-hand-side of the tracer advection-diffusion equation. """
function calculate_Gc!(Gc, grid, c, tracer_index, closure, buoyancy, U, C, K, Fc, clock)
    @loop_xyz i j k grid begin
        @inbounds Gc[i, j, k] = tracer_tendency(i, j, k, grid, c, tracer_index,
                                                closure, buoyancy, U, C, K, Fc, clock)
    end
    return nothing
end

#####
##### Boundary contributions to tendencies due to user-prescribed fluxes
#####

""" Apply boundary conditions by adding flux divergences to the right-hand-side. """
function calculate_boundary_tendency_contributions!(Gⁿ, arch, U, C, clock, state)

    # Velocity fields
    for i in 1:3
        apply_x_bcs!(Gⁿ[i], U[i], arch, clock, state)
        apply_y_bcs!(Gⁿ[i], U[i], arch, clock, state)
        apply_z_bcs!(Gⁿ[i], U[i], arch, clock, state)
    end

    # Tracer fields
    for i in 4:length(Gⁿ)
        apply_x_bcs!(Gⁿ[i], C[i-3], arch, clock, state)
        apply_y_bcs!(Gⁿ[i], C[i-3], arch, clock, state)
        apply_z_bcs!(Gⁿ[i], C[i-3], arch, clock, state)
    end

    return nothing
end

#####
##### Vertical integrals
#####

"""
Update the hydrostatic pressure perturbation pHY′. This is done by integrating
the `buoyancy_perturbation` downwards:

    `pHY′ = ∫ buoyancy_perturbation dz` from `z=0` down to `z=-Lz`
"""
function update_hydrostatic_pressure!(pHY′, grid, buoyancy, C)
    @loop_xy i j grid begin
        @inbounds pHY′[i, j, grid.Nz] = - ℑzᵃᵃᶠ(i, j, grid.Nz+1, grid, buoyancy_perturbation, buoyancy, C) * ΔzF(i, j, grid.Nz+1, grid)
        @unroll for k in grid.Nz-1 : -1 : 1
            @inbounds pHY′[i, j, k] =
                pHY′[i, j, k+1] - ℑzᵃᵃᶠ(i, j, k+1, grid, buoyancy_perturbation, buoyancy, C) * ΔzF(i, j, k+1, grid)
        end
    end
    return nothing
end

"""
Compute the vertical velocity w by integrating the continuity equation from the bottom upwards

    `w^{n+1} = -∫ [∂/∂x (u^{n+1}) + ∂/∂y (v^{n+1})] dz`
"""
function compute_w_from_continuity!(model)
    @launch(device(model.architecture), config=launch_config(model.grid, :xy),
            _compute_w_from_continuity!(datatuple(model.velocities), model.grid))
    return nothing
end

function _compute_w_from_continuity!(U, grid)
    @loop_xy i j grid begin
        # U.w[i, j, 1] = 0 is enforced via halo regions.
        @unroll for k in 2:grid.Nz
            @inbounds U.w[i, j, k] = U.w[i, j, k-1] - ΔzC(i, j, k, grid) * hdivᶜᶜᵃ(i, j, k-1, grid, U.u, U.v)
        end
    end
    return nothing
end


module CapeOpen
  export constantstrings, constantfloats, tempreturedependents, pressuredependents
  export MaterialObject, PropertyPackage, PhysicalPropertyCalculator

  """
    Constant string properties
      1- casRegistryNumber => Chemical Abstract Service Registry Number
      2- chemicalFormula => Chemical formula
      3- iupacName => Complete IUPAC Name
      4- SMILESformula => SMILES chemical structure formula
  """
  constantstrings=ASCIIString[
    "casRegistryNumber", # Chemical Abstract Service Registry Number
    "chemicalFormula", # Chemical formula
    "iupacName", # Complete IUPAC Name
    "SMILESformula" # SMILES chemical structure formula
  ]

  """
    Constant float properties
      1-  acentricFactor => Pitzer acentric factor
      2-  associationParameter => association-parameter (Hayden-O’Connell)
      3-  bornRadius => in m
      4-  charge =>
      5-  criticalCompressibilityFactor => critical compressibility factor Z
      6-  criticalDensity => critical density in mol/m3
      7-  criticalPressure => critical pressure in Pa
      8-  criticalTemperature => critical temperature in K
      9-  criticalVolume => critical volume in m3/mol
      10- diffusionVolume => diffusion volume in m3
      11- dipoleMoment => dipole moment in Cm
      12- energyLennardJones => Lennard-Jones energy parameter in K (divided by Boltzmann constant)
      13- gyrationRadius => radius of gyration in m
      14- heatOfFusionAtNormalFreezingPoint => enthalpy change on melting at normal freezing point (101325 Pa) in J/mol
      15- heatOfVaporizationAtNormalBoilingPoint => enthalpy change on vaporization at normal boiling point (101325 Pa) in J/mol
      16- idealGasGibbsFreeEnergyOfFormationAt25C => in J/mol
      17- liquidDensityAt25C => liquid density at 25 ºC in mol/m3
      18- liquidVolumeAt25C => liquid volume at 25 ºC in m3/mol
      19- lengthLennardJones => Lennard-Jones length parameter in m
      20- molecularWeight => relative molar mass
      21- normalBoilingPoint => boiling point temperature at 101325 Pa in K
      22- normalFreezingPoint => melting point temperature at 101325 Pa in K
      23- parachor => Parachor in m3 kg0.25/(s0.5 mol)
      24- standardEntropyGas => Standard entropy of gas in J/mol
      25- standardEntropyLiquid => standard entropy of liquid in J/mol
      26- standardEntropySolid => standard entropy of solid in J/mol
      27- standardEnthalpyAqueousDilution => Standard aqueous infinite dilution enthalpy in J/mol
      28- standardFormationEnthalpyGas => standard enthalpy change on formation of gas in J/mol
      29- standardFormationEnthalpyLiquid => standard enthalpy change on formation of liquid in J/mol
      30- standardFormationEnthalpySolid => standard enthalpy change on formation of solid in J/mol
      31- standardFormationGibbsEnergyGas => standard Gibbs energy change on formation of gas in J/mol
      32- standardFormationGibbsEnergyLiquid => standard Gibbs energy change on formation of liquid in J/mol
      33- standardFormationGibbsEnergySolid => standard Gibbs energy change on formation of solid in J/mol
      34- standardGibbsAqueousDilution => Standard aqueous infinite dilution Gibbs energy in J/mol
      35- triplePointPressure => triple point pressure in Pa
      36- triplePointTemperature => triple point temperature in K
      37- vanderwaalsArea => van der Waals area in m3/mol
      38- vanderwaalsVolume => van der Waals volume in m3/mol
  """
  constantfloats=ASCIIString[
    "acentricFactor", # Pitzer acentric factor
    "associationParameter", # association-parameter (Hayden-O’Connell)
    "bornRadius", # in m
    "charge",
    "criticalCompressibilityFactor", # critical compressibility factor Z
    "criticalDensity", # critical density in mol/m3
    "criticalPressure", # critical pressure in Pa
    "criticalTemperature", # critical temperature in K
    "criticalVolume", # critical volume in m3/mol
    "diffusionVolume", # diffusion volume in m3
    "dipoleMoment", # dipole moment in Cm
    "energyLennardJones", # Lennard-Jones energy parameter in K (divided by Boltzmann constant)
    "gyrationRadius", # radius of gyration in m
    "heatOfFusionAtNormalFreezingPoint", # enthalpy change on melting at normal freezing point (101325 Pa) in J/mol
    "heatOfVaporizationAtNormalBoilingPoint", # enthalpy change on vaporization at normal boiling point (101325 Pa) in J/mol
    "idealGasGibbsFreeEnergyOfFormationAt25C", # in J/mol
    "liquidDensityAt25C", # liquid density at 25 ºC in mol/m3
    "liquidVolumeAt25C", # liquid volume at 25 ºC in m3/mol
    "lengthLennardJones", # Lennard-Jones length parameter in m
    "molecularWeight", # relative molar mass
    "normalBoilingPoint", # boiling point temperature at 101325 Pa in K
    "normalFreezingPoint", # melting point temperature at 101325 Pa in K
    "parachor", # Parachor in m3 kg0.25/(s0.5 mol)
    "standardEntropyGas", # Standard entropy of gas in J/mol
    "standardEntropyLiquid", # standard entropy of liquid in J/mol
    "standardEntropySolid", # standard entropy of solid in J/mol
    "standardEnthalpyAqueousDilution", # Standard aqueous infinite dilution enthalpy in J/mol
    "standardFormationEnthalpyGas", # standard enthalpy change on formation of gas in J/mol
    "standardFormationEnthalpyLiquid", # standard enthalpy change on formation of liquid in J/mol
    "standardFormationEnthalpySolid", # standard enthalpy change on formation of solid in J/mol
    "standardFormationGibbsEnergyGas", # standard Gibbs energy change on formation of gas in J/mol
    "standardFormationGibbsEnergyLiquid", # standard Gibbs energy change on formation of liquid in J/mol
    "standardFormationGibbsEnergySolid", # standard Gibbs energy change on formation of solid in J/mol
    "standardGibbsAqueousDilution", # Standard aqueous infinite dilution Gibbs energy in J/mol
    "triplePointPressure", # triple point pressure in Pa
    "triplePointTemperature", # triple point temperature in K
    "vanderwaalsArea", # van der Waals area in m3/mol
    "vanderwaalsVolume" # van der Waals volume in m3/mol
  ]

  """
    Temprature dependent properties
      1-  cpAqueousInfiniteDilution => Heat capacity of a solute in an infinitely dilute aqueous solution. in J/(mol K)
      2-  dielectricConstant => The ratio of the capacity of a condenser with a particular substance as dielectric to the capacity of the same condenser with a vacuum for dielectric.
      3-  expansivity => Coefficient of linear expansion for a solid at 1 atm in 1/K
      4-  fugacityCoefficientOfVapor => Fugacity coefficient of vapour on the saturation line
      5-  glassTransitionPressure => Glass transition pressure in Pa
      6-  heatCapacityOfLiquid => Heat capacity (Cp) of liquid on the saturation line in J/(mol K)
      7-  heatCapacityOfSolid => Solid heat capacity (Cp) at 1 atm in J/(mol K)
      8-  heatOfFusion => Enthalpy change on fusion fot the solid on the melting line in J/mol
      9-  heatOfSublimation => Enthalpy change on evaporation of the solid on the sublimation line in J/mol
      10- heatOfSolidSolidPhaseTransition => Enthalpy change on phase transition in J/mol
      11- heatOfVaporization => Enthalpy change on evaporation of the liquid on the saturation line in J/mol
      12- idealGasEnthalpy => Enthalpy of ideal gas in J/mol
      13- idealGasEntropy => Temperature-dependent part of entropy of ideal gas in J/(mol K)
      14- idealGasHeatCapacity => Heat capacity (Cp) of ideal gas in J/(mol K)
      15- meltingPressure => Pressure on melting line in Pa
      16- selfDiffusionCoefficientGas => Self-diffusion coefficient in gas phase at 1 atm in m2/s
      17- selfDiffusionCoefficientLiquid => self-diffusion coefficient in liquid phase on saturation line in m2/s
      18- solidSolidPhaseTransitionPressure => Pressure at phase transition in Pa
      19- sublimationPressure => Vapour pressure of solid on the sublimation line in Pa
      20- surfaceTensionSatLiquid => Surface tension of liquid on the saturation line in N/m
      21- thermalConductivityOfLiquid => Thermal conductivity of liquid on saturation line in W/(m K)
      22- thermalConductivityOfSolid => Thermal conductivity of solid at 1 atm in W/(m K)
      23- thermalConductivityOfVapor => Thermal conductivity of dilute gas in W/(m K)
      24- vaporPressure => Vapour pressure of saturated liquid in Pas
      25- virialCoefficient => Second virial coefficient of gas in m3/mol
      26- viscosityOfLiquid => Viscosity of liquid on saturation line in Pas
      27- viscosityOfVapor => Viscosity in dilute gas state in Pas
      28- volumeChangeUponMelting => Volume change for the solid on the melting line in m3/mol
      29- volumeChangeUponSolidSolidPhaseTransition => Volume change upon solid-solid phase transition in m3/mol
      30- volumeChangeUponSublimation => Volume change for the solid on the sublimation line in m3/mol
      31- volumeChangeUponVaporization => Volume change for the liquid on the saturation line in m3/mol
      32- volumeOfLiquid => Volume of liquid on saturation line in m3/mol
      33- volumeOfSolid => Volume of solid at 1 atm in m3/mol

  """
  tempreturedependents=ASCIIString[
    "cpAqueousInfiniteDilution", # Heat capacity of a solute in an infinitely dilute aqueous solution. in J/(mol K)
    "dielectricConstant", # The ratio of the capacity of a condenser with a particular substance as dielectric to the capacity of the same condenser with a vacuum for dielectric.
    "expansivity", # Coefficient of linear expansion for a solid at 1 atm in 1/K
    "fugacityCoefficientOfVapor", # Fugacity coefficient of vapour on the saturation line
    "glassTransitionPressure", # Glass transition pressure in Pa
    "heatCapacityOfLiquid", # Heat capacity (Cp) of liquid on the saturation line in J/(mol K)
    "heatCapacityOfSolid", # Solid heat capacity (Cp) at 1 atm in J/(mol K)
    "heatOfFusion", # Enthalpy change on fusion fot the solid on the melting line in J/mol
    "heatOfSublimation", # Enthalpy change on evaporation of the solid on the sublimation line in J/mol
    "heatOfSolidSolidPhaseTransition", # Enthalpy change on phase transition in J/mol
    "heatOfVaporization", # Enthalpy change on evaporation of the liquid on the saturation line in J/mol
    "idealGasEnthalpy", # Enthalpy of ideal gas in J/mol
    "idealGasEntropy", # Temperature-dependent part of entropy of ideal gas in J/(mol K)
    "idealGasHeatCapacity", # Heat capacity (Cp) of ideal gas in J/(mol K)
    "meltingPressure", # Pressure on melting line in Pa
    "selfDiffusionCoefficientGas", # Self-diffusion coefficient in gas phase at 1 atm in m2/s
    "selfDiffusionCoefficientLiquid", # self-diffusion coefficient in liquid phase on saturation line in m2/s
    "solidSolidPhaseTransitionPressure", # Pressure at phase transition in Pa
    "sublimationPressure", # Vapour pressure of solid on the sublimation line in Pa
    "surfaceTensionSatLiquid", # Surface tension of liquid on the saturation line in N/m
    "thermalConductivityOfLiquid", # Thermal conductivity of liquid on saturation line in W/(m K)
    "thermalConductivityOfSolid", # Thermal conductivity of solid at 1 atm in W/(m K)
    "thermalConductivityOfVapor", # Thermal conductivity of dilute gas in W/(m K)
    "vaporPressure", # Vapour pressure of saturated liquid in Pas
    "virialCoefficient", # Second virial coefficient of gas in m3/mol
    "viscosityOfLiquid", # Viscosity of liquid on saturation line in Pas
    "viscosityOfVapor", # Viscosity in dilute gas state in Pas
    "volumeChangeUponMelting", # Volume change for the solid on the melting line in m3/mol
    "volumeChangeUponSolidSolidPhaseTransition", # Volume change upon solid-solid phase transition in m3/mol
    "volumeChangeUponSublimation", # Volume change for the solid on the sublimation line in m3/mol
    "volumeChangeUponVaporization", # Volume change for the liquid on the saturation line in m3/mol
    "volumeOfLiquid", # Volume of liquid on saturation line in m3/mol
    "volumeOfSolid" # Volume of solid at 1 atm in m3/mol
  ]
  """
    Pressure dependent properties
      1- boilingPointTemperature => Temperature at liquid-vapour transition in K
      2- glassTransitionTemperature => Glass transition temperature in K
      3- meltingTemperature => Temperature on melting line in K
      4- solidSolidPhaseTransitionTemperature => Temperature at phase transition in K
  """
  pressuredependents=ASCIIString[
    "boilingPointTemperature", # Temperature at liquid-vapour transition in K
    "glassTransitionTemperature", # Glass transition temperature in K
    "meltingTemperature", # Temperature on melting line in K
    "solidSolidPhaseTransitionTemperature"  # Temperature at phase transition in K
  ]

  """
    Material Object refers to the software object that implements the
    representation of a Material.
    Material Object must:
    * Allow a client to set any basis-dependent Physical Property on any basis.
    * Allow a client to get any basis-dependent Physical Property using the basis with which it was stored.
    * Perform basis conversions, or delegate basis conversion as necessary. If basis conversion is not meaningful (e.g. in the case of cement), the Material Object must be able to return the quantity in its original basis and to return an error should the quantity be requested in a different basis.
    * Ensure that quantities set in one basis are consistent with quantities set in another basis, or delegate that function as necessary. Where the basis conversion on a quantity is not  feasible, the Material Object must only store the quantity in the basis with which it was set most recently
  """
  type MaterialObject
  end

  typealias PropertyMap Dict{ASCIIString,ASCIIString}
  propmap(stvec,t)=[cs=>t[ind] for (ind,cs) in enumerate(stvec)]

  """
    Property Package – a software component that is both a Physical Property Calculator and
    an Equilibrium Calculator for Materials containing a specific set of Compounds
    occurring in a specific number of physical states. A Property Package will make use of
    certain models to perform these calculations. A Property Package can be configured to
    make use of external Physical Property Calculators and/or Equilibrium Calculators.
    Alternatively, it can provide the functionality of these two components internally without
    making use of external components. Configuring the Compounds, Phases, models and
    external components used in a Property Package is outside the scope of this CAPEOPEN interface specification.
  """
  type PropertyPackage
    """
      Convert from perry units to cape-open standard units, for other properties default value of 1.0 is used
      6-  criticalDensity =>  mol/m3 mol/dm3 1E+3
      7-  criticalPressure =>  Pa MPa 1E+6
      9-  criticalVolume => m3/mol m3/Kmol 1E-3
      15- heatOfVaporizationAtNormalBoilingPoint => J/mol J/kmol 1E-3
      16- idealGasGibbsFreeEnergyOfFormationAt25C => J/mol J/kmol 1E-3
      18- liquidVolumeAt25C => m3/mol dm3/mol 1E-3
      24- standardEntropyGas => J/mol J/Kmol  1E-3
      28- standardFormationEnthalpyGas => J/mol  J/Kmol 1E-3
      31- standardFormationGibbsEnergyGas => J/mol J/Kmol 1E-3
      6-  heatCapacityOfLiquid => J/(mol K) J/(Kmol K) 1E-3
      11- heatOfVaporization => J/mol J/kmol 1E-3
      12- idealGasEnthalpy => J/mol J/Kmol 1E-3
      13- idealGasEntropy =>  J/(mol K) J/(Kmol K) 1E-3
      14- idealGasHeatCapacity =>  J/(mol K) J/(Kmol K) 1E-3
      32- volumeOfLiquid => m3/mol dm3/mol 1E-3
    """
    function PropertyPackage(constantstrings,t1,constantfloats,t2,tempreturedependents,t3,pressuredependents,t4,propertytable)
      new(
        propmap(constantstrings,t1),
        propmap(constantfloats,t2),
        propmap(tempreturedependents,t3),
        propmap(pressuredependents,t4),
        propertytable,
        Dict(
          "criticalDensity"                         =>1E+3, #mol/m3 <= mol/dm3
          "criticalPressure"                        =>1E+6, #Pa <= MPa
          "criticalVolume"                          =>1E-3, #m3/mol <= m3/Kmol
          "heatOfVaporizationAtNormalBoilingPoint"  =>1E-3, #J/mol <= J/kmol
          "idealGasGibbsFreeEnergyOfFormationAt25C" =>1E-3, #J/mol <= J/kmol
          "liquidVolumeAt25C"                       =>1E-3, #m3/mol <= dm3/mol
          "standardEntropyGas"                      =>1E-3, #J/mol <= J/Kmol /1E3
          "standardFormationEnthalpyGas"            =>1E-3, #J/mol <=  J/Kmol /1E3
          "standardFormationGibbsEnergyGas"         =>1E-3, #J/mol <= J/Kmol /1E3
          "heatCapacityOfLiquid"                    =>1E-3, #J/(mol K) <= J/(Kmol K) /1E3
          "heatOfVaporization"                      =>1E-3, #J/mol <= J/kmol /1E3
          "idealGasEnthalpy"                        =>1E-3, #J/mol <= J/Kmol /1E3
          "idealGasEntropy"                         =>1E-3, #J/(mol K) <= J/(Kmol K)
          "idealGasHeatCapacity"                    =>1E-3, #J/(mol K) <= J/(Kmol K)
          "volumeOfLiquid"                          =>1E-3 #m3/mol <= dm3/mol
        ),
        Dict{ASCIIString, Array}(
          "Criticals"=>       Any[[0,0,0], Vector{Float64}(6)],
          "LiquidsDensities"=>Any[[0,0,0], Vector{Float64}(10), 0%UInt8],
          "FormationEnergy"=> Any[[0,0,0], Vector{Float64}(5)],
          "Compounds"=>       Any[[0,0,0], Vector{UInt8}(29), Vector{UInt8}(9), Vector{UInt8}(11), 0.0, 0.0],
          "Cp"=>              Any[[0,0,0], Vector{Float64}(10), 0%UInt8],
          "LiquidsCp"=>       Any[[0,0,0], Vector{Float64}(10), 0%UInt8],
          "VaporizHeat"=>     Any[[0,0,0], Vector{Float64}(10)],
          "LiquidThermal"=>   Any[[0,0,0], Vector{Float64}(10)],
          "VaporPressure"=>   Any[[0,0,0], Vector{Float64}(10)],
          "LiquidViscos"=>    Any[[0,0,0], Vector{Float64}(10)],
          "VaporThermal"=>    Any[[0,0,0], Vector{Float64}(9)],
          "VaporViscos"=>     Any[[0,0,0], Vector{Float64}(9)]
        ),
        "perryanalytic"
      )
    end
    constantstrings::PropertyMap
    constantfloats::PropertyMap
    tempreturedependents::PropertyMap
    pressuredependents::PropertyMap
    propertytable::Vector{ASCIIString}
    convertions::Dict{ASCIIString,Float64}
    tableaddreses::Dict{ASCIIString, Array}
    datafilename::ASCIIString
  end
  perryanalytic=PropertyPackage(
    getindex(constantstrings,1:3),
    ["Compounds" for i=1:3],
    getindex(constantfloats,[ 5,6,7,8,9,15,16,17,18,
                                20,21,24,28,31]),
    ["Criticals", "Criticals", "Criticals", "Criticals", "Criticals", "VaporizHeat", "FormationEnergy", "LiquidsDensities", "LiquidsDensities",
     "Compounds", "Compounds", "FormationEnergy", "FormationEnergy", "FormationEnergy"],
    getindex(tempreturedependents,[6,11,12,13,14,21,23,24,26,27,32]),
    ["LiquidsCp", "VaporizHeat", "Cp", "Cp", "Cp", "LiquidThermal", "VaporThermal", "VaporPressure", "LiquidViscos", "VaporViscos", "LiquidsDensities"],
    ["boilingPointTemperature"],
    ["VaporPressure"],
    ASCIIString[
      "Compounds"        ,
      "VaporPressure"    ,
      "LiquidsDensities",
      "Criticals"        ,
      "VaporizHeat"      ,
      "LiquidsCp"        ,
      "Cp"                ,
      "FormationEnergy" ,
      "VaporViscos"      ,
      "LiquidViscos"     ,
      "VaporThermal"     ,
      "LiquidThermal"
    ]
  )
end