using KNITRO
using Test

const MPS_PROBLEM =
"""
NAME         lo1
OBJSENSE     MAX
ROWS
 N  obj
 E  c1
 G  c2
 L  c3
COLUMNS
    x1        obj       3
    x1        c1        3
    x1        c2        2
    x2        obj       1
    x2        c1        1
    x2        c2        1
    x2        c3        2
    x3        obj       5
    x3        c1        2
    x3        c2        3
    x4        obj       1
    x4        c2        1
    x4        c3        3
RHS
    rhs       c1        30
    rhs       c2        15
    rhs       c3        25
RANGES
BOUNDS
 UP bound     x2        10
ENDATA
"""

@testset "Instantiation Knitro C interface" begin
    # get KNITRO.KNITRO release version
    rel = KNITRO.get_release()
    @test isa(rel, String)

    @testset "Definition of model" begin
        m = KNITRO.Model()
        options = joinpath(dirname(@__FILE__), "..", "examples", "knitro.opt")
        KNITRO.KN_reset_params_to_defaults(m)

        KNITRO.KN_free(m)
        @test m.env.ptr_env == C_NULL
    end

    @testset "License manager test" begin
        # create license manager context
        lm = KNITRO.LMcontext()

        # we create 10 KNITRO instances with the license manager
        n_instances = 10
        kcs = [KNITRO.KN_new_lm(lm) for i in 1:n_instances]

        # then we free the instances
        for kc in kcs
            KNITRO.KN_free(kc)
        end

        # and release the license
        KNITRO.KN_release_license(lm)
        @test lm.ptr_lmcontext == C_NULL
    end
end

# add generic callbacks for future tests

function evalAll(kc, cb, evalRequest, evalResult, userParams)
    x = evalRequest.x
    evalRequestCode = evalRequest.evalRequestCode

    if evalRequestCode == KNITRO.KN_RC_EVALFC
        # Evaluate nonlinear objective
        evalResult.obj[1] = x[1]^2 * x[3] + x[2]^3 * x[3]^2
    elseif evalRequestCode == KNITRO.KN_RC_EVALGA
        evalResult.objGrad[1] = 2 * x[1] * x[3]
        evalResult.objGrad[2] = 3 * x[2]^2 * x[3]^2
        evalResult.objGrad[3] = x[1]^2 + 2 * x[2]^3 * x[3]
    elseif evalRequestCode == KNITRO.KN_RC_EVALH
        evalResult.hess[1] = 2 * x[3]
        evalResult.hess[2] = 2 * x[1]
        evalResult.hess[3] = 6 * x[2] * x[3]^2
        evalResult.hess[4] = 6 * x[2]^2 * x[3]
        evalResult.hess[5] = 2 * x[2]^3
    elseif evalRequestCode == KNITRO.KN_RC_EVALHV
        vec = evalRequest.vec
        evalResult.hessVec[1] = (2 * x[3]) * vec[1] + (2 * x[1]) * vec[3]
        evalResult.hessVec[2] = (6 * x[2] * x[3]^2) * vec[2] + (6 * x[2]^2 * x[3]) * vec[3]
        evalResult.hessVec[3] = (2 * x[1]) * vec[1] + (6 * x[2]^2 * x[3]) * vec[2] + (2 * x[2]^3) * vec[3]

    elseif evalRequestCode == KNITRO.KN_RC_EVALH_NO_F
        evalResult.hess[1] = 0
        evalResult.hess[2] = 0
        evalResult.hess[3] = 0
        evalResult.hess[4] = 0
        evalResult.hess[5] = 0

    elseif evalRequestCode == KNITRO.KN_RC_EVALHV_NO_F
        vec = evalRequest.vec
        evalResult.hessVec[1] = 0
        evalResult.hessVec[2] = lambda_[4] * vec[3]
        evalResult.hessVec[3] = lambda_[4] * vec[2]

    else
        return KNITRO.KN_RC_CALLBACK_ERR
    end
    return 0
end

function callback(name)
    function callbackFn(kc, x, lambda_, userParams)
        println(name)
        return 0
    end
    return callbackFn
end

if KNITRO.KNITRO_VERSION >= v"12.0"
    @testset "Names getters" begin
        kc = KNITRO.KN_new()
        KNITRO.KN_add_vars(kc, 3)
        KNITRO.KN_add_cons(kc, 3)
        xnames = ["x1", "x2", "x3"]
        cnames = ["c1", "c2", "c3"]

        KNITRO.KN_set_var_names(kc, xnames)
        KNITRO.KN_set_con_names(kc, cnames)
        index = Cint(1)

        name = KNITRO.KN_get_var_names(kc, index)
        @test name == xnames[2]
        outnames = KNITRO.KN_get_var_names(kc, [index])
        @test outnames[1] == xnames[2]
        outnames = KNITRO.KN_get_var_names(kc)
        @test xnames == outnames

        name = KNITRO.KN_get_con_names(kc, index)
        @test name == cnames[2]
        outnames = KNITRO.KN_get_con_names(kc, [index])
        @test outnames[1] == cnames[2]
        outnames = KNITRO.KN_get_con_names(kc)
        @test cnames == outnames

        KNITRO.KN_free(kc)
    end
end
if KNITRO.KNITRO_VERSION >= v"12.1"
    @testset "MPS reader/writer" begin
        mps_name = joinpath(dirname(@__FILE__), "lp.mps")
        mps_name_out = joinpath(dirname(@__FILE__), "lp2.mps")
        open(mps_name, "w") do io
            write(io, MPS_PROBLEM)
        end
        kc = KNITRO.KN_new()
        KNITRO.KN_load_mps_file(kc, mps_name)
        KNITRO.KN_set_param(kc, "outlev", 0)
        KNITRO.KN_write_mps_file(kc, mps_name_out)
        status = KNITRO.KN_solve(kc)
        obj = KNITRO.get_objective(kc)
        KNITRO.KN_free(kc)
        @test status == 0
        @test obj ≈ 250.0 / 3.0

        # Resolve with dumped MPS file
        kc = KNITRO.KN_new()
        KNITRO.KN_load_mps_file(kc, mps_name_out)
        KNITRO.KN_set_param(kc, "outlev", 0)
        status = KNITRO.KN_solve(kc)
        obj = KNITRO.get_objective(kc)
        KNITRO.KN_free(kc)
        @test status == 0
        @test obj ≈ 250.0 / 3.0
    end
end

@testset "First problem" begin
    kc = KNITRO.KN_new()
    @test isa(kc, KNITRO.Model)
    # By default, kc does not have any callback
    @test !KNITRO.has_callbacks(kc)

    release = KNITRO.get_release()

    KNITRO.KN_reset_params_to_defaults(kc)

    options = joinpath(dirname(@__FILE__), "..", "examples", "test_knitro.opt")
    tuner1 = joinpath(dirname(@__FILE__), "..", "examples", "tuner-fixed.opt")
    tuner2 = joinpath(dirname(@__FILE__), "..", "examples", "tuner-explore.opt")
    KNITRO.KN_set_param(kc, "algorithm", 0)
    KNITRO.KN_set_param(kc, "cplexlibname", ".")
    KNITRO.KN_set_param(kc, "xtol", 1e-15)
    KNITRO.KN_set_param(kc, KNITRO.KN_PARAM_ALG, KNITRO.KN_ALG_BAR_DIRECT)
    KNITRO.KN_set_param(kc, KNITRO.KN_PARAM_CPLEXLIB, ".")
    KNITRO.KN_set_param(kc, KNITRO.KN_PARAM_XTOL, 1e-15)

    @test KNITRO.KN_get_int_param(kc, "algorithm") == KNITRO.KN_ALG_BAR_DIRECT
    @test KNITRO.KN_get_double_param(kc, "xtol") == 1e-15
    @test KNITRO.KN_get_int_param(kc, KNITRO.KN_PARAM_ALG) == KNITRO.KN_ALG_BAR_DIRECT
    @test KNITRO.KN_get_double_param(kc, KNITRO.KN_PARAM_XTOL) == 1e-15
    @test KNITRO.KN_get_param_name(kc, KNITRO.KN_PARAM_XTOL)  == "xtol"

    @test KNITRO.KN_get_param_doc(kc, KNITRO.KN_PARAM_XTOL) == "# Step size tolerance used for terminating the optimization.\n"
    @test KNITRO.KN_get_param_type(kc, KNITRO.KN_PARAM_XTOL) == KNITRO.KN_PARAMTYPE_FLOAT
    @test KNITRO.KN_get_num_param_values(kc, KNITRO.KN_PARAM_XTOL) == 0

    @test KNITRO.KN_get_param_value_doc(kc, KNITRO.KN_PARAM_GRADOPT, 1) == "exact"

    @test KNITRO.KN_get_param_id(kc, "xtol") == KNITRO.KN_PARAM_XTOL

    # START: Some specific parameter settings
    KNITRO.KN_set_param(kc, "hessopt", 1)
    KNITRO.KN_set_param(kc, "presolve", 0)
    KNITRO.KN_set_param(kc, "outlev", 0)
    # END:   Some specific parameter settings

    # Perform a derivative check.
    KNITRO.KN_set_param(kc, KNITRO.KN_PARAM_DERIVCHECK, KNITRO.KN_DERIVCHECK_ALL)

    function newpt_callback(kc, x, lambda_, user_data)
        a = KNITRO.KN_get_rel_feas_error(kc)
        b = KNITRO.KN_get_rel_opt_error(kc)
        return 0
    end

    # Define objective goal
    objGoal = KNITRO.KN_OBJGOAL_MAXIMIZE
    KNITRO.KN_set_obj_goal(kc, objGoal)

    # Add the variables and set their bounds.
    nV = 3
    KNITRO.KN_add_vars(kc, nV)
    KNITRO.KN_set_var_lobnds(kc, [0, 0.1, 0])
    KNITRO.KN_set_var_upbnds(kc, [0., 2, 2])
    # Define an initial point.
    KNITRO.KN_set_var_primal_init_values(kc, [1., 1, 1.5])
    KNITRO.KN_set_var_dual_init_values(kc,  [1., 1, 1, 1])

    # Add the constraints and set their bounds.
    nC = 1
    KNITRO.KN_add_cons(kc, nC)
    KNITRO.KN_set_con_lobnds(kc, [0.1])
    KNITRO.KN_set_con_upbnds(kc, [2 * 2 * 0.99])

    # Test getters.
    if KNITRO.KNITRO_VERSION >= v"12.0"
        xindex = Cint[0, 1, 2]
        @test KNITRO.KN_get_var_lobnds(kc, xindex) == [0, 0.1, 0]
        @test KNITRO.KN_get_var_upbnds(kc, xindex) == [0., 2, 2]

        cindex = Cint[0]
        @test KNITRO.KN_get_con_lobnds(kc, cindex) == [0.1]
        @test KNITRO.KN_get_con_upbnds(kc, cindex) == [2 * 2 * 0.99]
    end

    # Load quadratic structure x1*x2 for the constraint.
    KNITRO.KN_add_con_quadratic_struct(kc, 0, 1, 2, 1.0)

    # Define callback functions.
    cb = KNITRO.KN_add_objective_callback(kc, evalAll)
    @test KNITRO.has_callbacks(kc)
    KNITRO.KN_set_cb_grad(kc, cb, evalAll)
    KNITRO.KN_set_cb_hess(kc, cb, 5, evalAll,
                          hessIndexVars1=Int32[0, 0, 1, 1, 2],
                          hessIndexVars2=Int32[0, 2, 1, 2, 2])

    KNITRO.KN_set_newpt_callback(kc, newpt_callback)

    # Add complementarity constraints.
    KNITRO.KN_set_compcons(kc, [KNITRO.KN_CCTYPE_VARVAR], Int32[0], Int32[1])

    # Solve the problem.
    status = KNITRO.KN_solve(kc)
    @test status == 0

    # Restart using the previous solution.
    nStatus, objSol, x, lambda_ = KNITRO.KN_get_solution(kc)
    KNITRO.KN_set_var_primal_init_values(kc, x)
    KNITRO.KN_set_var_dual_init_values(kc, lambda_)
    status = KNITRO.KN_solve(kc)
    @test status == 0

    # Restart with new variable bounds
    KNITRO.KN_set_var_lobnds(kc, Float64[0., 0, 0])
    KNITRO.KN_set_var_upbnds(kc, Float64[2., 2, 2])
    status = KNITRO.KN_solve(kc)
    @test status == 0

    # Retrieve relevant solve information
    @test KNITRO.KN_get_number_FC_evals(kc) >= 1
    @test KNITRO.KN_get_number_GA_evals(kc) >= 1
    @test KNITRO.KN_get_number_H_evals(kc) >= 1
    @test KNITRO.KN_get_number_HV_evals(kc) == 0
    @test KNITRO.KN_get_number_iters(kc) >= 1
    @test KNITRO.KN_get_number_cg_iters(kc) >= 0
    @test KNITRO.KN_get_abs_feas_error(kc) < 1e-10
    @test KNITRO.KN_get_rel_feas_error(kc) < 1e-10
    @test KNITRO.KN_get_abs_opt_error(kc) < 1e-7
    @test KNITRO.KN_get_rel_opt_error(kc) < 1e-8
    @test KNITRO.KN_get_con_values(kc)[1] ≈ 3.96

    nStatus, objSol, x, lambda_ = KNITRO.KN_get_solution(kc)
    @test nStatus == 0
    @test x ≈ [0., 2.0, 1.98]

    @test objSol ≈ 31.363199 atol=1e-5

    # Test getters for primal and dual variables
    if KNITRO.KNITRO_VERSION >= v"12.0"
        xopt = KNITRO.KN_get_var_primal_values(kc, Cint[0, 1, 2])
        @test xopt == x
        rc = KNITRO.KN_get_var_dual_values(kc, Cint[0, 1, 2])
        @test rc == lambda_[2:4]
        dual = KNITRO.KN_get_con_dual_values(kc, Cint[0])
        @test dual == [lambda_[1]]
    end

    KNITRO.KN_free(kc)
end

@testset "Second problem test" begin
    kc = KNITRO.KN_new()

    function prettyPrinting(str, userParams)
        s = "KNITRO-Julia: " * str
        println(s)
        return length(s)
    end

    KNITRO.KN_set_puts_callback(kc, prettyPrinting)

    # START: Some specific parameter settings
    KNITRO.KN_set_param(kc, "outlev", 0)
    KNITRO.KN_set_param(kc, "presolve", 0)
    KNITRO.KN_set_param(kc, "ms_enable", 1)
    KNITRO.KN_set_param(kc, "ms_maxsolves", 5)
    KNITRO.KN_set_param(kc, "hessian_no_f", 1)
    KNITRO.KN_set_param(kc, "hessopt", KNITRO.KN_HESSOPT_PRODUCT)
    # END:   Some specific parameter settings

    # Define objective goal
    objGoal = KNITRO.KN_OBJGOAL_MAXIMIZE
    KNITRO.KN_set_obj_goal(kc, objGoal)

    # Add the variables and set their bounds.
    nV = 3
    KNITRO.KN_add_vars(kc, nV)
    KNITRO.KN_set_var_lobnds(kc, [0, 0.1, 0])
    KNITRO.KN_set_var_upbnds(kc, [0., 2, 2])

    # Define an initial point.
    KNITRO.KN_set_var_primal_init_values(kc, [1, 1, 1.5])
    KNITRO.KN_set_var_dual_init_values(kc, [1, 1, 1, 1.])

    # Add the constraints and set their lower bounds.
    nC = 1
    KNITRO.KN_add_cons(kc, nC)
    KNITRO.KN_set_con_lobnds(kc, [0.1])
    KNITRO.KN_set_con_upbnds(kc, [2 * 2 * 0.99])

    # Load quadratic structure x1*x2 for the constraint.
    KNITRO.KN_add_con_quadratic_struct(kc, 0, 1, 2, 1.0)

    # Define callback functions.
    cb = KNITRO.KN_add_objective_callback(kc, evalAll)

    KNITRO.KN_set_cb_grad(kc, cb, evalAll)
    KNITRO.KN_set_cb_hess(kc, cb, KNITRO.KN_DENSE_ROWMAJOR, evalAll)

    KNITRO.KN_set_ms_process_callback(kc, callback("ms callback"))

    function ms_initpt_callbackFn(kc, nSolveNumber, x, lambda_, userParams)
        x[:] = [1, 1, 1.1 + 0.1 * nSolveNumber]
        lambda_[:] = [1., 1, 1, 1]
        return 0
    end

    # Set multistart initial point callback
    KNITRO.KN_set_ms_initpt_callback(kc, ms_initpt_callbackFn)

    # Add complementarity constraints.
    KNITRO.KN_set_compcons(kc, [KNITRO.KN_CCTYPE_VARVAR], Int32[0], Int32[1])

    # Solve the problem.
    status = KNITRO.KN_solve(kc)

    @test status == 0
    nStatus, objSol, x, lambda_ = KNITRO.KN_get_solution(kc)
    @test nStatus == 0
    @test x ≈ [0., 2.0, 1.98] atol=1e-5

    @test objSol ≈ 31.363199 atol=1e-5

    KNITRO.KN_free(kc)
end

@testset "Third problem test" begin
    kc = KNITRO.KN_new()

    KNITRO.KN_set_param(kc, "outlev", 0)
    KNITRO.KN_set_param(kc, "presolve", KNITRO.KN_PRESOLVEDBG_NONE)

    # Define objective goal
    objGoal = KNITRO.KN_OBJGOAL_MAXIMIZE
    KNITRO.KN_set_obj_goal(kc, objGoal)

    # Add the variables and set their bounds.
    nV = 3
    KNITRO.KN_add_vars(kc, nV)
    KNITRO.KN_set_var_lobnds(kc, [0, 0.1, 0])
    KNITRO.KN_set_var_upbnds(kc, [0., 2, 2])

    # Define an initial point.
    KNITRO.KN_set_var_primal_init_values(kc, [1, 1, 1.5])
    KNITRO.KN_set_var_dual_init_values(kc, [1., 1, 1, 1])

    # Add the constraints and set their lower bounds.
    nC = 1
    KNITRO.KN_add_cons(kc, nC)
    KNITRO.KN_set_con_lobnds(kc, [0.1])
    KNITRO.KN_set_con_upbnds(kc, [2 * 2 * 0.99])

    # Load quadratic structure x1*x2 for the constraint.
    KNITRO.KN_add_con_quadratic_struct(kc, 0, 1, 2, 1.0)

    # Define callback functions.
    cb = KNITRO.KN_add_objective_callback(kc, evalAll)
    KNITRO.KN_set_cb_grad(kc, cb, evalAll)
    KNITRO.KN_set_cb_hess(kc, cb, KNITRO.KN_DENSE_ROWMAJOR, evalAll)

    KNITRO.KN_set_compcons(kc, [KNITRO.KN_CCTYPE_VARVAR], Int32[0], Int32[1])

    KNITRO.KN_set_var_honorbnds(kc,
                                [KNITRO.KN_HONORBNDS_ALWAYS,
                                KNITRO.KN_HONORBNDS_INITPT,
                                KNITRO.KN_HONORBNDS_NO])

    KNITRO.KN_set_var_scalings(kc, Int32[1,2,3], [1.,1,1])
    KNITRO.KN_set_con_scalings(kc, [0.5])
    KNITRO.KN_set_compcon_scalings(kc, [2.])
    KNITRO.KN_set_obj_scaling(kc, 10.)

    status = KNITRO.KN_solve(kc)
    @test status == 0

    # Retrieve derivatives values
    objGrad = KNITRO.KN_get_objgrad_values(kc)
    jac     = KNITRO.KN_get_jacobian_values(kc)
    hess    = KNITRO.KN_get_hessian_values(kc)

    nStatus, objSol, x, lambda_ = KNITRO.KN_get_solution(kc)
    @test nStatus == 0
    @test x ≈ [0., 2.0, 1.98]
    @test objSol ≈ 31.363199 atol=1e-5

    KNITRO.KN_free(kc)
end

@testset "Fourth problem test" begin
    kc = KNITRO.KN_new()

    # START: Some specific parameter settings
    KNITRO.KN_set_param(kc, "presolve", 0)
    KNITRO.KN_set_param(kc, "outlev", 0)
    KNITRO.KN_set_param(kc, "gradopt", 2)
    KNITRO.KN_set_param(kc, "hessopt", 2)
    # END:   Some specific parameter settings

    function evalF_evalGA(kc, cb, evalRequest, evalResult, userParams)
        x = evalRequest.x
        evalRequestCode = evalRequest.evalRequestCode

        if evalRequestCode == KNITRO.KN_RC_EVALFC
            # Evaluate nonlinear objective
            evalResult.obj[1] = x[1]^2 * x[3] + x[2]^3 * x[3]^2
        elseif evalRequestCode == KNITRO.KN_RC_EVALGA
            evalResult.objGrad[1] = 2 * x[1] * x[3]
            evalResult.objGrad[2] = 3 * x[2]^2 * x[3]^2
            evalResult.objGrad[3] = x[1]^2 + 2 * x[2]^3 * x[3]
        else
            return KNITRO.KN_RC_CALLBACK_ERR
        end
        return 0
    end

    # Define objective goal
    objGoal = KNITRO.KN_OBJGOAL_MAXIMIZE
    KNITRO.KN_set_obj_goal(kc, objGoal)

    # Add the variables and set their bounds.
    nV = 3
    KNITRO.KN_add_vars(kc, nV)
    KNITRO.KN_set_var_lobnds(kc, [0, 0.1, 0])
    KNITRO.KN_set_var_upbnds(kc, [0., 2, 2])
    KNITRO.KN_set_var_types(kc, [KNITRO.KN_VARTYPE_CONTINUOUS, KNITRO.KN_VARTYPE_INTEGER, KNITRO.KN_VARTYPE_INTEGER])

    # Define an initial point.
    KNITRO.KN_set_var_primal_init_values(kc, [1, 1, 1.5])
    KNITRO.KN_set_var_dual_init_values(kc, [1., 1, 1, 1])

    # Add the constraints and set their lower bounds.
    nC = 1
    KNITRO.KN_add_cons(kc, nC)
    KNITRO.KN_set_con_lobnds(kc, [0.1])
    KNITRO.KN_set_con_upbnds(kc, [2 * 2 * 0.99])

    # Load quadratic structure x1*x2 for the constraint.
    KNITRO.KN_add_con_quadratic_struct(kc, 0, 1, 2, 1.0)

    # Define callback functions.
    cb = KNITRO.KN_add_objective_callback(kc, evalF_evalGA)
    KNITRO.KN_set_cb_grad(kc, cb, evalF_evalGA)

    # Define complementarity constraints
    KNITRO.KN_set_compcons(kc, [KNITRO.KN_CCTYPE_VARVAR], Int32[0], Int32[1])

    # Set MIP parameters
    KNITRO.KN_set_mip_branching_priorities(kc, Int32[0, 1, 2])
    # not compatible with MPEC constraint as a variable cannot be involved in two different complementarity constraints.
    # KNITRO.KN_set_mip_intvar_strategies(kc, 2, KNITRO.KN_MIP_INTVAR_STRATEGY_MPEC)
    KNITRO.KN_set_mip_node_callback(kc, callback("mip_node"))

    # Set var, con and obj names
    KNITRO.KN_set_var_names(kc, ["myvar1", "myvar2", "myvar3"])
    KNITRO.KN_set_con_names(kc, ["mycon1"])
    KNITRO.KN_set_obj_name(kc, "myobj")

    # Set feasibility tolerances
    KNITRO.KN_set_var_feastols(kc, [0.1, 0.001, 0.1])
    KNITRO.KN_set_con_feastols(kc, [0.1])
    KNITRO.KN_set_compcon_feastols(kc, [0.1])

    # Set finite differences step size
    KNITRO.KN_set_cb_relstepsizes(kc, cb, [0.1, 0.001, 0.1])

    # Solve the problem.
    status = KNITRO.KN_solve(kc)
    @test status == 0

    @test KNITRO.KN_get_mip_number_nodes(kc) >= 1
    @test KNITRO.KN_get_mip_number_solves(kc) >= 1
    @test KNITRO.KN_get_mip_relaxation_bnd(kc) ≈ 31.0495680023
    @test KNITRO.KN_get_mip_lastnode_obj(kc) ≈ 32.0
    @test KNITRO.KN_get_con_values(kc)[1] == 4.0
    @test KNITRO.KN_get_mip_incumbent_obj(kc) ≈ 32.0
    @test KNITRO.KN_get_mip_incumbent_x(kc) == 0.0

    KNITRO.KN_free(kc)
end

@testset "Fifth problem test" begin
    # Test in this environment the setting of user params
    myParams = "stringUserParam"
    kc = KNITRO.KN_new()

    # START: Some specific parameter settings
    KNITRO.KN_set_param(kc, "outlev", 0)
    KNITRO.KN_set_param(kc, "gradopt", 1)
    # END:   Some specific parameter settings

    function evalR(kc, cb, evalRequest, evalResult, userParams)
        x = evalRequest.x
        # Each time we call this callback, the userParams should
        # be as specified.
        @test userParams == myParams
        evalResult.rsd[1] = x[1] * 1.309^x[2] - 2.138
        evalResult.rsd[2] = x[1] * 1.471^x[2] - 3.421
        evalResult.rsd[3] = x[1] * 1.49^x[2] - 3.597
        evalResult.rsd[4] = x[1] * 1.565^x[2] - 4.34
        evalResult.rsd[5] = x[1] * 1.611^x[2] - 4.882
        evalResult.rsd[6] = x[1] * 1.68^x[2] - 5.66
        return 0
    end

    function evalJ(kc, cb, evalRequest, evalResult, userParams)
        x = evalRequest.x
        # Each time we call this callback, the userParams should
        # be as specified.
        @test userParams == myParams
        evalResult.rsdJac[1] = 1.309^x[2]
        evalResult.rsdJac[2] = x[1] * log(1.309) * 1.309^x[2]
        evalResult.rsdJac[3] = 1.471^x[2]
        evalResult.rsdJac[4] = x[1] * log(1.471) * 1.471^x[2]
        evalResult.rsdJac[5] = 1.49^x[2]
        evalResult.rsdJac[6] = x[1] * log(1.49) * 1.49^x[2]
        evalResult.rsdJac[7] = 1.565^x[2]
        evalResult.rsdJac[8] = x[1] * log(1.565) * 1.565^x[2]
        evalResult.rsdJac[9] = 1.611^x[2]
        evalResult.rsdJac[10] = x[1] * log(1.611) * 1.611^x[2]
        evalResult.rsdJac[11] = 1.68^x[2]
        evalResult.rsdJac[12] = x[1] * log(1.68) * 1.68^x[2]
        return 0
    end

    # Add the variables and set their bounds.
    nV = 2
    KNITRO.KN_add_vars(kc, nV)
    KNITRO.KN_set_var_lobnds(kc, [ -1.0, -1.0 ])
    KNITRO.KN_set_var_upbnds(kc, [ 1.0, 1.0 ])
    KNITRO.KN_set_var_primal_init_values(kc, [ 1.0, 5.0 ])

    # Add the residuals
    KNITRO.KN_add_rsds(kc, 6)

    # Define callbacks
    cb = KNITRO.KN_add_lsq_eval_callback(kc, evalR)
    nnzJ = 12
    KNITRO.KN_set_cb_rsd_jac(kc, cb, nnzJ, evalJ,
                            jacIndexRsds=Int32[ 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5 ],
                            jacIndexVars=Int32[ 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1 ])
    KNITRO.KN_set_cb_user_params(kc, cb, myParams)

    # Solve the problem.
    status = KNITRO.KN_solve(kc)
    @test status == 0

    jac = KNITRO.KN_get_rsd_jacobian_values(kc)

    nStatus, objSol, x, lambda_ = KNITRO.KN_get_solution(kc)
    @test nStatus == 0

    @test objSol ≈ 21.5848 atol=1e-3
    @test x ≈ [1., 1.] atol=1e-5

    KNITRO.KN_free(kc)
end

@testset "User callback test (issue #110)" begin
    kc = KNITRO.KN_new()

    KNITRO.KN_set_param(kc, "outlev", 0)
    # Define objective goal
    objGoal = KNITRO.KN_OBJGOAL_MAXIMIZE
    KNITRO.KN_set_obj_goal(kc, objGoal)

    # Add the variables and set their bounds.
    nV = 3
    KNITRO.KN_add_vars(kc, nV)
    KNITRO.KN_set_var_lobnds(kc, [0, 0.1, 0])
    KNITRO.KN_set_var_upbnds(kc, [0., 2, 2])

    # Define an initial point.
    KNITRO.KN_set_var_primal_init_values(kc, [1, 1, 1.5])
    KNITRO.KN_set_var_dual_init_values(kc, [1., 1, 1, 1])

    # Add the constraints and set their lower bounds.
    nC = 1
    KNITRO.KN_add_cons(kc, nC)
    KNITRO.KN_set_con_lobnds(kc, [0.1])
    KNITRO.KN_set_con_upbnds(kc, [2 * 2 * 0.99])

    # Load quadratic structure x1*x2 for the constraint.
    KNITRO.KN_add_con_quadratic_struct(kc, 0, 1, 2, 1.0)

    # Define callback functions.
    cb = KNITRO.KN_add_eval_callback_all(kc, evalAll)
    KNITRO.KN_set_cb_grad(kc, cb, evalAll)
    KNITRO.KN_set_cb_hess(kc, cb, KNITRO.KN_DENSE_ROWMAJOR, evalAll)

    KNITRO.KN_set_compcons(kc, [KNITRO.KN_CCTYPE_VARVAR], Int32[0], Int32[1])

    function newpt_callback(kc, x, lambda_, user_data)
        if KNITRO.KN_get_number_iters(kc) > 1
            return KNITRO.KN_RC_USER_TERMINATION
        end
        return 0
    end

    KNITRO.KN_set_newpt_callback(kc, newpt_callback)

    status = KNITRO.KN_solve(kc)
    @test status == KNITRO.KN_RC_USER_TERMINATION

    nStatus, objSol, x, lambda_ = KNITRO.KN_get_solution(kc)
    @test nStatus == KNITRO.KN_RC_USER_TERMINATION
    @test KNITRO.KN_get_number_iters(kc) == 2

    KNITRO.KN_free(kc)
end

@testset "User params in newpoint callback with license manager (issue #151)" begin
    function callbackNewPoint(kc, x, duals, userParams)
        # Test that inputs are of valid type.
        @test isa(kc, KNITRO.Model)
        @test isa(x, Vector{Cdouble})
        @test isa(duals, Vector{Cdouble})
        return 0
    end
    # Instantiate license manager context.
    lm = KNITRO.LMcontext()
    # Test different values for userparams: nothing is the default value
    # so allows to test default Knitro behavior. "myuserparam" tries
    # setting a string userparam in Knitro.
    for userparam in [nothing, "myuserparam"]
        kc = KNITRO.KN_new_lm(lm)
        KNITRO.KN_set_param(kc, "outlev", 0)
        # At first, `newpoint_user` is nothing
        @test isnothing(kc.newpoint_user)
        KNITRO.KN_add_vars(kc, 1)
        KNITRO.KN_set_var_lobnds(kc, Cint(0), 2.0)
        KNITRO.KN_set_var_primal_init_values(kc, [0.0])
        KNITRO.KN_set_obj_goal(kc, KNITRO.KN_OBJGOAL_MINIMIZE)
        KNITRO.KN_add_obj_quadratic_struct(kc, Cint[0], Cint[0], [1.0])
        KNITRO.KN_set_newpt_callback(kc, callbackNewPoint, userparam)
        # Test that userparam is correctly set
        @test kc.newpoint_user == userparam
        nstatus = KNITRO.KN_solve(kc)
        @test nstatus == KNITRO.KN_RC_OPTIMAL
        KNITRO.KN_free(kc)
    end
    KNITRO.KN_release_license(lm)
end

@testset "Handling exception in callbacks" begin
    function eval_kn(kc, cb, evalRequest, evalResult, userParams)
        # Generate exception in callback
        throw(LoadError)
        return 0
    end

    kc = KNITRO.KN_new()
    KNITRO.KN_set_param(kc, "outlev", 0)
    KNITRO.KN_add_vars(kc, 1)
    KNITRO.KN_set_var_primal_init_values(kc, [0.0])
    cb = KNITRO.KN_add_objective_callback(kc, eval_kn)
    nstatus = KNITRO.KN_solve(kc)
    @test nstatus == KNITRO.KN_RC_CALLBACK_ERR
    KNITRO.KN_free(kc)
end


export AmplModel, AmplException,
       reset!,
       write_sol, amplmodel_finalize, varscale, lagscale, conscale,
       obj, grad, grad!,
       cons, cons!, jth_con, jth_congrad, jth_congrad!, jth_sparse_congrad,
       jac_coord, jac, jprod, jprod!, jtprod, jtprod!,
       jth_hprod, jth_hprod!, ghjvprod, ghjvprod!,
       hess_coord, hess, hprod, hprod!

# Convenience macro.
macro asl_call(func, args...)
  args = map(esc,args)
  quote
    ccall(($(esc(func)), libasl), $(args...))
  end
end

type AmplException
  msg :: String
end

macro check_ampl_model()
  esc(:(nlp.__asl == C_NULL && throw(AmplException("Uninitialized AMPL model"))))
end

type AmplModel <: AbstractNLPModel
  meta  :: NLPModelMeta;     # Problem metadata.
  __asl :: Ptr{Void};        # Pointer to internal ASL structure. Do not touch.

  counters :: Counters       # Evaluation counters
  safe :: Bool               # Always evaluate the objective before the Hessian.
  cvar :: Array{Int32}       # x[cv(i)] is complementary to the constraints cv(i)

  function AmplModel(stub :: AbstractString; safe :: Bool=false)
    asl = @compat @asl_call(:asl_init, Ptr{Void}, (Ptr{UInt8},), stub);
    asl == C_NULL && error("Error allocating ASL structure")

    minimize = @asl_call(:asl_objtype, Int32, (Ptr{Void},), asl) == 0;
    islp = @asl_call(:asl_islp, Int32, (Ptr{Void},), asl) != 0;

    nlo = @compat Int(@asl_call(:asl_nlo, Int32, (Ptr{Void},), asl));

    nvar = @compat Int(@asl_call(:asl_nvar, Int32, (Ptr{Void},), asl));
    ncon = @compat Int(@asl_call(:asl_ncon, Int32, (Ptr{Void},), asl));

    x0   = @compat unsafe_wrap(Array, @asl_call(:asl_x0,   Ptr{Float64}, (Ptr{Void},), asl),
                            (nvar,), false)
    y0   = @compat unsafe_wrap(Array, @asl_call(:asl_y0,   Ptr{Float64}, (Ptr{Void},), asl),
                            (ncon,), false)

    lvar = @compat unsafe_wrap(Array, @asl_call(:asl_lvar, Ptr{Float64}, (Ptr{Void},), asl),
                            (nvar,), false)
    uvar = @compat unsafe_wrap(Array, @asl_call(:asl_uvar, Ptr{Float64}, (Ptr{Void},), asl),
                            (nvar,), false)

    nzo = @compat Int(@asl_call(:asl_nzo, Int32, (Ptr{Void},), asl))
    nbv = @compat Int(@asl_call(:asl_nbv, Int32, (Ptr{Void},), asl))
    niv = @compat Int(@asl_call(:asl_niv, Int32, (Ptr{Void},), asl))
    nlvb = @compat Int(@asl_call(:asl_nlvb, Int32, (Ptr{Void},), asl))
    nlvo = @compat Int(@asl_call(:asl_nlvo, Int32, (Ptr{Void},), asl))
    nlvc = @compat Int(@asl_call(:asl_nlvc, Int32, (Ptr{Void},), asl))
    nlvbi = @compat Int(@asl_call(:asl_nlvbi, Int32, (Ptr{Void},), asl))
    nlvci = @compat Int(@asl_call(:asl_nlvci, Int32, (Ptr{Void},), asl))
    nlvoi = @compat Int(@asl_call(:asl_nlvoi, Int32, (Ptr{Void},), asl))
    nwv = @compat Int(@asl_call(:asl_nwv, Int32, (Ptr{Void},), asl))
#Tangi add's
    n_cc = @compat Int(@asl_call(:asl_n_cc, Int32, (Ptr{Void},), asl))
    nlcc = @compat Int(@asl_call(:asl_nlcc, Int32, (Ptr{Void},), asl))
    ndcc = @compat Int(@asl_call(:asl_ndcc, Int32, (Ptr{Void},), asl))
    nzlb = @compat Int(@asl_call(:asl_nzlb, Int32, (Ptr{Void},), asl))
#Fin Tangi add's
    lcon = @compat unsafe_wrap(Array, @asl_call(:asl_lcon, Ptr{Float64}, (Ptr{Void},), asl),
                            (ncon,), false)
    ucon = @compat unsafe_wrap(Array, @asl_call(:asl_ucon, Ptr{Float64}, (Ptr{Void},), asl),
                            (ncon,), false)
#Tangi add's
    if n_cc>0
    #not sure what to do if nzlb!=0
    cvar = @compat unsafe_wrap(Array, @asl_call(:asl_cvar, Ptr{Int32}, (Ptr{Void},), asl),
                            (ncon,), false)
    @show n_cc nlcc ndcc nzlb cvar nvar ncon
    else
    cvar=[]
    end
#Fin Tangi add's
    nlnet = @compat Int(@asl_call(:asl_lnc, Int32, (Ptr{Void},), asl))
    nnnet = @compat Int(@asl_call(:asl_nlnc, Int32, (Ptr{Void},), asl))
    nnln = @compat(Int(@asl_call(:asl_nlc,  Int32, (Ptr{Void},), asl))) - nnnet
    nlin = ncon - nnln - nnnet

    nln  = 1 : nnln
    nnet = nnln+1 : nnln+nnnet
    lnet = nnln+nnnet+1 : nnln+nnnet+nlnet
    lin  = nnln+nnnet+nlnet+1 : ncon

    nnzj = @compat Int(@asl_call(:asl_nnzj, Int32, (Ptr{Void},), asl))
    nnzh = @compat Int(@asl_call(:asl_nnzh, Int32, (Ptr{Void},), asl))

    meta = NLPModelMeta(nvar, x0=x0, lvar=lvar, uvar=uvar,
                        nlo=nlo, nnzo=nzo,
                        ncon=ncon, y0=y0, lcon=lcon, ucon=ucon,
                        nnzj=nnzj, nnzh=nnzh,
                        nbv=nbv, niv=niv,
                        nlvb=nlvb, nlvo=nlvo, nlvc=nlvc,
                        nlvbi=nlvbi, nlvci=nlvci, nlvoi=nlvoi, nwv=nwv,
                        lin=lin, nln=nln, nnet=nnet, lnet=lnet,
                        nlin=nlin, nnln=nnln, nnet=nnet, nlnet=nlnet,
                        minimize=minimize, islp=islp, name=stub)

    nlp = new(meta, asl, Counters(), safe, cvar)

    finalizer(nlp, amplmodel_finalize)
    return nlp
  end

end

# Import methods we override.
import Base.show, Base.print
import NLPModels.reset!
import NLPModels.varscale, NLPModels.lagscale, NLPModels.conscale
import NLPModels.obj, NLPModels.grad, NLPModels.grad!
import NLPModels.cons, NLPModels.cons!, NLPModels.jth_con
import NLPModels.jth_congrad, NLPModels.jth_congrad!, NLPModels.jth_sparse_congrad
import NLPModels.jac_coord, NLPModels.jac
import NLPModels.jprod, NLPModels.jtprod, NLPModels.jprod!, NLPModels.jtprod!
import NLPModels.jth_hprod, NLPModels.jth_hprod!
import NLPModels.ghjvprod, NLPModels.ghjvprod!
import NLPModels.hess_coord, NLPModels.hess, NLPModels.hprod, NLPModels.hprod!
import NLPModels.NLPtoMPB

# Methods associated to AmplModel instances.

"Reset evaluation counters in `nlp`."
function reset!(nlp :: AmplModel)
  reset!(nlp.counters)
  return nlp
end

"Write message `msg` along with primal and dual variables `x` and `y` to file."
function write_sol(nlp :: AmplModel, msg :: String, x :: Vector{Float64}, y :: Vector{Float64})
  @check_ampl_model
  length(x) == nlp.meta.nvar || error("x must have length $(nlp.meta.nvar)")
  length(y) == nlp.meta.ncon || error("y must have length $(nlp.meta.ncon)")

  @compat @asl_call(:asl_write_sol, Void,
                    (Ptr{Void}, Ptr{UInt8}, Ptr{Float64}, Ptr{Float64}),
                     nlp.__asl, msg,        x,            y)
end

function amplmodel_finalize(nlp :: AmplModel)
  if nlp.__asl == C_NULL
    return
  end
  @asl_call(:asl_finalize, Void, (Ptr{Void},), nlp.__asl)
  nlp.__asl = C_NULL
end

# Displaying AmplModel instances.

function show(io :: IO, nlp :: AmplModel)
  @check_ampl_model
  show(io, nlp.meta)
end

function print(io :: IO, nlp :: AmplModel)
  @check_ampl_model
  print(io, nlp.meta)
end

# Scaling AmplModel instances.

"Scale the vector of variables by the vector `s`."
function varscale(nlp :: AmplModel, s :: Vector{Float64})
  @check_ampl_model
  length(s) >= nlp.meta.nvar || error("s must have length at least $(nlp.meta.nvar)")

  err = Cint[0]
  @asl_call(:asl_varscale, Void, (Ptr{Void}, Ptr{Float64}, Ptr{Cint}), nlp.__asl, s, err)
  err[1] == 0 || throw(AmplException("Error while scaling variables"))
end

"""Set the scaling factor σ in the Lagrangian:
    L(x,y) = f(x) + σ ∑ yi ci(x).
"""
function lagscale(nlp :: AmplModel, σ :: Float64)
  @check_ampl_model
  err = Cint[0]
  @asl_call(:asl_lagscale, Void, (Ptr{Void}, Float64, Ptr{Cint}), nlp.__asl, σ, err)
  err[1] == 0 || throw(AmplException("Error while scaling Lagrangian"))
end

"Scale the vector of constraints by the vector `s`."
function conscale(nlp :: AmplModel, s :: Vector{Float64})
  @check_ampl_model
  length(s) >= nlp.meta.ncon || error("s must have length at least $(nlp.meta.ncon)")

  err = Cint[0]
  @asl_call(:asl_conscale, Void, (Ptr{Void}, Ptr{Float64}, Ptr{Cint}), nlp.__asl, s, err)
  err[1] == 0 || throw(AmplException("Error while scaling constraints"))
end

# Evaluating objective, constraints and derivatives.

"Evaluate the objective function of `nlp` at `x`."
function obj(nlp :: AmplModel, x :: Vector{Float64})
  @check_ampl_model
  length(x) >= nlp.meta.nvar || error("x must have length at least $(nlp.meta.nvar)")

  err = Cint[0]
  f = @asl_call(:asl_obj, Float64, (Ptr{Void}, Ptr{Float64}, Ptr{Cint}), nlp.__asl, x, err)
  nlp.counters.neval_obj += 1
  err[1] == 0 || throw(AmplException("Error while evaluating objective"))
  return f
end

"Evaluate the gradient of the objective function at `x`."
function grad(nlp :: AmplModel, x :: Vector{Float64})
  g = Array{Float64}(nlp.meta.nvar)
  return grad!(nlp, x, g)
end

"Evaluate the gradient of the objective function at `x` in place."
function grad!(nlp :: AmplModel, x :: Vector{Float64}, g :: Vector{Float64})
  @check_ampl_model
  length(x) >= nlp.meta.nvar || error("x must have length at least $(nlp.meta.nvar)")

  err = Cint[0]
  @asl_call(:asl_grad, Ptr{Float64},
            (Ptr{Void}, Ptr{Float64}, Ptr{Float64}, Ptr{Cint}),
             nlp.__asl, x,            g,            err)
  nlp.counters.neval_grad += 1
  err[1] == 0 || throw(AmplException("Error while evaluating objective gradient"))
  return g
end

"Evaluate the constraints at `x`."
function cons(nlp :: AmplModel, x :: Vector{Float64})
  c = Array{Float64}(nlp.meta.ncon)
  return cons!(nlp, x, c)
end

"Evaluate the constraints at `x` in place."
function cons!(nlp :: AmplModel, x :: Vector{Float64}, c :: Vector{Float64})
  @check_ampl_model
  length(x) >= nlp.meta.nvar || error("x must have length at least $(nlp.meta.nvar)")

  err = Cint[0]
  @asl_call(:asl_cons, Void,
            (Ptr{Void}, Ptr{Float64}, Ptr{Float64}, Ptr{Cint}),
             nlp.__asl, x,            c,            err)
  nlp.counters.neval_cons += 1
  err[1] == 0 || throw(AmplException("Error while evaluating constraints"))
  return c
end

"Evaluate the `j`-th constraint at `x`."
function jth_con(nlp :: AmplModel, x :: Vector{Float64}, j :: Int)
  @check_ampl_model
  (1 <= j <= nlp.meta.ncon)  || error("expected 0 ≤ j ≤ $(nlp.meta.ncon)")
  length(x) >= nlp.meta.nvar || error("x must have length at least $(nlp.meta.nvar)")

  err = Cint[0]
  cj = @asl_call(:asl_jcon, Float64,
                 (Ptr{Void}, Ptr{Float64}, Int32, Ptr{Cint}),
                  nlp.__asl, x,            j-1,   err)
  nlp.counters.neval_jcon += 1
  err[1] == 0 || throw(AmplException("Error while evaluating $j-th constraint"))
  return cj
end

"Evaluate the `j`-th constraint gradient at `x`."
function jth_congrad(nlp :: AmplModel, x :: Vector{Float64}, j :: Int)
  g = Array{Float64}(nlp.meta.nvar)
  return jth_congrad!(nlp, x, j, g)
end

"Evaluate the `j`-th constraint gradient at `x` in place."
function jth_congrad!(nlp :: AmplModel, x :: Vector{Float64}, j :: Int, g :: Vector{Float64})
  @check_ampl_model
  (1 <= j <= nlp.meta.ncon)  || error("expected 0 ≤ j ≤ $(nlp.meta.ncon)")
  length(x) >= nlp.meta.nvar || error("x must have length at least $(nlp.meta.nvar)")

  err = Cint[0]
  @asl_call(:asl_jcongrad, Ptr{Float64},
            (Ptr{Void}, Ptr{Float64}, Ptr{Float64}, Int32, Ptr{Cint}),
             nlp.__asl, x,            g,            j-1,   err)
  nlp.counters.neval_jgrad += 1
  err[1] == 0 || throw(AmplException("Error while evaluating $j-th constraint gradient"))
  return g
end

"Evaluate the `j`-th constraint sparse gradient at `x`."
function jth_sparse_congrad(nlp :: AmplModel, x :: Vector{Float64}, j :: Int)
  @check_ampl_model
  (1 <= j <= nlp.meta.ncon)  || error("expected 0 ≤ j ≤ $(nlp.meta.ncon)")
  length(x) >= nlp.meta.nvar || error("x must have length at least $(nlp.meta.nvar)")

  nnz = @asl_call(:asl_sparse_congrad_nnz, Csize_t,
                  (Ptr{Void}, Cint), nlp.__asl, j-1)

  err = Cint[0]
  inds = Array{Int64}(nnz)
  vals = Array{Float64}(nnz)
  @asl_call(:asl_sparse_congrad, Void,
            (Ptr{Void}, Ptr{Float64}, Int32, Ptr{Int64}, Ptr{Float64}, Ptr{Cint}),
             nlp.__asl, x,            j-1,   inds,       vals,         err)
  nlp.counters.neval_jgrad += 1
  err[1] == 0 || throw(AmplException("Error while evaluating $j-th sparse constraint gradient"))
  # Use 1-based indexing.
  return sparsevec(inds+1, vals, nlp.meta.nvar)
end

"Evaluate the constraints Jacobian at `x` in sparse coordinate format."
function jac_coord(nlp :: AmplModel, x :: Vector{Float64})
  @check_ampl_model
  length(x) >= nlp.meta.nvar || error("x must have length at least $(nlp.meta.nvar)")

  err = Cint[0]
  rows = Array{Int64}(nlp.meta.nnzj)
  cols = Array{Int64}(nlp.meta.nnzj)
  vals = Array{Float64}(nlp.meta.nnzj)
  @asl_call(:asl_jac, Void,
            (Ptr{Void}, Ptr{Float64}, Ptr{Int64}, Ptr{Int64}, Ptr{Float64}, Ptr{Cint}),
             nlp.__asl, x,            rows,       cols,       vals,         err)
  nlp.counters.neval_jac += 1
  err[1] == 0 || throw(AmplException("Error while evaluating constraints Jacobian"))
  # Use 1-based indexing.
  return (rows+1, cols+1, vals)
end

"Evaluate the constraints Jacobian at `x` as a sparse matrix."
function jac(nlp :: AmplModel, x :: Vector{Float64})
  @check_ampl_model
  (rows, cols, vals) = jac_coord(nlp, x)
  return sparse(rows, cols, vals, nlp.meta.ncon, nlp.meta.nvar)
end

"""
Evaluate the Jacobian-vector product at `x`.
Warning: Currently building the Jacobian for this.
"""
function jprod(nlp :: AmplModel, x :: Vector{Float64}, v :: Vector{Float64})
  Jv = zeros(nlp.meta.ncon)
  return jprod!(nlp, x, v, Jv)
end

"""
Evaluate the Jacobian-vector product at `x` in place.
Warning: Currently building the Jacobian for this.
"""
function jprod!(nlp :: AmplModel,
                x :: Vector{Float64},
                v :: Vector{Float64},
                Jv :: Vector{Float64})
  nlp.counters.neval_jac -= 1
  nlp.counters.neval_jprod += 1
  Jv[1:nlp.meta.ncon] = jac(nlp, x) * v
  return Jv
end

"""
Evaluate the transposed-Jacobian-vector product at `x`.
Warning: Currently building the Jacobian for this.
"""
function jtprod(nlp :: AmplModel, x :: Vector{Float64}, v :: Vector{Float64})
  Jtv = zeros(nlp.meta.nvar)
  return jtprod!(nlp, x, v, Jtv)
end

"""
Evaluate the transposed-Jacobian-vector product at `x` in place.
Warning: Currently building the Jacobian for this.
"""
function jtprod!(nlp :: AmplModel,
                 x :: Vector{Float64},
                 v :: Vector{Float64},
                 Jtv :: Vector{Float64})
  nlp.counters.neval_jac -= 1
  nlp.counters.neval_jtprod += 1
  Jtv[1:nlp.meta.nvar] = jac(nlp, x)' * v
  return Jtv
end

"Evaluate the product of the Lagrangian Hessian at `(x,y)` with the vector `v`."
function hprod(nlp :: AmplModel,
               x :: Vector{Float64},
               v :: Vector{Float64};
               y :: Vector{Float64} = nlp.meta.y0,
               obj_weight :: Float64 = 1.0)
  hv = Array{Float64}(nlp.meta.nvar);
  return hprod!(nlp, x, v, hv, y=y, obj_weight=obj_weight)
end

"Evaluate the product of the Lagrangian Hessian at `(x,y)` with the vector `v` in place."
function hprod!(nlp :: AmplModel,
                x :: Vector{Float64},
                v :: Vector{Float64},
                hv :: Vector{Float64};
                y :: Vector{Float64} = nlp.meta.y0,
                obj_weight :: Float64 = 1.0)
  # Note: x is in fact not used in hprod.
  @check_ampl_model
  length(x) >= nlp.meta.nvar || error("x must have length at least $(nlp.meta.nvar)")
  length(y) >= nlp.meta.ncon || error("y must have length at least $(nlp.meta.ncon)")
  length(v) >= nlp.meta.nvar || error("v must have length at least $(nlp.meta.nvar)")

  if nlp.safe
    _ = obj(nlp, x) ; nlp.counters.neval_obj -= 1
    _ = cons(nlp, x) ; nlp.counters.neval_cons -= 1
  end
  @asl_call(:asl_hprod, Ptr{Float64},
            (Ptr{Void}, Ptr{Float64}, Ptr{Float64}, Ptr{Float64}, Float64),
             nlp.__asl, y,            v,            hv,           obj_weight);
  nlp.counters.neval_hprod += 1
  return hv
end

"""Evaluate the product of the `j`-th constraint Hessian at `x` with the vector `v`.
The objective Hessian is used if `j=0`.
"""
function jth_hprod(nlp :: AmplModel,
                   x :: Vector{Float64}, v :: Vector{Float64}, j :: Int)
  hv = Array{Float64}(nlp.meta.nvar)
  return jth_hprod!(nlp, x, v, j, hv)
end

"""Evaluate the product of the `j`-th constraint Hessian at `x` with the vector `v` in place.
The objective Hessian is used if `j=0`.
"""
function jth_hprod!(nlp :: AmplModel,
                    x :: Vector{Float64}, v :: Vector{Float64},
                    j :: Int, hv :: Vector{Float64})
# Note: x is in fact not used in hprod.
  @check_ampl_model
  length(x) >= nlp.meta.nvar || error("x must have length at least $(nlp.meta.nvar)")
  length(v) >= nlp.meta.nvar || error("v must have length at least $(nlp.meta.nvar)")
  (1 <= j <= nlp.meta.ncon)  || error("expected 0 ≤ j ≤ $(nlp.meta.ncon)")

  if nlp.safe
    if j == 0
      _ = obj(nlp, x) ; nlp.counters.neval_obj -= 1
    else
      _ = cons(nlp, x) ; nlp.counters.neval_cons -= 1
    end
  end
  @asl_call(:asl_hvcompd, Ptr{Float64},
            (Ptr{Void}, Ptr{Float64}, Ptr{Float64}, Int),
             nlp.__asl, v,            hv,           j-1);
  nlp.counters.neval_jhprod += 1
  return hv
end

"""Compute the vector of dot products `(g, Hj*v)`
where `Hj` is the Hessian of the `j`-th constraint at `x`.
"""
function ghjvprod(nlp :: AmplModel,
                  x :: Vector{Float64}, g :: Vector{Float64}, v :: Vector{Float64})
  gHv = Array{Float64}(nlp.meta.ncon);
  return ghjvprod!(nlp, x, g, v, gHv)
end

"""Compute the vector of dot products `(g, Hj*v)` in place
where `Hj` is the Hessian of the `j`-th constraint at `x`.
"""
function ghjvprod!(nlp :: AmplModel,
                   x :: Vector{Float64}, g :: Vector{Float64},
                   v :: Vector{Float64}, gHv :: Vector{Float64})
  # Note: x is in fact not used.
  @check_ampl_model
  length(x) >= nlp.meta.nvar || error("x must have length at least $(nlp.meta.nvar)")
  length(g) >= nlp.meta.nvar || error("g must have length at least $(nlp.meta.nvar)")
  length(v) >= nlp.meta.nvar || error("v must have length at least $(nlp.meta.nvar)")

  if nlp.safe
    _ = cons(nlp, x) ; nlp.counters.neval_cons -= 1
  end
  @asl_call(:asl_ghjvprod, Ptr{Float64},
            (Ptr{Void}, Ptr{Float64}, Ptr{Float64}, Ptr{Float64}),
             nlp.__asl, g,            v,            gHv);
  nlp.counters.neval_hprod += nlp.meta.ncon
end

"""Evaluate the Lagrangian Hessian at `(x,y)` in sparse coordinate format.
Only the lower triangle is returned.
"""
function hess_coord(nlp :: AmplModel,
                    x :: Vector{Float64};
                    y :: Vector{Float64} = nlp.meta.y0,
                    obj_weight :: Float64 = 1.0)
  # Note: x is in fact not used.
  @check_ampl_model
  length(x) >= nlp.meta.nvar || error("x must have length at least $(nlp.meta.nvar)")
  length(y) >= nlp.meta.ncon || error("y must have length at least $(nlp.meta.ncon)")

  if nlp.safe
    _ = obj(nlp, x) ; nlp.counters.neval_obj -= 1
    _ = cons(nlp, x) ; nlp.counters.neval_cons -= 1
  end

  rows = Array{Int64}(nlp.meta.nnzh)
  cols = Array{Int64}(nlp.meta.nnzh)
  vals = Array{Float64}(nlp.meta.nnzh)
  @asl_call(:asl_hess, Void,
            (Ptr{Void}, Ptr{Float64}, Float64,    Ptr{Int64}, Ptr{Int64}, Ptr{Float64}),
             nlp.__asl, y,            obj_weight, rows,       cols,       vals)
  nlp.counters.neval_hess += 1
  # Use 1-based indexing.
  # Swap rows and cols to obtain the lower triangle.
  return (cols+1, rows+1, vals)
end

"""Evaluate the Lagrangian Hessian at `(x,y)` as a sparse matrix.
Only the lower triangle is returned.
"""
function hess(nlp :: AmplModel,
              x :: Vector{Float64};
              y :: Vector{Float64} = nlp.meta.y0,
              obj_weight :: Float64 = 1.0)
  @check_ampl_model
  (rows, cols, vals) = hess_coord(nlp, x, y=y, obj_weight=obj_weight);
  return sparse(rows, cols, vals, nlp.meta.nvar, nlp.meta.nvar)
end


extract_val(::Type{Val{X}}) where X = X
function rewrite_backend_specific(m::CLMethod, f::F, types, expr) where F
    li = m.parent
    if f == (ifelse) && length(types) == 3 && types[1] == Bool && all(x-> x <: cli.Types || is_fixedsize_array(x), types[2:3])
        _types = (types[2], types[3], types[1])
        sm = LazyMethod(li, cli.cl_select, _types)
        expr.args = [sm, expr.args[3], expr.args[4], expr.args[2]]
        return true, expr
    elseif f == abs && length(types) == 1 && all(t-> t <: cli.Floats, types)
        expr.args[1] = LazyMethod(li, fabs, types)
        return true, expr
    elseif f == mod && length(types) == 2 && all(t-> t <: cli.Ints, types)
        expr.args[1] = LazyMethod(li, %, types)
        return true, expr
    elseif f == rem && length(types) == 2 && all(t-> t <: cli.Ints, types)
        expr.args[1] = LazyMethod(li, %, types)
        return true, expr
    elseif f == muladd && length(types) == 3 && all(t-> t <: cli.Floats, types)

        expr.args[1] = LazyMethod(li, fma, types)
        return true, expr
    elseif Symbol(f) == :synchronize_threads
        expr.args[1] = LazyMethod(li, cli.barrier, (Cuint,))
        expr.args[2] = :CLK_LOCAL_MEM_FENCE
        return true, expr
    elseif Symbol(f) == Symbol("GPUArrays.LocalMemory")
        if length(expr.args) != 5
            error("LocalMemory needs to be used as: LocalMemory(state, T, N), with T & N being constants / static function parameters")
        end
        T = Sugar.rewrite_ast.(m, expr.args[3])
        N = expr.args[4].args[1]
        if !(isa(T, Type)) #&& (isa(N, Val{<:Integer}) || isa(N, Val{NTuple{N, Integer}} where N)))
             error("LocalMemory needs to be used as: LocalMemory(T, N), with T being a type and Val{N} where N <: Union{Integer, Tuple}. Found: $T $N")
        end
        name = gensym("localmem")
        ret = Expr(:local_memory, T, name)
        ret.typ = cli.LocalPointer{T}
        inline = Sugar.InlineNode([Expr(:local_memory_init, T, prod(extract_val(N)), name)], ret)
        return true, inline
    else
        return false, expr
    end
end