# 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


# *********************************************************************************
# REopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# Redistributions of source code must retain the above copyright notice, this list
# of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice, this
# list of conditions and the following disclaimer in the documentation and/or other
# materials provided with the distribution.
#
# Neither the name of the copyright holder nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
# *********************************************************************************
"""
    Generator

struct with inner constructor:
```julia
function Generator(;
    existing_kw::Real=0,
    min_kw::Real=0,
    max_kw::Real=1.0e6,
    installed_cost_per_kw::Real=500.0,
    om_cost_per_kw::Real=10.0,
    om_cost_per_kwh::Float64=0.0,
    fuel_cost_per_gallon::Float64 = 3.0,
    fuel_slope_gal_per_kwh::Float64 = 0.076,
    fuel_intercept_gal_per_hr::Float64 = 0.0,
    fuel_avail_gal::Float64 = 660.0,
    min_turn_down_pct::Float64 = 0.0,
    only_runs_during_grid_outage::Bool = true,
    sells_energy_back_to_grid::Bool = false,
    can_net_meter::Bool = false,
    can_wholesale::Bool = false,
    can_export_beyond_nem_limit = false,
    macrs_option_years::Int = 0,
    macrs_bonus_pct::Float64 = 1.0,
    macrs_itc_reduction::Float64 = 0.0,
    federal_itc_pct::Float64 = 0.0,
    federal_rebate_per_kw::Float64 = 0.0,
    state_ibi_pct::Float64 = 0.0,
    state_ibi_max::Float64 = 1.0e10,
    state_rebate_per_kw::Float64 = 0.0,
    state_rebate_max::Float64 = 1.0e10,
    utility_ibi_pct::Float64 = 0.0,
    utility_ibi_max::Float64 = 1.0e10,
    utility_rebate_per_kw::Float64 = 0.0,
    utility_rebate_max::Float64 = 1.0e10,
    production_incentive_per_kwh::Float64 = 0.0,
    production_incentive_max_benefit::Float64 = 1.0e9,
    production_incentive_years::Int = 0,
    production_incentive_max_kw::Float64 = 1.0e9,
)
```
"""
struct Generator <: AbstractGenerator
    existing_kw
    min_kw
    max_kw
    installed_cost_per_kw
    om_cost_per_kw
    om_cost_per_kwh
    fuel_cost_per_gallon
    fuel_slope_gal_per_kwh
    fuel_intercept_gal_per_hr
    fuel_avail_gal
    min_turn_down_pct
    only_runs_during_grid_outage
    sells_energy_back_to_grid
    can_net_meter
    can_wholesale
    can_export_beyond_nem_limit
    can_curtail
    macrs_option_years
    macrs_bonus_pct
    macrs_itc_reduction
    federal_itc_pct
    federal_rebate_per_kw
    state_ibi_pct
    state_ibi_max
    state_rebate_per_kw
    state_rebate_max
    utility_ibi_pct
    utility_ibi_max
    utility_rebate_per_kw
    utility_rebate_max
    production_incentive_per_kwh
    production_incentive_max_benefit
    production_incentive_years
    production_incentive_max_kw

    function Generator(;
        existing_kw::Real=0,
        min_kw::Real=0,
        max_kw::Real=1.0e6,
        installed_cost_per_kw::Real=500.0,
        om_cost_per_kw::Real=10.0,
        om_cost_per_kwh::Float64=0.0,
        fuel_cost_per_gallon::Float64 = 3.0,
        fuel_slope_gal_per_kwh::Float64 = 0.076,
        fuel_intercept_gal_per_hr::Float64 = 0.0,
        fuel_avail_gal::Float64 = 660.0,
        min_turn_down_pct::Float64 = 0.0,
        only_runs_during_grid_outage::Bool = true,
        sells_energy_back_to_grid::Bool = false,
        can_net_meter::Bool = false,
        can_wholesale::Bool = false,
        can_export_beyond_nem_limit = false,
        can_curtail::Bool = false,
        macrs_option_years::Int = 0,
        macrs_bonus_pct::Float64 = 1.0,
        macrs_itc_reduction::Float64 = 0.0,
        federal_itc_pct::Float64 = 0.0,
        federal_rebate_per_kw::Float64 = 0.0,
        state_ibi_pct::Float64 = 0.0,
        state_ibi_max::Float64 = 1.0e10,
        state_rebate_per_kw::Float64 = 0.0,
        state_rebate_max::Float64 = 1.0e10,
        utility_ibi_pct::Float64 = 0.0,
        utility_ibi_max::Float64 = 1.0e10,
        utility_rebate_per_kw::Float64 = 0.0,
        utility_rebate_max::Float64 = 1.0e10,
        production_incentive_per_kwh::Float64 = 0.0,
        production_incentive_max_benefit::Float64 = 1.0e9,
        production_incentive_years::Int = 0,
        production_incentive_max_kw::Float64 = 1.0e9,
        )

        new(
            existing_kw,
            min_kw,
            max_kw,
            installed_cost_per_kw,
            om_cost_per_kw,
            om_cost_per_kwh,
            fuel_cost_per_gallon,
            fuel_slope_gal_per_kwh,
            fuel_intercept_gal_per_hr,
            fuel_avail_gal,
            min_turn_down_pct,
            only_runs_during_grid_outage,
            sells_energy_back_to_grid,
            can_net_meter,
            can_wholesale,
            can_export_beyond_nem_limit,
            can_curtail,
            macrs_option_years,
            macrs_bonus_pct,
            macrs_itc_reduction,
            federal_itc_pct,
            federal_rebate_per_kw,
            state_ibi_pct,
            state_ibi_max,
            state_rebate_per_kw,
            state_rebate_max,
            utility_ibi_pct,
            utility_ibi_max,
            utility_rebate_per_kw,
            utility_rebate_max,
            production_incentive_per_kwh,
            production_incentive_max_benefit,
            production_incentive_years,
            production_incentive_max_kw
        )
    end
end


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


using BlockArrays, LinearAlgebra, Base64

struct PartiallyImplementedBlockVector <: AbstractBlockArray{Float64,1} end

@testset "partially implemented block array" begin
    # the error thrown before was incorrect
    A = PartiallyImplementedBlockVector()
    try
        getblock(A, 1)
    catch err
        @test err isa ErrorException && err.msg == "getblock for PartiallyImplementedBlockVector is not implemented"
    end
    try
        getblock!(zeros(5), A, Block(1))
    catch err
        @test err isa ErrorException && err.msg == "getblock! for PartiallyImplementedBlockVector is not implemented"
    end
    try
        BlockArrays.setblock!(A, zeros(5), Block(1))
    catch err
        @test err isa ErrorException && err.msg == "setblock! for PartiallyImplementedBlockVector is not implemented"
    end
    try
        BlockArrays.blocksize(A, 2)
    catch err
        @test err isa ErrorException && err.msg == "blocksizes for PartiallyImplementedBlockVector is not implemented"
    end
end

@testset "Array block interface" begin
    @test blocksizes(1) == BlockArrays.BlockSizes{0}()
    @test 1[Block()] == 1

    A = randn(5)
    @test blocksizes(A) == BlockArrays.BlockSizes([5])
    @test A[Block(1)] == A
    view(A,Block(1))[1] = 2
    @test A[1] == 2
    @test_throws BoundsError A[Block(2)]

    A = randn(5,5)
    @test A[Block(1,1)] == A
end

@testset "Triangular/Symmetric/Hermitian block arrays" begin
    A = PseudoBlockArray{ComplexF64}(undef, (1:4), (1:4))
    A .= reshape(1:length(A), size(A))

    @test blocksizes(UpperTriangular(A)) == blocksizes(Symmetric(A)) == blocksizes(A)
    @test UpperTriangular(A)[Block(2,2)] == UpperTriangular(A[2:3,2:3])
    @test UpperTriangular(A)[Block(2,3)] == A[2:3,4:6]
    @test UpperTriangular(A)[Block(3,2)] == zeros(3,2)
    @test Symmetric(A)[Block(2,2)] == Symmetric(A[2:3,2:3])
    @test Symmetric(A)[Block(2,3)] == A[2:3,4:6]
    @test Symmetric(A)[Block(3,2)] == transpose(A[2:3,4:6])
    @test Hermitian(A)[Block(2,2)] == Hermitian(A[2:3,2:3])
    @test Hermitian(A)[Block(2,3)] == A[2:3,4:6]
    @test Hermitian(A)[Block(3,2)] == A[2:3,4:6]'

    @test stringmime("text/plain", UpperTriangular(A)) == "10×10 UpperTriangular{Complex{Float64},PseudoBlockArray{Complex{Float64},2,Array{Complex{Float64},2},BlockArrays.BlockSizes{2,Tuple{Array{Int64,1},Array{Int64,1}}}}}:\n 1.0+0.0im  │  11.0+0.0im  21.0+0.0im  │  31.0+0.0im  41.0+0.0im  51.0+0.0im  │  61.0+0.0im  71.0+0.0im  81.0+0.0im   91.0+0.0im\n ───────────┼──────────────────────────┼──────────────────────────────────────┼─────────────────────────────────────────────────\n     ⋅      │  12.0+0.0im  22.0+0.0im  │  32.0+0.0im  42.0+0.0im  52.0+0.0im  │  62.0+0.0im  72.0+0.0im  82.0+0.0im   92.0+0.0im\n     ⋅      │       ⋅      23.0+0.0im  │  33.0+0.0im  43.0+0.0im  53.0+0.0im  │  63.0+0.0im  73.0+0.0im  83.0+0.0im   93.0+0.0im\n ───────────┼──────────────────────────┼──────────────────────────────────────┼─────────────────────────────────────────────────\n     ⋅      │       ⋅           ⋅      │  34.0+0.0im  44.0+0.0im  54.0+0.0im  │  64.0+0.0im  74.0+0.0im  84.0+0.0im   94.0+0.0im\n     ⋅      │       ⋅           ⋅      │       ⋅      45.0+0.0im  55.0+0.0im  │  65.0+0.0im  75.0+0.0im  85.0+0.0im   95.0+0.0im\n     ⋅      │       ⋅           ⋅      │       ⋅           ⋅      56.0+0.0im  │  66.0+0.0im  76.0+0.0im  86.0+0.0im   96.0+0.0im\n ───────────┼──────────────────────────┼──────────────────────────────────────┼─────────────────────────────────────────────────\n     ⋅      │       ⋅           ⋅      │       ⋅           ⋅           ⋅      │  67.0+0.0im  77.0+0.0im  87.0+0.0im   97.0+0.0im\n     ⋅      │       ⋅           ⋅      │       ⋅           ⋅           ⋅      │       ⋅      78.0+0.0im  88.0+0.0im   98.0+0.0im\n     ⋅      │       ⋅           ⋅      │       ⋅           ⋅           ⋅      │       ⋅           ⋅      89.0+0.0im   99.0+0.0im\n     ⋅      │       ⋅           ⋅      │       ⋅           ⋅           ⋅      │       ⋅           ⋅           ⋅      100.0+0.0im"
end

@testset "Adjoint/Transpose block arrays" begin
    A = PseudoBlockArray{ComplexF64}(undef, (1:4), (2:5))
    A .= randn.() .+ randn.().*im

    @test blocksizes(A') == BlockArrays.BlockSizes(2:5, 1:4)
    @test blocksizes(Transpose(A)) == BlockArrays.BlockSizes(2:5, 1:4)

    @test A'[Block(2,2)] == A[Block(2,2)]' == A[2:3,3:5]'
    @test transpose(A)[Block(2,2)] == transpose(A[2:3,3:5])
    @test A'[Block(2,3)] == A[Block(3,2)]'
    @test transpose(A)[Block(2,3)] == transpose(A[Block(3,2)])

    @test BlockArray(A') == A'
    @test BlockArray(transpose(A)) == transpose(A)
end

@testset "Diagonal BlockArray" begin
    A = mortar(Diagonal(fill([1 2],2)))
    @test A isa BlockMatrix{Int,Diagonal{Matrix{Int}, Vector{Matrix{Int}}}}
    @test A[Block(1,2)] == [0 0]
    @test_throws BlockBoundsError A[Block(1,3)]
    @test A == [1 2 0 0; 0 0 1 2]
end

@testset "non-standard BlockSizes" begin
    A = BlockArray([1 2; 3 4], BlockArrays.BlockSizes((1:3, 1:3)))
    @test A isa BlockMatrix{Int,Matrix{Matrix{Int}},BlockArrays.BlockSizes{2,NTuple{2,UnitRange{Int}}}}
    A = BlockArray([1 2; 3 4], BlockArrays.BlockSizes((1:3, [1,2,3])))
    @test A isa BlockMatrix{Int,Matrix{Matrix{Int}},BlockArrays.BlockSizes{2,Tuple{UnitRange{Int},Vector{Int}}}}
end


module Octree

using GRU.Shapes
using GRU.Shapes: intersect, isvalid

export Tree, add, remove, for_overlapping, getbound

abstract type OptionalNode{T} end

mutable struct NullNode{T} <: OptionalNode{T}
end

mutable struct Node{T} <: OptionalNode{T}
	subNodes::Array{OptionalNode{T}, 3}
	objects::Array{T, 1}

	Node{T}() where T = new(fill(NullNode{T}(), 2, 2, 2), T[])
end

mutable struct Tree{T, F}
	bound::AABB{F}
	root::Node{T}
	minNodeSize::F
	canExpand::Bool
end

Tree(::Type{T}, bound::AABB{F}; minNodeSize::F = F(16), canExpand::Bool = false) where {T, F} = Tree{T, F}(bound, Node{T}(), minNodeSize, canExpand)

function add(octree::Tree{T, F}, obj::T, objBound::AABB{F}) where {T, F}
	@assert isvalid(objBound)
	@assert !any(x->isinf(x), objBound.p)
	if !inside(octree.bound, objBound)
		if octree.canExpand
			expand(octree, objBound)
			@assert inside(octree.bound, objBound)
		else
			error("Octree: Trying to add an object outside of tree bounds")
		end
	end
	add(octree.root, octree.bound, octree, obj, objBound)
end
add(octree::Tree{T, F}, obj::T) where {T, F} = add(octree, obj, getbound(obj))

remove(octree::Tree{T, F}, obj::T, objBound::AABB{F}) where {T, F} = remove(octree.root, octree.bound, octree, obj, objBound)
remove(octree::Tree{T, F}, obj::T) where {T, F} = remove(octree, obj, getbound(obj))

for_overlapping(f::Function, octree::Tree{T, F}, bound::AABB{F}) where {T, F} = for_overlapping(f, octree.root, octree.bound, bound)

getbound(::Any) = nothing # this needs to be overriden by the user

function expand(octree::Tree{T, F}, objBound::AABB{F}) where {T, F}
	ind = Int[0, 0, 0]
	while true
		for i = 1:3
			if objBound.p[i, 1] < octree.bound.p[i, 1]
				ind[i] = 2
			elseif objBound.p[i, 2] > octree.bound.p[i, 2]
				ind[i] = 1
			else
				ind[i] = 0
			end
		end
		if all(x->x==0, ind)
			break;
		end
		for i = 1:3
			if ind[i] == 0 # In case the bound we want to fit doesn't exit a dimension, grow symmetrically around 0
				ind[i] = abs(octree.bound.p[i, 1]) < abs(octree.bound.p[i, 2]) ? 2 : 1;
			end
			sz = octree.bound.p[i, 2] - octree.bound.p[i, 1]
			if ind[i] == 1
				octree.bound.p[i, 2] += sz
			else
				@assert ind[i] == 2
				octree.bound.p[i, 1] -= sz
			end
		end
		newRoot = Node{T}()
		newRoot.subNodes[ind...] = octree.root
		octree.root = newRoot
	end
end

cansubdivide(nodeBound::AABB{F}, octree::Tree{T, F}) where {T, F} = minimum(nodeBound.p[:, 2] - nodeBound.p[:, 1]) / 2 >= octree.minNodeSize

function getsubbound(nodeBound::AABB{F}, ind::Vector{Int}) where F
	subBound = AABB{F}()
	for i=1:3
		mid = (nodeBound.p[i, 1] + nodeBound.p[i, 2]) / 2
		subBound.p[i, ind[i]] = nodeBound.p[i, ind[i]]
		subBound.p[i, 3-ind[i]] = mid
	end
	return subBound
end

function getsubindex(nodeBound::AABB{F}, objBound::AABB{F}) where F
	@assert inside(nodeBound, objBound)
	ind = Int[]
	for i=1:3
		mid = (nodeBound.p[i, 1] + nodeBound.p[i, 2]) / 2
		if objBound.p[i, 2] <= mid
			push!(ind, 1)
		elseif objBound.p[i, 1] >= mid
			push!(ind, 2)
		else
			return ind
		end
	end
	return ind
end

function getsubnode(node::Node{T}, nodeBound::AABB{F}, octree::Tree{T, F}, objBound::AABB{F}, create::Bool) where {T, F}
	if create && !cansubdivide(nodeBound, octree)
		return NullNode{T}(), nodeBound
	end
	ind = getsubindex(nodeBound, objBound)
	if length(ind) != 3
		return NullNode{T}(), nodeBound
	end
	subNode = node.subNodes[ind...]
	if isa(subNode, NullNode{T})
		if !create
			return NullNode{T}(), nodeBound
		end
		subNode = Node{T}()
		node.subNodes[ind...] = subNode
	end
	return subNode, getsubbound(nodeBound, ind)
end

function add(node::Node{T}, nodeBound::AABB{F}, octree::Tree{T, F}, obj::T, objBound::AABB{F}) where {T, F}
	subNode, subBound = getsubnode(node, nodeBound, octree, objBound, true)
	if isa(subNode, NullNode{T})
		@assert findfirst(isequal(obj), node.objects) == nothing
		push!(node.objects, obj)
	else
		add(subNode, subBound, octree, obj, objBound)
	end
end

function deleteat_unordered!(v::Vector{T}, ind::Int) where T
	v[ind] = v[end]
	resize!(v, length(v)-1)
end

function remove(node::Node{T}, nodeBound::AABB{F}, octree::Tree{T, F}, obj::T, objBound::AABB{F}) where {T, F}
	subNode, subBound = getsubnode(node, nodeBound, octree, objBound, false)
	if isa(subNode, NullNode{T})
		deleteat_unordered!(node.objects, findfirst(node.objects, obj))
	else
		remove(subNode, subBound, octree, obj, objBound)
	end
end

function for_overlapping(f::Function, node::Node{T}, nodeBound::AABB{F}, bound::AABB{F}) where {T, F}
	for obj in node.objects
		if intersect(bound, getbound(obj))
			f(obj)
		end
	end
	for z = 1:2, y = 1:2, x = 1:2
		if !isa(node.subNodes[x, y, z], NullNode{T})
			subBound = getsubbound(nodeBound, [x, y, z])
			if intersect(bound, subBound)
				for_overlapping(f, node.subNodes[x, y, z], subBound, bound)
			end
		end
	end
end

#=
ab = AABB([-32.0, -32, -32], [32.0, 32, 32])
ab1 = AABB([-2.0, -2, -2], [-1.0, -1, -1])
ab5 = AABB([-5.0, -5, -5], [5.0, 5, 5])
ab10 = AABB([10.0, 10, 10], [12.0, 12, 12])
ab50 = AABB([50.0, 0, 10], [60.0, 10, 20])

getbound(ab::AABB) = ab

oc = Tree(typeof(ab5), ab; canExpand=true)
add(oc, ab5)
add(oc, ab1)
add(oc, ab10)
add(oc, ab50)
for_overlapping(oc, ab5) do ab
	println(ab)
end
remove(oc, ab5)
println()
for o in @async for_overlapping(x->produce(x), oc, oc.bound)
	println(o)
end
=#

end


include("Graph.jl")
include("Exp2.jl")

using Printf

function runMain(ags1, ags2; resultDir = "../result/")
    lg = open(resultDir*"log.txt", "a")
    G = readUnLabeledGraph(ags1)
    println(lg, ags1, " ", G.n, " ", G.m)
    ds = Dict{String, String}()
    ds["1"] = "RoleSim : "
    ds["2"] = "StructSim : "
    ds["3"] = "ForestSimExact : "
    ds["4"] = "ForestSimApprox : "
    df = Dict{String, Function}()
    df["1"] = testRoleSim
    df["2"] = testStructSim
    df["3"] = testForestSim
    df["4"] = testForestSimApprox
    for x in split(ags2, ',')
        println(lg, ds[x], df[x](G, 10))
    end
    close(lg)
end

runMain("Diseasome", "1,2,3,4")
runMain("EmailUniv", "1,2,3,4")
runMain("Hamster", "1,2,3,4")
runMain("GridWorm", "1,2,3,4")
runMain("GrQc", "1,2,3,4")
runMain("Erdos992", "1,2,3,4")
runMain("Reality", "1,2,3,4")
runMain("Dmela", "1,2,3,4")
runMain("HepPh", "2,3,4")
runMain("PagesCompany", "2,3,4")
runMain("AstroPh", "2,3,4")
runMain("Gplus", "2,3,4")
runMain("Brightkite", "2,4")
runMain("BlogCatalog", "2,4")
runMain("Douban", "2,4")
runMain("MathSciNet", "4")
runMain("Flickr", "4")
runMain("IMDB", "4")
runMain("YoutubeSnap", "4")
runMain("Flixster", "4")


module BARON

using MathProgBase
using MathProgBase.SolverInterface
using Compat

export BaronSolver
struct BaronSolver <: AbstractMathProgSolver
    options
end

BaronSolver(;kwargs...) = BaronSolver(kwargs)

const baron_exec = ENV["BARON_EXEC"]

mutable struct BaronMathProgModel <: AbstractNonlinearModel
    options

    xˡ::Vector{Float64}
    xᵘ::Vector{Float64}
    gˡ::Vector{Float64}
    gᵘ::Vector{Float64}

    nvar::Int
    ncon::Int

    obj
    constrs

    vartypes::Vector{Symbol}

    v_names::Vector{String}
    c_names::Vector{String}

    sense::Symbol

    x₀::Vector{Float64}

    probfile::String
    sumfile::String
    resfile::String

    objval::Float64
    dualbound::Float64
    solution::Vector{Float64}
    walltime::Float64
    status::Symbol

    d::AbstractNLPEvaluator

    function BaronMathProgModel(;options...)
        dir = tempdir()
        push!(options, (:ResName, joinpath(dir, "res.lst")))
        push!(options, (:TimName, joinpath(dir, "tim.lst")))
        push!(options, (:SumName, joinpath(dir, "sum.lst")))
        new(options,
        zeros(0),
        zeros(0),
        zeros(0),
        zeros(0),
        0,
        0,
        :(0),
        Expr[],
        Symbol[],
        String[],
        String[],
        :Min,
        zeros(0),
        "",
         "",
        "",
        NaN,
        NaN,
        [NaN],
        0.0,
        :NotSolved)
    end
end

MathProgBase.NonlinearModel(s::BaronSolver) = BaronMathProgModel(;s.options...)
MathProgBase.LinearQuadraticModel(s::BaronSolver) = MathProgBase.NonlinearToLPQPBridge(MathProgBase.NonlinearModel(s))

verify_support(c) = c

function verify_support(c::Expr)
    if c.head == :comparison
        map(verify_support, c.args)
        return c
    end
    if c.head == :call
        if c.args[1] in (:+, :-, :*, :/, :exp, :log)
            return c
        elseif c.args[1] in (:<=, :>=, :(==))
            map(verify_support, c.args[2:end])
            return c
        elseif c.args[1] == :^
            @assert isa(c.args[2], Real) || isa(c.args[3], Real)
            return c
        else # TODO: do automatic transformation for x^y, |x|
            error("Unsupported expression $c")
        end
    end
    return c
end

function MathProgBase.loadproblem!(m::BaronMathProgModel,
    nvar, ncon, xˡ, xᵘ, gˡ, gᵘ, sense,
    d::MathProgBase.AbstractNLPEvaluator)

    @assert nvar == length(xˡ) == length(xᵘ)
    @assert ncon == length(gˡ) == length(gᵘ)

    m.xˡ, m.xᵘ = xˡ, xᵘ
    m.gˡ, m.gᵘ = gˡ, gᵘ
    m.sense = sense
    m.nvar, m.ncon = nvar, ncon
    m.d = d

    m.v_names = ["x$i" for i in 1:nvar]
    m.c_names = ["e$i" for i in 1:ncon]

    MathProgBase.initialize(d, [:ExprGraph])

    m.obj = verify_support(MathProgBase.obj_expr(d))
    m.vartypes = fill(:Cont, nvar)

    m.constrs = map(1:ncon) do c
        verify_support(MathProgBase.constr_expr(d,c))
    end

    dir = tempdir()
    m.probfile = joinpath(dir, "baron_problem.bar")
    m.sumfile  = joinpath(dir, "sum.lst")
    m.resfile  = joinpath(dir, "res.lst")
    m
end

function MathProgBase.setvartype!(m::BaronMathProgModel, cat::Vector{Symbol})
    @assert all(x-> (x in (:Cont,:Bin,:Int)), cat)
    m.vartypes = cat
end

function print_var_definitions(m, fp, header, condition)
    idx = filter(condition, 1:m.nvar)
    if !isempty(idx)
        println(fp, header, join([m.v_names[i] for i in idx], ", "), ";")
    end
end

# to_str(x::Int) = string(x)
# to_str(x) = (@show(x); string(float(x)))
to_str(x) = string(x)

function to_str(c::Expr)
    if c.head == :comparison
        if length(c.args) == 3
            return join([to_str(c.args[1]), c.args[2], c.args[3]], " ")
        elseif length(c.args) == 5
            return join([c.args[1], c.args[2], to_str(c.args[3]),
                         c.args[4], c.args[5]], " ")
        end
    elseif c.head == :call
        if c.args[1] in (:<=,:>=,:(==))
            if length(c.args) == 3
                return join([to_str(c.args[2]), to_str(c.args[1]), to_str(c.args[3])], " ")
            elseif length(c.args) == 5
                return join([to_str(c.args[1]), to_str(c.args[2]), to_str(c.args[3]), to_str(c.args[4]), to_str(c.args[5])], " ")
            end
        elseif c.args[1] in (:+,:-,:*,:/,:^)
            if all(d->isa(d, Real), c.args[2:end]) # handle unary case
                return string(eval(c))
            elseif c.args[1] == :- && length(c.args) == 2
                return string("(-$(to_str(c.args[2])))")
            else
                return string("(", join([to_str(d) for d in c.args[2:end]], string(c.args[1])), ")")
            end
        elseif c.args[1] in (:exp,:log)
            if isa(c.args[2], Real)
                return string(eval(c))
            else
                return string(c.args[1], "( ", to_str(c.args[2]), " )")
            end
        end
    elseif c.head == :ref
        if c.args[1] == :x
            @assert isa(c.args[2], Int)
            return "x$(c.args[2])"
        else
            error("Unrecognized reference expression $c")
        end
    end
end

function write_bar_file(m::BaronMathProgModel)
    fp = open(m.probfile, "w")

    # First: process any options
    println(fp, "OPTIONS{")
    for (opt,setting) in m.options
        if isa(setting, AbstractString) # wrap it in quotes
             println(fp, "$opt: $('"')$setting$('"');")
        else
            println(fp, "$opt: $setting;")
        end
    end
    println(fp, "}")
    println(fp)

    # Next, define variables
    print_var_definitions(m, fp, "BINARY_VARIABLES ",   v->(m.vartypes[v]==:Bin))
    print_var_definitions(m, fp, "INTEGER_VARIABLES ",  v->(m.vartypes[v]==:Int))
    print_var_definitions(m, fp, "POSITIVE_VARIABLES ", v->(m.vartypes[v]==:Cont && m.xˡ[v]==0))
    print_var_definitions(m, fp, "VARIABLE ",           v->(m.vartypes[v]==:Cont && m.xˡ[v]!=0))
    println(fp)

    # Print variable bounds
    if any(c->!isinf(c), m.xˡ)
        println(fp, "LOWER_BOUNDS{")
        for (i,l) in enumerate(m.xˡ)
            if !isinf(l)
                println(fp, "$(m.v_names[i]): $l;")
            end
        end
        println(fp, "}")
        println(fp)
    end
    if any(c->!isinf(c), m.xᵘ)
        println(fp, "UPPER_BOUNDS{")
        for (i,u) in enumerate(m.xᵘ)
            if !isinf(u)
                println(fp, "$(m.v_names[i]): $u;")
            end
        end
        println(fp, "}")
        println(fp)
    end

    # Now let's declare the equations
    if !isempty(m.constrs)
        println(fp, "EQUATIONS ", join(m.c_names, ", "), ";")
        for (i,c) in enumerate(m.constrs)
            str = to_str(c)
            println(fp, "$(m.c_names[i]): $str;")
        end
        println(fp)
    end

    # Now let's do the objective
    print(fp, "OBJ: ")
    print(fp, m.sense == :Min ? "minimize " : "maximize ")
    print(fp, to_str(m.obj))
    println(fp, ";")
    println(fp)

    if !isempty(m.x₀)
        println(fp, "STARTING_POINT{")
        for (i,v) in enumerate(m.x₀)
            println(fp, "$(m.v_names[i]): $v;")
        end
        println(fp, "}")
    end
    close(fp)
end

const normal_status = [
    ["***", "Normal", "completion", "***"]
]

const userlimits_status = [
    ["***", "Max.", "allowable", "nodes", "in", "memory", "reached", "***"],
    ["***", "Max.", "allowable", "BaR", "iterations", "reached", "***"],
    ["***", "Max.", "allowable", "CPU", "time", "exceeded", "***"],
    ["***", "Max.", "allowable", "time", "exceeded", "***"],
    ["***", "Problem", "is", "numerically", "sensitive", "***"],
    ["***", "Heuristic", "termination", "***"],
    ["***", "Globality", "is", "therefore", "not", "guaranteed", "***"]
]

const infeasible_status = [
    ["***", "Problem", "is", "infeasible", "***"],
    ["***", "No", "feasible", "solution", "was", "found", "***"],
    ["***", "Infeasibility", "is", "therefore", "not", "guaranteed", "***"],
    ["***", "Insufficient", "Memory", "for", "Data", "structures", "***"],
]

const unbounded_status = [
    ["***", "Problem", "is", "unbounded", "***"],
    ["***", "User" ,"did", "not", "provide", "appropriate", "variable", "bounds", "***"]
]

const numerical_status = [
    ["***", "Problem", "is", "numerically", "sensitive", "***"]
]

const interrupt_status = [
    ["***", "Search", "interrupted", "by", "user", "***"]
]

const error_status = [
    ["***", "A", "potentially", "catastrophic", "access", "violation", "just", "took", "place", "***"]
]

function read_results(m::BaronMathProgModel)

    # First, we read the summary file to get the solution status
    stat_code = []
    fp = open(m.sumfile, "r")
    stat = :Undefined
    t = -1.0
    n = -99
    while true
        line = readline(fp)
        spl = split(chomp(line))
        if !isempty(spl) && spl[1] == "***"
            push!(stat_code, spl)
        elseif length(spl)>=3 && spl[1:3] == ["Wall", "clock", "time:"]
            t = parse(Float64,match(r"\d+.\d+", line).match)
        elseif length(spl)>=3 && spl[1:3] == ["Best", "solution", "found"]
            n = parse(Int,match(r"-?\d+", line).match)
        # Grab dual bound if solved during presolve (printed directly to summary file)
        elseif length(spl)>=3 && spl[1:3] == ["Lower", "bound", "is"]
            m.dualbound = parse(Float64, spl[4])
        # Grab dual bound if branching (need to get it from solver update log)
        elseif spl == ["Iteration", "Open", "nodes", "Time", "(s)", "Lower", "bound", "Upper", "bound"]
            while true
                line = readline(fp)
                spl = split(chomp(line))
                if isempty(spl)
                    break
                end
                # Lowerbound is 4th column in table, but log line might include * for heuristic solution
                m.dualbound = parse(Float64, spl[end-1])
            end
        end
        eof(fp) && break
    end
    close(fp)

    t < 0.0 && warn("No solution time is found in sum.lst")
    n == -99 && error("No solution node information found sum.lst")

    # Now navigate to the right problem status by looking at the main status
    if stat_code[1] in userlimits_status
        m.status = :UserLimit
        n == -3 && (m.status = :Infeasible)
    elseif stat_code[1] in normal_status
        m.status = :Optimal
        n == -3 && (m.status = :Infeasible)
    elseif stat_code[1] in infeasible_status
        m.status = :Infeasible
    elseif stat_code[1] in unbounded_status
        m.status = :Unbounded
    elseif stat_code[1] in numerical_status
        m.status = :NumericalSensitive
        n == -3 && (m.status = :Infeasible)
    elseif stat_code[1] in interrupt_status
        m.status = :Interrupted
        n == -3 && (m.status = :Infeasible)
    elseif stat_code[1] in error_status
        m.status = :Error
    end

    m.walltime = t # Track the time

    # Next, we read the results file to get the solution
    x = fill(NaN, m.nvar)
    m.objval = NaN
    if n != -3 # parse as long as there exist a solution
        fp = open(m.resfile, "r")
        while true
            startswith(readline(fp), "The best solution found") && break
            eof(fp) && error()
        end
        readline(fp)
        readline(fp)

        while true
            line = chomp(readline(fp))
            parts = split(line)
            isempty(parts) && break
            mt = match(r"\d+", parts[1])
            mt == nothing && error("Cannot find appropriate variable index from $(parts[1])")
            v_idx = parse(Int,mt.match)
            v_val = float(parts[3])
            x[v_idx] = v_val
        end
        m.solution = x
        line = readline(fp)
        val = match(r"[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?", chomp(line))
        if val != nothing
            m.objval = float(val.match)
        end
    end
    nothing
end

MathProgBase.setwarmstart!(m::BaronMathProgModel, v::Vector{Float64}) = (m.x₀ = v)

function MathProgBase.optimize!(m::BaronMathProgModel)
    write_bar_file(m)
    run(`$baron_exec $(m.probfile)`)

    read_results(m)
end

MathProgBase.status(m::BaronMathProgModel) = m.status

MathProgBase.numvar(m::BaronMathProgModel) = m.nvar
MathProgBase.getsolution(m::BaronMathProgModel) = m.solution
MathProgBase.getobjval(m::BaronMathProgModel) = m.objval
MathProgBase.getobjbound(m::BaronMathProgModel) = m.dualbound
MathProgBase.getsolvetime(m::BaronMathProgModel) = m.walltime

end


function gather_kpts_scfres(scfres::NamedTuple)
    # TODO Rename this to gather_kpts once scfres gets its proper type

    # Need gathering over k-Points:
    kpt_properties = (:ψ, :occupation, :eigenvalues)
    scfdict = Dict{Symbol, Any}()
    for symbol in kpt_properties
        scfdict[symbol] = gather_kpts(getproperty(scfres, symbol), scfres.basis)
    end
    scfdict[:basis] = gather_kpts(scfres.basis)

    if mpi_master()
        (; (symbol => get(scfdict, symbol, getproperty(scfres, symbol))
            for symbol in propertynames(scfres))...)
    else
        nothing
    end
end

"""
    load_scfres(filename)

Load back an `scfres`, which has previously been stored with [`save_scfres`](@ref).
Note the warning in [`save_scfres`](@ref).
"""
function load_scfres end

"""
    save_scfres(filename, scfres)

Save an `scfres` obtained from `self_consistent_field` to a file.
The format is determined from the file extension. Currently the following
file extensions are recognized and supported:

- **jld2**: A JLD2 file. Stores the complete state and can be used
  (with [`load_scfres`](@ref)) to restart an SCF from a checkpoint or
  post-process an SCF solution. See [Saving SCF results on disk and SCF checkpoints](@ref)
  for details.
- **vts**: A VTK file for visualisation e.g. in [paraview](https://www.paraview.org/).
  Stores the density, spin density and some metadata (energy, Fermi level, occupation etc.).
  Supports these keyword arguments:
    * `save_ψ`: Save the real-space representation of the orbitals as well
      (may lead to larger files).
    * `extra_data`: `Dict{String,Array}` with additional data on the 3D real-space
      grid to store into the VTK file.

!!! warning "No compatibility guarantees"
    No guarantees are made with respect to this function at this point.
    It may change incompatibly between DFTK versions or stop working / be removed
    in the future.
"""
function save_scfres(filename::AbstractString, scfres::NamedTuple; kwargs...)
    _, ext = splitext(filename)
    ext = Symbol(ext[2:end])

    # Whitelist valid extensions
    !(ext in (:jld2, :vts)) && error("Extension '$ext' not supported by DFTK.")

    # Gather scfres data on master MPI process
    scfres = gather_kpts_scfres(scfres)

    # On master MPI process dispatch to individual functions based on extension
    ret = isnothing(scfres) ? nothing : save_scfres_master(filename, scfres, Val(ext); kwargs...)

    # Return from this function synchronously on all processes
    # (to ensure that data has actually been written upon return)
    MPI.Barrier(MPI.COMM_WORLD)
    ret
end

function save_scfres_master(filename::AbstractString, ::NamedTuple, ::Any; kwargs...)
    error("The extension $(last(splitext(filename))) is currently not available. " *
          "A required package (e.g. JLD2 or WriteVTK) is not yet loaded.")
end