module ParametricOptInterface

using MathOptInterface

const MOI = MathOptInterface

const PARAMETER_INDEX_THRESHOLD = 1_000_000_000_000_000_000

@enum ConstraintsInterpretationCode ONLY_CONSTRAINTS ONLY_BOUNDS BOUNDS_AND_CONSTRAINTS

"""
    Parameter(val::Float64)

The `Parameter` structure stores the numerical value associated to a given
parameter.

## Example

```julia-repl
julia> ParametricOptInterface.Parameter(5)
ParametricOptInterface.Parameter(5)
```
"""
struct Parameter <: MOI.AbstractScalarSet
    val::Float64
end

# Utilities for using a CleverDict in Parameters
struct ParameterIndex
    index::Int64
end
function MOI.Utilities.CleverDicts.index_to_key(
    ::Type{ParameterIndex},
    index::Int64,
)
    return ParameterIndex(index)
end
function MOI.Utilities.CleverDicts.key_to_index(key::ParameterIndex)
    return key.index
end
function p_idx(vi::MOI.VariableIndex)
    return ParameterIndex(vi.value - PARAMETER_INDEX_THRESHOLD)
end

"""
    Optimizer{T, OT <: MOI.ModelLike} <: MOI.AbstractOptimizer

Declares a `Optimizer`, which allows the handling of parameters in a
optimization model.

## Keyword arguments

- `evaluate_duals::Bool`: If `true`, evaluates the dual of parameters. Users might want to set it to false
  to increase performance when the duals of parameters are not necessary. Defaults to `true`.

- `constraints_interpretation`: Decides how to interpret constraints with `ScalarAffineFunctions`. More details
  are in [`POI.ConstraintsInterpretation`](@ref) and [JuMP Example - Dealing with parametric expressions as variable bounds](@ref).
  Defaults to `ONLY_CONSTRAINTS`.

## Example

```julia-repl
julia> ParametricOptInterface.Optimizer(GLPK.Optimizer())
ParametricOptInterface.Optimizer{Float64,GLPK.Optimizer}
```
"""
mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer
    optimizer::OT
    parameters::MOI.Utilities.CleverDicts.CleverDict{
        ParameterIndex,
        T,
        typeof(MOI.Utilities.CleverDicts.key_to_index),
        typeof(MOI.Utilities.CleverDicts.index_to_key),
    }
    parameters_name::Dict{MOI.VariableIndex,String}
    # The updated_parameters dictionary has the same dimension of the
    # parameters dictionary and if the value stored is a NaN is means
    # that the parameter has not been updated.
    updated_parameters::MOI.Utilities.CleverDicts.CleverDict{
        ParameterIndex,
        T,
        typeof(MOI.Utilities.CleverDicts.key_to_index),
        typeof(MOI.Utilities.CleverDicts.index_to_key),
    }
    variables::MOI.Utilities.CleverDicts.CleverDict{
        MOI.VariableIndex,
        MOI.VariableIndex,
        typeof(MOI.Utilities.CleverDicts.key_to_index),
        typeof(MOI.Utilities.CleverDicts.index_to_key),
    }
    last_variable_index_added::Int64
    last_parameter_index_added::Int64
    # Store the constraint function and set passed to POI by MOI
    original_constraint_function_and_set_cache::Dict{
        MOI.ConstraintIndex,
        Tuple{MOI.AbstractFunction,MOI.AbstractSet},
    }
    # Store the map for SAFs that might be transformed into VI
    affine_added_cache::MOI.Utilities.DoubleDicts.DoubleDict{
        MOI.ConstraintIndex,
    }
    last_affine_added::Int64
    # Store reference to parameters of affine constraints with parameters: v + p
    affine_constraint_cache::MOI.Utilities.DoubleDicts.DoubleDict{
        Vector{MOI.ScalarAffineTerm{Float64}},
    }
    # Store reference quadratic constraints with parameter * variable constraints: p * v
    quadratic_constraint_cache_pv::MOI.Utilities.DoubleDicts.DoubleDict{
        Vector{MOI.ScalarQuadraticTerm{Float64}},
    }
    # Store reference quadratic constraints with parameter * variable constraints: p * v
    quadratic_constraint_cache_pp::MOI.Utilities.DoubleDicts.DoubleDict{
        Vector{MOI.ScalarQuadraticTerm{Float64}},
    }
    # Store reference to constraints with quad_variable_term + affine_with_parameters: v * v + p
    quadratic_constraint_cache_pc::MOI.Utilities.DoubleDicts.DoubleDict{
        Vector{MOI.ScalarAffineTerm{Float64}},
    }
    # Store the reference to variables in the scalar affine part that are
    # multiplied by parameters in the scalar quadratic terms.
    # i.e.
    # If we have a constraint function with both scalar quadratic terms and
    # scalar affine terms such as p_1 * v_1 + 2.0 * v_1
    # When we need to update the constraint coefficient after updating the parameter
    # we must do (new_p_1 + 2.0) * v_1
    # This cache is storing the 2.0 * v_1 part.
    quadratic_constraint_variables_associated_to_parameters_cache::MOI.Utilities.DoubleDicts.DoubleDict{
        Vector{MOI.ScalarAffineTerm{T}},
    }
    # Store the map for SQFs that might be transformed into SAF
    # for instance p*p + var -> ScalarAffine(var)
    quadratic_added_cache::Dict{MOI.ConstraintIndex,MOI.ConstraintIndex}
    last_quad_add_added::Int64
    vector_constraint_cache::MOI.Utilities.DoubleDicts.DoubleDict{
        Vector{MOI.VectorAffineTerm{Float64}},
    }
    # Ditto from the constraint caches but for the objective function
    original_objective_function::MOI.AbstractFunction
    affine_objective_cache::Vector{MOI.ScalarAffineTerm{T}}
    quadratic_objective_cache_pv::Vector{MOI.ScalarQuadraticTerm{T}}
    quadratic_objective_cache_pp::Vector{MOI.ScalarQuadraticTerm{T}}
    quadratic_objective_cache_pc::Vector{MOI.ScalarAffineTerm{T}}
    quadratic_objective_variables_associated_to_parameters_cache::Vector{
        MOI.ScalarAffineTerm{T},
    }
    multiplicative_parameters::Set{Int64}
    dual_value_of_parameters::Vector{Float64}
    evaluate_duals::Bool
    number_of_parameters_in_model::Int64
    constraints_interpretation::ConstraintsInterpretationCode
    function Optimizer(optimizer::OT; evaluate_duals::Bool = true) where {OT}
        return new{Float64,OT}(
            optimizer,
            MOI.Utilities.CleverDicts.CleverDict{ParameterIndex,Float64}(
                MOI.Utilities.CleverDicts.key_to_index,
                MOI.Utilities.CleverDicts.index_to_key,
            ),
            Dict{MOI.VariableIndex,String}(),
            MOI.Utilities.CleverDicts.CleverDict{ParameterIndex,Float64}(
                MOI.Utilities.CleverDicts.key_to_index,
                MOI.Utilities.CleverDicts.index_to_key,
            ),
            MOI.Utilities.CleverDicts.CleverDict{
                MOI.VariableIndex,
                MOI.VariableIndex,
            }(
                MOI.Utilities.CleverDicts.key_to_index,
                MOI.Utilities.CleverDicts.index_to_key,
            ),
            0,
            PARAMETER_INDEX_THRESHOLD,
            Dict{
                MOI.ConstraintIndex,
                Tuple{MOI.AbstractFunction,MOI.AbstractSet},
            }(),
            MOI.Utilities.DoubleDicts.DoubleDict{MOI.ConstraintIndex}(),
            0,
            MOI.Utilities.DoubleDicts.DoubleDict{
                Vector{MOI.ScalarAffineTerm{Float64}},
            }(),
            MOI.Utilities.DoubleDicts.DoubleDict{
                Vector{MOI.ScalarQuadraticTerm{Float64}},
            }(),
            MOI.Utilities.DoubleDicts.DoubleDict{
                Vector{MOI.ScalarQuadraticTerm{Float64}},
            }(),
            MOI.Utilities.DoubleDicts.DoubleDict{
                Vector{MOI.ScalarAffineTerm{Float64}},
            }(),
            MOI.Utilities.DoubleDicts.DoubleDict{
                Vector{MOI.ScalarAffineTerm{Float64}},
            }(),
            Dict{MOI.ConstraintIndex,MOI.ConstraintIndex}(),
            0,
            MOI.Utilities.DoubleDicts.DoubleDict{
                Vector{MOI.VectorAffineTerm{Float64}},
            }(),
            MOI.VariableIndex(-1),
            Vector{MOI.ScalarAffineTerm{Float64}}(),
            Vector{MOI.ScalarQuadraticTerm{Float64}}(),
            Vector{MOI.ScalarQuadraticTerm{Float64}}(),
            Vector{MOI.ScalarAffineTerm{Float64}}(),
            Vector{MOI.ScalarAffineTerm{Float64}}(),
            Set{Int64}(),
            Vector{Float64}(),
            evaluate_duals,
            0,
            ONLY_CONSTRAINTS,
        )
    end
end

include("utils.jl")
include("duals.jl")
include("update_parameters.jl")

function MOI.is_empty(model::Optimizer)
    return MOI.is_empty(model.optimizer) &&
           isempty(model.parameters) &&
           isempty(model.parameters_name) &&
           isempty(model.variables) &&
           isempty(model.updated_parameters) &&
           isempty(model.variables) &&
           model.last_variable_index_added == 0 &&
           model.last_parameter_index_added == PARAMETER_INDEX_THRESHOLD &&
           isempty(model.original_constraint_function_and_set_cache) &&
           isempty(model.affine_added_cache) &&
           model.last_affine_added == 0 &&
           isempty(model.affine_constraint_cache) &&
           isempty(model.quadratic_constraint_cache_pv) &&
           isempty(model.quadratic_constraint_cache_pp) &&
           isempty(model.quadratic_constraint_cache_pc) &&
           isempty(
               model.quadratic_constraint_variables_associated_to_parameters_cache,
           ) &&
           isempty(model.quadratic_added_cache) &&
           model.last_quad_add_added == 0 &&
           model.original_objective_function == MOI.VariableIndex(-1) &&
           isempty(model.affine_objective_cache) &&
           isempty(model.quadratic_objective_cache_pv) &&
           isempty(model.quadratic_objective_cache_pp) &&
           isempty(model.quadratic_objective_cache_pc) &&
           isempty(
               model.quadratic_objective_variables_associated_to_parameters_cache,
           ) &&
           isempty(model.dual_value_of_parameters) &&
           model.number_of_parameters_in_model == 0
end

function MOI.supports_constraint(
    model::Optimizer,
    F::Union{
        Type{MOI.VariableIndex},
        Type{MOI.ScalarAffineFunction{T}},
        Type{MOI.VectorOfVariables},
        Type{MOI.VectorAffineFunction{T}},
    },
    S::Type{<:MOI.AbstractSet},
) where {T}
    return MOI.supports_constraint(model.optimizer, F, S)
end

function MOI.supports_constraint(
    model::Optimizer,
    ::Type{MOI.ScalarQuadraticFunction{T}},
    S::Type{<:MOI.AbstractSet},
) where {T}
    return MOI.supports_constraint(
        model.optimizer,
        MOI.ScalarAffineFunction{T},
        S,
    )
end

function MOI.supports_constraint(
    model::Optimizer,
    ::Type{MOI.VectorQuadraticFunction{T}},
    S::Type{<:MOI.AbstractSet},
) where {T}
    return MOI.supports_constraint(
        model.optimizer,
        MOI.VectorAffineFunction{T},
        S,
    )
end

function MOI.supports(
    model::Optimizer,
    attr::Union{
        MOI.ObjectiveSense,
        MOI.ObjectiveFunction{MOI.VariableIndex},
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}},
    },
) where {T}
    return MOI.supports(model.optimizer, attr)
end

function MOI.supports(model::Optimizer, ::MOI.NLPBlock)
    return MOI.supports(model.optimizer, MOI.NLPBlock())
end

function MOI.set(model::Optimizer, ::MOI.NLPBlock, nlp_data::MOI.NLPBlockData)
    return MOI.set(model.optimizer, MOI.NLPBlock(), nlp_data)
end

function MOI.supports(
    model::Optimizer,
    ::MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{T}},
) where {T}
    return MOI.supports(
        model.optimizer,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}(),
    )
end

function MOI.supports_incremental_interface(model::Optimizer)
    return MOI.supports_incremental_interface(model.optimizer)
end
function MOI.supports(model::Optimizer, ::MOI.Name)
    return MOI.supports(model.optimizer, MOI.Name())
end
function MOI.get(model::Optimizer, ::MOI.ListOfModelAttributesSet)
    return MOI.get(model.optimizer, MOI.ListOfModelAttributesSet())
end
MOI.get(model::Optimizer, ::MOI.Name) = MOI.get(model.optimizer, MOI.Name())
function MOI.set(model::Optimizer, ::MOI.Name, name::String)
    return MOI.set(model.optimizer, MOI.Name(), name)
end
function MOI.get(model::Optimizer, ::MOI.ListOfVariableIndices)
    return MOI.get(model.optimizer, MOI.ListOfVariableIndices())
end
function MOI.get(model::Optimizer, ::MOI.ListOfVariableAttributesSet)
    return MOI.get(model.optimizer, MOI.ListOfVariableAttributesSet())
end
function MOI.get(
    model::Optimizer,
    ::MOI.ListOfConstraintAttributesSet{F,S},
) where {F,S}
    if F === MOI.ScalarQuadraticFunction
        error(
            "MOI.ListOfConstraintAttributesSet is not implemented for ScalarQuadraticFunction.",
        )
    end
    return MOI.get(model.optimizer, MOI.ListOfConstraintAttributesSet{F,S}())
end
function MOI.set(
    model::Optimizer,
    ::MOI.ConstraintFunction,
    c::MOI.ConstraintIndex{F,S},
    f::F,
) where {F,S}
    MOI.set(model.optimizer, MOI.ConstraintFunction(), c, f)
    return
end
function MOI.set(
    model::Optimizer,
    ::MOI.ConstraintSet,
    c::MOI.ConstraintIndex{F,S},
    s::S,
) where {F,S}
    MOI.set(model.optimizer, MOI.ConstraintSet(), c, s)
    return
end
function MOI.modify(
    model::Optimizer,
    c::MOI.ConstraintIndex{F,S},
    chg::MOI.ScalarCoefficientChange{Float64},
) where {F,S}
    MOI.modify(model.optimizer, c, chg)
    return
end
function MOI.modify(
    model::Optimizer,
    c::MOI.ObjectiveFunction{F},
    chg::MOI.ScalarCoefficientChange{Float64},
) where {F<:MathOptInterface.AbstractScalarFunction}
    MOI.modify(model.optimizer, c, chg)
    return
end
function MOI.modify(
    model::Optimizer,
    c::MOI.ObjectiveFunction{F},
    chg::MOI.ScalarConstantChange{Float64},
) where {F<:MathOptInterface.AbstractScalarFunction}
    MOI.modify(model.optimizer, c, chg)
    return
end

function MOI.empty!(model::Optimizer{T}) where {T}
    MOI.empty!(model.optimizer)
    empty!(model.parameters)
    empty!(model.parameters_name)
    empty!(model.updated_parameters)
    empty!(model.variables)
    model.last_variable_index_added = 0
    model.last_parameter_index_added = PARAMETER_INDEX_THRESHOLD
    empty!(model.original_constraint_function_and_set_cache)
    empty!(model.affine_added_cache)
    model.last_affine_added = 0
    empty!(model.affine_constraint_cache)
    empty!(model.quadratic_constraint_cache_pv)
    empty!(model.quadratic_constraint_cache_pp)
    empty!(model.quadratic_constraint_cache_pc)
    empty!(model.quadratic_constraint_variables_associated_to_parameters_cache)
    empty!(model.quadratic_added_cache)
    model.last_quad_add_added = 0
    model.original_objective_function = MOI.VariableIndex(-1)
    empty!(model.vector_constraint_cache)
    empty!(model.affine_objective_cache)
    empty!(model.quadratic_objective_cache_pv)
    empty!(model.quadratic_objective_cache_pp)
    empty!(model.quadratic_objective_cache_pc)
    empty!(model.quadratic_objective_variables_associated_to_parameters_cache)
    empty!(model.dual_value_of_parameters)
    model.number_of_parameters_in_model = 0
    return
end

function MOI.set(
    model::Optimizer,
    attr::MOI.VariableName,
    v::MOI.VariableIndex,
    name::String,
)
    if is_parameter_in_model(model, v)
        model.parameters_name[v] = name
    else
        MOI.set(model.optimizer, attr, v, name)
    end
    return
end

function MOI.get(model::Optimizer, attr::MOI.VariableName, v::MOI.VariableIndex)
    if is_parameter_in_model(model, v)
        return get(model.parameters_name, v, "")
    else
        return MOI.get(model.optimizer, attr, v)
    end
end

function MOI.get(model::Optimizer, tp::Type{MOI.VariableIndex}, attr::String)
    return MOI.get(model.optimizer, tp, attr)
end

function MOI.get(model::Optimizer, attr::MOI.ObjectiveBound)
    return MOI.get(model.optimizer, attr)
end

function MOI.supports(
    model::Optimizer,
    attr::MOI.VariableName,
    tp::Type{MOI.VariableIndex},
)
    return MOI.supports(model.optimizer, attr, tp)
end

function MOI.set(
    model::Optimizer,
    attr::MOI.ConstraintName,
    c::MOI.ConstraintIndex,
    name::String,
)
    MOI.set(model.optimizer, attr, c, name)
    return
end

function MOI.get(
    model::Optimizer,
    attr::MOI.ConstraintName,
    c::MOI.ConstraintIndex,
)
    return MOI.get(model.optimizer, attr, c)
end

function MOI.get(
    model::Optimizer,
    attr::MOI.ConstraintName,
    c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S},
) where {T,S}
    moi_ci = get(model.affine_added_cache, c, c)
    # This SAF constraint was transformed into variable bound
    if typeof(moi_ci) === MOI.ConstraintIndex{MOI.VariableIndex,S}
        v = MOI.get(model.optimizer, MOI.ConstraintFunction(), moi_ci)
        variable_name = MOI.get(model.optimizer, MOI.VariableName(), v)
        return "ParametricBound_$(S)_$(variable_name)"
    else
        return MOI.get(model.optimizer, attr, moi_ci)
    end
end

function MOI.get(model::Optimizer, ::MOI.NumberOfVariables)
    return length(model.parameters) + length(model.variables)
end

function MOI.supports(
    model::Optimizer,
    attr::MOI.ConstraintName,
    tp::Type{<:MOI.ConstraintIndex},
)
    return MOI.supports(model.optimizer, attr, tp)
end

function MOI.get(
    model::Optimizer,
    attr::MOI.ConstraintFunction,
    ci::MOI.ConstraintIndex{F,S},
) where {F,S}
    if haskey(model.original_constraint_function_and_set_cache, ci)
        return model.original_constraint_function_and_set_cache[ci][1]
    else
        return MOI.get(model.optimizer, attr, ci)
    end
end

function MOI.get(
    model::Optimizer,
    tp::Type{MOI.ConstraintIndex{F,S}},
    attr::String,
) where {F,S}
    return MOI.get(model.optimizer, tp, attr)
end

function MOI.get(model::Optimizer, tp::Type{MOI.ConstraintIndex}, attr::String)
    return MOI.get(model.optimizer, tp, attr)
end

function MOI.is_valid(model::Optimizer, vi::MOI.VariableIndex)
    return MOI.is_valid(model.optimizer, vi)
end

function MOI.supports(model::Optimizer, ::MOI.NumberOfThreads)
    return MOI.supports(model.optimizer, MOI.NumberOfThreads())
end

function MOI.supports(model::Optimizer, ::MOI.TimeLimitSec)
    return MOI.supports(model.optimizer, MOI.TimeLimitSec())
end

function MOI.set(model::Optimizer, ::MOI.TimeLimitSec, value)
    MOI.set(model.optimizer, MOI.TimeLimitSec(), value)
    return
end

function MOI.get(model::Optimizer, ::MOI.TimeLimitSec)
    return MOI.get(model.optimizer, MOI.TimeLimitSec())
end

function MOI.get(model::Optimizer, ::MOI.SolveTimeSec)
    return MOI.get(model.optimizer, MOI.SolveTimeSec())
end

function MOI.supports(model::Optimizer, ::MOI.Silent)
    return MOI.supports(model.optimizer, MOI.Silent())
end

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

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

function MOI.get(model::Optimizer, ::MOI.RawStatusString)
    return MOI.get(model.optimizer, MOI.RawStatusString())
end

function MOI.get(model::Optimizer, ::MOI.NumberOfConstraints{F,S}) where {F,S}
    return length(MOI.get(model, MOI.ListOfConstraintIndices{F,S}()))
end

function MOI.get(
    model::Optimizer,
    attr::MOI.ConstraintSet,
    ci::MOI.ConstraintIndex{F,S},
) where {F,S}
    if haskey(model.original_constraint_function_and_set_cache, ci)
        return model.original_constraint_function_and_set_cache[ci][2]
    else
        MOI.throw_if_not_valid(model, ci)
        return MOI.get(model.optimizer, attr, ci)
    end
end

function MOI.get(model::Optimizer, attr::MOI.ObjectiveSense)
    return MOI.get(model.optimizer, attr)
end

function MOI.get(model::Optimizer, attr::MOI.ObjectiveFunctionType)
    return typeof(model.original_objective_function)
end

function MOI.get(
    model::Optimizer,
    attr::MOI.ObjectiveFunction{F},
) where {
    F<:Union{
        MOI.VariableIndex,
        MOI.ScalarAffineFunction{T},
        MOI.ScalarQuadraticFunction{T},
    },
} where {T}
    if !function_has_parameters(model, model.original_objective_function)
        return MOI.get(model.optimizer, attr)
    else
        if F === typeof(model.original_objective_function)
            return model.original_objective_function
        else
            throw(InexactError)
        end
    end
end

function MOI.get(model::Optimizer, attr::MOI.ResultCount)
    return MOI.get(model.optimizer, attr)
end

# In the AbstractBridgeOptimizer, we collect all the possible constraint types and them filter with NumberOfConstraints.
# If NumberOfConstraints is zero then we remove it from the list.
# Here, you can look over keys(quadratic_added_cache) and add the F-S types of all the keys in constraints.
# To implement NumberOfConstraints, you call NumberOfConstraints to the inner optimizer.
# Then you remove the number of constraints of that that in values(quadratic_added_cache)
function MOI.get(model::Optimizer, ::MOI.ListOfConstraintTypesPresent)
    inner_ctrs = MOI.get(model.optimizer, MOI.ListOfConstraintTypesPresent())
    if !has_quadratic_constraint_caches(model)
        return inner_ctrs
    end

    cache_keys = collect(keys(model.quadratic_added_cache))
    constraints = Set{Tuple{DataType,DataType}}()

    for (F, S) in inner_ctrs
        inner_index =
            MOI.get(model.optimizer, MOI.ListOfConstraintIndices{F,S}())
        cache_map_check =
            quadratic_constraint_cache_map_check.(model, inner_index)
        for type in typeof.(cache_keys[cache_map_check])
            push!(constraints, (type.parameters[1], type.parameters[2]))
        end
        # If not all the constraints are chached then also push the original type
        if !all(cache_map_check)
            push!(constraints, (F, S))
        end
    end

    return collect(constraints)
end

function MOI.get(
    model::Optimizer,
    attr::MOI.ListOfConstraintIndices{F,S},
) where {S,F<:Union{MOI.VectorOfVariables,MOI.VariableIndex}}
    return MOI.get(model.optimizer, attr)
end

function MOI.get(
    model::Optimizer,
    ::MOI.ListOfConstraintIndices{F,S},
) where {
    S<:MOI.AbstractSet,
    F<:Union{MOI.ScalarAffineFunction{T},MOI.VectorAffineFunction{T}},
} where {T}
    inner_index = MOI.get(model.optimizer, MOI.ListOfConstraintIndices{F,S}())
    if !has_quadratic_constraint_caches(model)
        return inner_index
    end

    cache_map_check = quadratic_constraint_cache_map_check(mode, inner_index)
    return inner_index[cache_map_check]
end

function MOI.get(
    model::Optimizer,
    ::MOI.ListOfConstraintIndices{F,S},
) where {S<:MOI.AbstractSet,F<:MOI.ScalarQuadraticFunction{T}} where {T}
    inner_index = MOI.ConstraintIndex{F,S}[]
    if MOI.supports_constraint(model.optimizer, F, S)
        inner_index =
            MOI.get(model.optimizer, MOI.ListOfConstraintIndices{F,S}())
        if !has_quadratic_constraint_caches(model)
            return inner_index
        end
    end

    quadratic_caches = [
        :quadratic_constraint_cache_pc,
        :quadratic_constraint_cache_pp,
        :quadratic_constraint_cache_pv,
        # JD: Check if this applies here
        # :quadratic_constraint_variables_associated_to_parameters_cache
    ]

    for field in quadratic_caches
        cache = getfield(model, field)
        push!(inner_index, keys(cache)...)
    end
    return inner_index
end

function MOI.supports_add_constrained_variable(::Optimizer, ::Type{Parameter})
    return true
end

function MOI.supports_add_constrained_variables(
    model::Optimizer,
    ::Type{MOI.Reals},
)
    return MOI.supports_add_constrained_variables(model.optimizer, MOI.Reals)
end

function MOI.add_variable(model::Optimizer)
    next_variable_index!(model)
    return MOI.Utilities.CleverDicts.add_item(
        model.variables,
        MOI.add_variable(model.optimizer),
    )
end

function MOI.add_constrained_variable(model::Optimizer, set::Parameter)
    next_parameter_index!(model)
    p = MOI.VariableIndex(model.last_parameter_index_added)
    MOI.Utilities.CleverDicts.add_item(model.parameters, set.val)
    cp = MOI.ConstraintIndex{MOI.VariableIndex,Parameter}(
        model.last_parameter_index_added,
    )
    MOI.Utilities.CleverDicts.add_item(model.updated_parameters, NaN)
    update_number_of_parameters!(model)
    return p, cp
end

function MOI.add_constraint(
    model::Optimizer,
    f::MOI.VariableIndex,
    set::MOI.AbstractScalarSet,
)
    if is_parameter_in_model(model, f)
        error("Cannot constrain a parameter")
    elseif !is_variable_in_model(model, f)
        error("Variable not in the model")
    end
    return MOI.add_constraint(model.optimizer, f, set)
end

function add_constraint_with_parameters_on_function(
    model::Optimizer,
    f::MOI.ScalarAffineFunction{T},
    set::S,
) where {T,S}
    vars, params, param_constant =
        separate_possible_terms_and_calculate_parameter_constant(model, f.terms)
    model.last_affine_added += 1
    if model.constraints_interpretation == ONLY_BOUNDS
        if (length(vars) == 1) && isone(MOI.coefficient(vars[1]))
            poi_ci =
                add_vi_constraint(model, vars, params, param_constant, f, set)
        else
            error(
                "It was not possible to interpret this constraint as a variable bound.",
            )
        end
    elseif model.constraints_interpretation == ONLY_CONSTRAINTS
        poi_ci = add_saf_constraint(model, vars, params, param_constant, f, set)
    elseif model.constraints_interpretation == BOUNDS_AND_CONSTRAINTS
        if (length(vars) == 1) && isone(MOI.coefficient(vars[1]))
            poi_ci =
                add_vi_constraint(model, vars, params, param_constant, f, set)
        else
            poi_ci =
                add_saf_constraint(model, vars, params, param_constant, f, set)
        end
    end
    model.original_constraint_function_and_set_cache[poi_ci] = (f, set)
    return poi_ci
end

function add_saf_constraint(
    model::Optimizer,
    vars::Vector{MOI.ScalarAffineTerm{T}},
    params::Vector{MOI.ScalarAffineTerm{T}},
    param_constant::T,
    f::MOI.ScalarAffineFunction{T},
    set::S,
) where {T,S}
    moi_ci = MOI.Utilities.normalize_and_add_constraint(
        model.optimizer,
        MOI.ScalarAffineFunction(vars, f.constant + param_constant),
        set,
    )
    poi_ci = create_new_poi_ci_and_save_affine_caches(model, params, moi_ci)
    return poi_ci
end

function add_vi_constraint(
    model::Optimizer,
    vars::Vector{MOI.ScalarAffineTerm{T}},
    params::Vector{MOI.ScalarAffineTerm{T}},
    param_constant::T,
    f::MOI.ScalarAffineFunction{T},
    set::S,
) where {T,S}
    moi_ci = MOI.Utilities.normalize_and_add_constraint(
        model.optimizer,
        MOI.VariableIndex(vars[1].variable.value),
        update_constant!(set, f.constant + param_constant),
    )
    poi_ci = create_new_poi_ci_and_save_affine_caches(model, params, moi_ci)
    return poi_ci
end

function create_new_poi_ci_and_save_affine_caches(
    model::Optimizer,
    params::Vector{MOI.ScalarAffineTerm{T}},
    moi_ci::MOI.ConstraintIndex{F,S},
) where {T,F,S}
    poi_ci = MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}(
        model.last_affine_added,
    )
    model.affine_constraint_cache[poi_ci] = params
    model.affine_added_cache[poi_ci] = moi_ci
    return poi_ci
end

function MOI.add_constraint(
    model::Optimizer,
    f::MOI.ScalarAffineFunction{T},
    set::MOI.AbstractScalarSet,
) where {T}
    if !function_has_parameters(model, f)
        return MOI.add_constraint(model.optimizer, f, set)
    else
        return add_constraint_with_parameters_on_function(model, f, set)
    end
end

function MOI.get(
    model::Optimizer,
    attr::MOI.VariablePrimal,
    v::MOI.VariableIndex,
)
    if is_parameter_in_model(model, v)
        return model.parameters[p_idx(v)]
    elseif is_variable_in_model(model, v)
        return MOI.get(model.optimizer, attr, model.variables[v])
    else
        error("Variable not in the model")
    end
end

function MOI.get(
    model::Optimizer,
    attr::MOI.VariableBasisStatus,
    v::MOI.VariableIndex,
)
    if is_variable_in_model(model, v)
        return MOI.get(model.optimizer, attr, model.variables[v])
    else
        error("VariableBasisStatus is not supported for parameters.")
    end
end

function MOI.set(
    model::Optimizer,
    ::MOI.ConstraintSet,
    cp::MOI.ConstraintIndex{MOI.VariableIndex,Parameter},
    set::Parameter,
)
    p = MOI.VariableIndex(cp.value)
    if !is_parameter_in_model(model, p)
        error("Parameter not in the model")
    end
    return model.updated_parameters[p_idx(p)] = set.val
end

"""
    ParameterValue <: MOI.AbstractVariableAttribute

Attribute defined to set and get parameter values

# Example

```julia
MOI.set(model, POI.ParameterValue(), p, 2.0)
MOI.get(model, POI.ParameterValue(), p)
```
"""
struct ParameterValue <: MOI.AbstractVariableAttribute end

# We need a CachingOptimizer fallback to
# get ParameterValue working correctly on JuMP
# TODO: Think of a better solution for this

function MOI.set(
    opt::MOI.Utilities.CachingOptimizer,
    ::ParameterValue,
    var::MOI.VariableIndex,
    val::Float64,
)
    ci = MOI.ConstraintIndex{MOI.VariableIndex,Parameter}(var.value)
    set = MOI.set(opt, MOI.ConstraintSet(), ci, Parameter(val))
    return nothing
end

function MOI.set(
    model::Optimizer,
    ::ParameterValue,
    var::MOI.VariableIndex,
    val::Float64,
)
    ci = MOI.ConstraintIndex{MOI.VariableIndex,Parameter}(var.value)
    set = MOI.set(model, MOI.ConstraintSet(), ci, Parameter(val))
    return nothing
end

function MOI.set(
    opt::MOI.Utilities.CachingOptimizer,
    ::ParameterValue,
    vi::MOI.VariableIndex,
    val::Real,
)
    return MOI.set(opt, ParameterValue(), vi, convert(Float64, val))
end

function MOI.set(
    model::Optimizer,
    ::ParameterValue,
    vi::MOI.VariableIndex,
    val::Real,
)
    return MOI.set(model, ParameterValue(), vi, convert(Float64, val))
end

function MOI.get(
    opt::MOI.Utilities.CachingOptimizer,
    ::ParameterValue,
    var::MOI.VariableIndex,
)
    ci = MOI.ConstraintIndex{MOI.VariableIndex,Parameter}(var.value)
    set = MOI.get(opt, MOI.ConstraintSet(), ci)
    return set.val
end

function MOI.get(model::Optimizer, ::ParameterValue, var::MOI.VariableIndex)
    ci = MOI.ConstraintIndex{MOI.VariableIndex,Parameter}(var.value)
    set = MOI.get(model, MOI.ConstraintSet(), ci)
    return set.val
end

"""
    ConstraintsInterpretation <: MOI.AbstractOptimizerAttribute

Attribute to define how [`POI.Optimizer`](@ref) should interpret constraints.

- `POI.ONLY_CONSTRAINTS`: Only interpret `ScalarAffineFunction` constraints as linear constraints
  If an expression such as `x >= p1 + p2` appears it will be trated like a new constraint.
  **This is the default behaviour of [`POI.Optimizer`](@ref)**

- `POI.ONLY_BOUNDS`: Only interpret `ScalarAffineFunction` constraints as a variable bound.
  This is valid for constraints such as `x >= p` or `x >= p1 + p2`. If a constraint `x1 + x2 >= p` appears,
  which is not a valid variable bound it will throw an error.

- `POI.BOUNDS_AND_CONSTRAINTS`: Interpret `ScalarAffineFunction` constraints as a variable bound if they
  are a valid variable bound, i.e., `x >= p` or `x >= p1 + p2` and interpret them as linear constraints
  otherwise.

# Example

```julia
MOI.set(model, POI.InterpretConstraintsAsBounds(), POI.ONLY_BOUNDS)
MOI.set(model, POI.InterpretConstraintsAsBounds(), POI.ONLY_CONSTRAINTS)
MOI.set(model, POI.InterpretConstraintsAsBounds(), POI.BOUNDS_AND_CONSTRAINTS)
```
"""
struct ConstraintsInterpretation <: MOI.AbstractOptimizerAttribute end

function MOI.set(
    model::Optimizer,
    ::ConstraintsInterpretation,
    value::ConstraintsInterpretationCode,
)
    return model.constraints_interpretation = value
end

function empty_objective_function_caches!(model::Optimizer)
    empty!(model.affine_objective_cache)
    empty!(model.quadratic_objective_cache_pv)
    empty!(model.quadratic_objective_cache_pp)
    empty!(model.quadratic_objective_cache_pc)
    return
end

function MOI.set(
    model::Optimizer,
    attr::MOI.ObjectiveFunction,
    f::MOI.ScalarAffineFunction{T},
) where {T}
    # clear previously defined objetive function cache
    empty_objective_function_caches!(model)
    if !function_has_parameters(model, f)
        MOI.set(model.optimizer, attr, f)
    else
        vars, params, param_constant =
            separate_possible_terms_and_calculate_parameter_constant(
                model,
                f.terms,
            )
        MOI.set(
            model.optimizer,
            attr,
            MOI.ScalarAffineFunction(vars, f.constant + param_constant),
        )
        model.affine_objective_cache = params
    end
    model.original_objective_function = f
    return
end

function MOI.set(
    model::Optimizer,
    attr::MOI.ObjectiveFunction,
    v::MOI.VariableIndex,
)
    if is_parameter_in_model(model, v)
        error("Cannot use a parameter as objective function alone")
    elseif !is_variable_in_model(model, v)
        error("Variable not in the model")
    end
    model.original_objective_function = v
    return MOI.set(model.optimizer, attr, model.variables[v])
end

function MOI.set(
    model::Optimizer,
    attr::MOI.ObjectiveSense,
    sense::MOI.OptimizationSense,
)
    MOI.set(model.optimizer, attr, sense)
    return
end

function MOI.get(
    model::Optimizer,
    attr::T,
) where {
    T<:Union{
        MOI.TerminationStatus,
        MOI.ObjectiveValue,
        MOI.DualObjectiveValue,
        MOI.PrimalStatus,
        MOI.DualStatus,
    },
}
    return MOI.get(model.optimizer, attr)
end

function MOI.get(
    model::Optimizer,
    attr::T,
    c::MOI.ConstraintIndex,
) where {
    T<:Union{MOI.ConstraintPrimal,MOI.ConstraintDual,MOI.ConstraintBasisStatus},
}
    return MOI.get(model.optimizer, attr, c)
end

function MOI.get(
    model::Optimizer,
    attr::AT,
    c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S},
) where {
    AT<:Union{
        MOI.ConstraintPrimal,
        MOI.ConstraintDual,
        MOI.ConstraintBasisStatus,
    },
    T,
    S<:MOI.AbstractScalarSet,
}
    moi_ci = get(model.affine_added_cache, c, c)
    return MOI.get(model.optimizer, attr, moi_ci)
end

function MOI.set(model::Optimizer, attr::MOI.RawOptimizerAttribute, val::Any)
    MOI.set(model.optimizer, attr, val)
    return
end

function MOI.get(model::Optimizer, ::MOI.SolverName)
    name = MOI.get(model.optimizer, MOI.SolverName())
    return "Parametric Optimizer with $(name) attached"
end

function MOI.get(model::Optimizer, ::MOI.SolverVersion)
    return MOI.get(model.optimizer, MOI.SolverVersion())
end

function MOI.add_constraint(
    model::Optimizer,
    f::MOI.VectorOfVariables,
    set::MOI.AbstractVectorSet,
) where {T}
    if function_has_parameters(model, f)
        error("VectorOfVariables does not allow parameters")
    end
    return MOI.add_constraint(model.optimizer, f, set)
end

function MOI.add_constraint(
    model::Optimizer,
    f::MOI.VectorAffineFunction{T},
    set::MOI.AbstractVectorSet,
) where {T}
    if !function_has_parameters(model, f)
        return MOI.add_constraint(model.optimizer, f, set)
    else
        return add_constraint_with_parameters_on_function(model, f, set)
    end
end

function add_constraint_with_parameters_on_function(
    model::Optimizer,
    f::MOI.VectorAffineFunction{T},
    set::MOI.AbstractVectorSet,
) where {T}
    vars, params, param_constants =
        separate_possible_terms_and_calculate_parameter_constant(model, f, set)
    ci = MOI.add_constraint(
        model.optimizer,
        MOI.VectorAffineFunction(vars, f.constants + param_constants),
        set,
    )
    model.vector_constraint_cache[ci] = params
    model.original_constraint_function_and_set_cache[ci] = (f, set)
    return ci
end

function add_constraint_with_parameters_on_function(
    model::Optimizer,
    f::MOI.ScalarQuadraticFunction{T},
    set::S,
) where {T,S<:MOI.AbstractScalarSet}
    (
        quad_vars,
        quad_aff_vars,
        quad_params,
        aff_terms,
        variables_associated_to_parameters,
        quad_param_constant,
    ) = separate_possible_terms_and_calculate_parameter_constant(
        model,
        f.quadratic_terms,
    )

    (
        aff_vars,
        aff_params,
        terms_with_variables_associated_to_parameters,
        aff_param_constant,
    ) = separate_possible_terms_and_calculate_parameter_constant(
        model,
        f.affine_terms,
        variables_associated_to_parameters,
    )

    aff_terms = vcat(aff_terms, aff_vars)
    const_term = f.constant + aff_param_constant + quad_param_constant
    quad_terms = quad_vars
    f_quad = if !isempty(quad_vars)
        MOI.ScalarQuadraticFunction(quad_terms, aff_terms, const_term)
    else
        MOI.ScalarAffineFunction(aff_terms, const_term)
    end
    model.last_quad_add_added += 1
    ci =
        MOI.Utilities.normalize_and_add_constraint(model.optimizer, f_quad, set)
    # This part is used to remember that ci came from a quadratic function
    # It is particularly useful because sometimes the constraint mutates
    new_ci = MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{T},S}(
        model.last_quad_add_added,
    )
    model.quadratic_added_cache[new_ci] = ci
    model.original_constraint_function_and_set_cache[new_ci] = (f, set)
    fill_quadratic_constraint_caches!(
        model,
        new_ci,
        quad_aff_vars,
        quad_params,
        aff_params,
        terms_with_variables_associated_to_parameters,
    )
    return new_ci
end

function MOI.add_constraint(
    model::Optimizer,
    f::MOI.ScalarQuadraticFunction{T},
    set::MOI.AbstractScalarSet,
) where {T}
    if !function_has_parameters(model, f)
        return MOI.add_constraint(model.optimizer, f, set)
    else
        return add_constraint_with_parameters_on_function(model, f, set)
    end
end

function MOI.delete(model::Optimizer, v::MOI.VariableIndex)
    delete!(model.variables, v)
    MOI.delete(model.optimizer, v)
    return
end

function MOI.delete(
    model::Optimizer,
    c::MOI.ConstraintIndex{F,S},
) where {F<:MOI.ScalarAffineFunction,S<:MOI.AbstractSet}
    if haskey(model.affine_constraint_cache, c)
        delete!(model.affine_constraint_cache, c)
    end
    MOI.delete(model.optimizer, c)
    return
end

function MOI.delete(
    model::Optimizer,
    c::MOI.ConstraintIndex{F,S},
) where {
    F<:Union{MOI.VariableIndex,MOI.VectorOfVariables,MOI.VectorAffineFunction},
    S<:MOI.AbstractSet,
}
    MOI.delete(model.optimizer, c)
    return
end

function MOI.is_valid(
    model::Optimizer,
    c::MOI.ConstraintIndex{F,S},
) where {
    F<:Union{MOI.VariableIndex,MOI.VectorOfVariables,MOI.VectorAffineFunction},
    S<:MOI.AbstractSet,
}
    return MOI.is_valid(model.optimizer, c)
end

function MOI.is_valid(
    model::Optimizer,
    c::MOI.ConstraintIndex{F,S},
) where {F<:MOI.ScalarAffineFunction,S<:MOI.AbstractSet}
    return MOI.is_valid(model.optimizer, c)
end

function MOI.set(
    model::Optimizer,
    attr::MOI.ObjectiveFunction,
    f::MOI.ScalarQuadraticFunction{T},
) where {T}
    # clear previously defined objetive function cache
    empty_objective_function_caches!(model)
    if !function_has_parameters(model, f)
        MOI.set(model.optimizer, attr, f)
        return
    end
    (
        quad_vars,
        quad_aff_vars,
        quad_params,
        aff_terms,
        variables_associated_to_parameters,
        quad_param_constant,
    ) = separate_possible_terms_and_calculate_parameter_constant(
        model,
        f.quadratic_terms,
    )

    (
        aff_vars,
        aff_params,
        terms_with_variables_associated_to_parameters,
        aff_param_constant,
    ) = separate_possible_terms_and_calculate_parameter_constant(
        model,
        f.affine_terms,
        variables_associated_to_parameters,
    )

    aff_terms = vcat(aff_terms, aff_vars)
    const_term = f.constant + aff_param_constant + quad_param_constant

    if !isempty(quad_vars)
        MOI.set(
            model.optimizer,
            attr,
            MOI.ScalarQuadraticFunction(quad_vars, aff_terms, const_term),
        )
    else
        MOI.set(
            model.optimizer,
            MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}(),
            MOI.ScalarAffineFunction(aff_terms, const_term),
        )
    end

    model.original_objective_function = f
    model.quadratic_objective_cache_pv = quad_aff_vars
    model.quadratic_objective_cache_pp = quad_params
    model.quadratic_objective_cache_pc = aff_params
    model.quadratic_objective_variables_associated_to_parameters_cache =
        terms_with_variables_associated_to_parameters

    return
end

function _poi_default_copy_to(dest::T, src::MOI.ModelLike) where {T}
    if !MOI.supports_incremental_interface(dest)
        error("Model $(typeof(dest)) does not support copy_to.")
    end
    MOI.empty!(dest)
    vis_src = MOI.get(src, MOI.ListOfVariableIndices())
    index_map = MOI.IndexMap()
    # The `NLPBlock` assumes that the order of variables does not change (#849)
    # Therefore, all VariableIndex and VectorOfVariable constraints are added
    # seprately, and no variables constrained-on-creation are added.

    # This is not valid for NLPs with Parameters, they should enter
    has_nlp = MOI.NLPBlock() in MOI.get(src, MOI.ListOfModelAttributesSet())
    constraints_not_added = if has_nlp
        vcat(
            Any[
                MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) for
                (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) if
                MOI.Utilities._is_variable_function(F) && S != Parameter
            ],
            Any[MOI.Utilities._try_constrain_variables_on_creation(
                dest,
                src,
                index_map,
                Parameter,
            )],
        )
    else
        Any[
            MOI.Utilities._try_constrain_variables_on_creation(
                dest,
                src,
                index_map,
                S,
            ) for S in MOI.Utilities.sorted_variable_sets_by_cost(dest, src)
        ]
    end
    MOI.Utilities._copy_free_variables(dest, index_map, vis_src)
    # Copy variable attributes
    MOI.Utilities.pass_attributes(dest, src, index_map, vis_src)
    # Copy model attributes
    MOI.Utilities.pass_attributes(dest, src, index_map)
    # Copy constraints
    MOI.Utilities._pass_constraints(dest, src, index_map, constraints_not_added)
    MOI.Utilities.final_touch(dest, index_map)
    return index_map
end

function MOI.Utilities.default_copy_to(
    dest::MOI.Bridges.LazyBridgeOptimizer{Optimizer{Float64,T}},
    src::MOI.ModelLike,
) where {T}
    return _poi_default_copy_to(dest, src)
end

function MOI.Utilities.default_copy_to(
    dest::Optimizer{Float64,T},
    src::MOI.ModelLike,
) where {T}
    return _poi_default_copy_to(dest, src)
end

function MOI.optimize!(model::Optimizer)
    if !isempty(model.updated_parameters)
        update_parameters!(model)
    end
    MOI.optimize!(model.optimizer)
    if model.evaluate_duals
        calculate_dual_of_parameters(model)
    end
    return
end

end # module


###
### This file contains the definition of `offdiagonals`, as well as some internal functions
### operating on Bosonic addresses.
###
"""
    AbstractOffdiagonals{A,T}<:AbstractVector{Tuple{A,T}}

Iterator over new address and matrix element for reachable off-diagonal matrix elements of a
linear operator.

See [`Offdiagonals`](@ref) for a default implementation.

# Methods to define

* [`offdiagonals(h, a)::AbstractOffdiagonals`](@ref): This function is used to construct the
  correct type of offdiagonals for a given combination of hamiltonian `h` and fock address
  `a`.
* `Base.getindex(::AbstractOffdiagonals, i)`: should be equivalent to
  `get_offdiagonal(h, a, i)`.
* `Base.size(::AbstractOffdiagonals)`: should be equivalent to `num_offdiagonals(h, a)`.

"""
abstract type AbstractOffdiagonals{A,T} <: AbstractVector{Tuple{A,T}} end

Base.IndexStyle(::Type{<:AbstractOffdiagonals}) = IndexLinear()

offdiagonals(h, a) = Offdiagonals(h, a)

"""
    Offdiagonals(h, address)

Iterator over new address and matrix element for reachable off-diagonal matrix elements of
linear operator `h` from address `address`.  Represents an abstract vector containing the
possibly non-zero off-diagonal matrix elements of the column of ham indexed by add.

This is the default implementation defined in terms of [`num_offdiagonals`](@ref) and
[`get_offdiagonal`](@ref).

# See also

* [`offdiagonals`](@ref)

"""
struct Offdiagonals{A,T,H<:AbstractHamiltonian{T}} <: AbstractOffdiagonals{A,T}
    hamiltonian::H
    address::A
    length::Int
end

# default constructor
function Offdiagonals(h, a)
    return Offdiagonals(h, a, num_offdiagonals(h, a))
end

function Base.getindex(s::Offdiagonals{A,T}, i)::Tuple{A,T} where {A,T}
    @boundscheck 1 ≤ i ≤ s.length || throw(BoundsError(s, i))
    new_address, matrix_element = get_offdiagonal(s.hamiltonian, s.address, i)
    return (new_address, matrix_element)
end

Base.size(s::Offdiagonals) = (s.length,)

###
### Internal functions common to several different bosonic Hamiltonians
###
"""
    new_address, product = hopnextneighbour(add, chosen)

Compute the new address of a hopping event for the Bose-Hubbard model. Returns the new
address and the product of occupation numbers of the involved modes.

The off-diagonals are indexed as follows:

* `(chosen + 1) ÷ 2` selects the hopping site.
* Even `chosen` indicates a hop to the left.
* Odd `chosen` indicates a hop to the right.
* Boundary conditions are periodic.

# Example

```jldoctest
julia> using Rimu.Hamiltonians: hopnextneighbour

julia> hopnextneighbour(BoseFS((1, 0, 1)), 3)
(BoseFS{2,3}((2, 0, 0)), 2)
julia> hopnextneighbour(BoseFS((1, 0, 1)), 4)
(BoseFS{2,3}((1, 1, 0)), 1)
```
"""
function hopnextneighbour(b::BoseFS{N,M,A}, chosen) where {N,M,A}
    address = b.bs
    T = chunk_type(address)
    site = (chosen + 1) >>> 0x1
    if isodd(chosen) # Hopping to the right
        next = 0
        curr = 0
        offset = 0
        sc = 0
        reached_end = false
        for (i, (num, sn, bit)) in enumerate(occupied_modes(b))
            next = num * (sn == sc + 1) # only set next to > 0 if sites are neighbours
            reached_end = i == site + 1
            reached_end && break
            curr = num
            offset = bit + num
            sc = sn
        end
        if sc == M
            new_address = (address << 0x1) | A(T(1))
            prod = curr * (trailing_ones(address) + 1) # mul occupation num of first obital
        else
            next *= reached_end
            new_address = address ⊻ A(T(3)) << ((offset - 1) % T)
            prod = curr * (next + 1)
        end
    else # Hopping to the left
        if site == 1 && isodd(address)
            # For leftmost site, we shift the whole address circularly by one bit.
            new_address = (address >>> 0x1) | A(T(1)) << ((N + M - 2) % T)
            prod = trailing_ones(address) * leading_ones(new_address)
        else
            prev = 0
            curr = 0
            offset = 0
            sp = 0
            for (i, (num, sc, bit)) in enumerate(occupied_modes(b))
                prev = curr * (sc == sp + 1) # only set prev to > 0 if sites are neighbours
                curr = num
                offset = bit
                i == site && break
                sp = sc
            end
            new_address = address ⊻ A(T(3)) << ((offset - 1) % T)
            prod = curr * (prev + 1)
        end
    end
    return BoseFS{N,M,A}(new_address), prod
end


using MathOptInterface, CPLEX, Test

const MOI  = MathOptInterface
const MOIT = MOI.Test
const MOIB = MOI.Bridges

const SOLVER = CPLEX.Optimizer(CPX_PARAM_SCRIND = 0)
const CONFIG = MOIT.TestConfig()

@testset "Unit Tests" begin
    MOIT.unittest(SOLVER, CONFIG, [
        "solve_affine_interval",  # not implemented
        "solve_qp_edge_cases",    # not implemented
        "solve_qcp_edge_cases",   # not implemented
        "solve_objbound_edge_cases"
    ])
    @testset "solve_affine_interval" begin
        MOIT.solve_affine_interval(MOIB.SplitInterval{Float64}(SOLVER), CONFIG)
    end
    MOIT.modificationtest(SOLVER, CONFIG, [
        "solve_func_scalaraffine_lessthan"
    ])
end

@testset "Linear tests" begin
    @testset "Default Solver"  begin
        MOIT.contlineartest(SOLVER, CONFIG, [
            "linear10",  # Requires interval
            # Requires infeasiblity certificates
            "linear8a", "linear8b", "linear8c", "linear11", "linear12",
            # VariablePrimalStart not implemented.
            "partial_start"
        ])
    end
    @testset "linear10" begin
        MOIT.linear10test(MOIB.SplitInterval{Float64}(SOLVER), CONFIG)
    end
    @testset "No certificate" begin
        MOIT.linear12test(
            SOLVER,
            MOIT.TestConfig(infeas_certificates=false)
        )
    end
end
@testset "Integer Linear tests" begin
    MOIT.intlineartest(SOLVER, CONFIG, [
        "int3"  # Requires Interval
    ])
    @testset "int3" begin
        MOIT.int3test(MOIB.SplitInterval{Float64}(SOLVER), CONFIG)
    end
end

@testset "ModelLike tests" begin
    @test MOI.get(SOLVER, MOI.SolverName()) == "CPLEX"
    @testset "default_objective_test" begin
         MOIT.default_objective_test(SOLVER)
     end
     @testset "default_status_test" begin
         MOIT.default_status_test(SOLVER)
     end
    @testset "nametest" begin
        MOIT.nametest(SOLVER)
    end
    @testset "validtest" begin
        MOIT.validtest(SOLVER)
    end
    @testset "emptytest" begin
        MOIT.emptytest(SOLVER)
    end
    @testset "orderedindicestest" begin
        MOIT.orderedindicestest(SOLVER)
    end
    @testset "copytest" begin
        MOIT.copytest(SOLVER, CPLEX.Optimizer())
    end
end

@testset "Env" begin
    @testset "User-provided" begin
        env = CPLEX.Env()
        model_1 = CPLEX.Optimizer(env)
        @test model_1.inner.env === env
        model_2 = CPLEX.Optimizer(env)
        @test model_2.inner.env === env
        # Check that finalizer doesn't touch env when manually provided.
        finalize(model_1.inner)
        @test CPLEX.is_valid(env)
    end
    @testset "Automatic" begin
        model_1 = CPLEX.Optimizer()
        model_2 = CPLEX.Optimizer()
        @test model_1.inner.env !== model_2.inner.env
    end
    @testset "Env when emptied" begin
        @testset "User-provided" begin
            env = CPLEX.Env()
            model = CPLEX.Optimizer(env)
            @test model.inner.env === env
            @test CPLEX.is_valid(env)
            MOI.empty!(model)
            @test model.inner.env === env
            @test CPLEX.is_valid(env)
        end
        @testset "Automatic" begin
            model = CPLEX.Optimizer()
            env = model.inner.env
            MOI.empty!(model)
            @test model.inner.env !== env
            @test CPLEX.is_valid(model.inner.env)
        end
    end
end


export computeHinvTRec

function computeHinvTRec(pMis::Array{RemoteChannel})
HinvTRec = Array{Array{ComplexF64}}(undef,length(pMis))
@sync begin
	for k=1:length(pMis)
		@async begin
			HinvTRec[k] = remotecall_fetch(computeHinvTRec,pMis[k].where,pMis[k]);
		end
	end
end
return HinvTRec;
end

function calculateShiftedH(pMis, m)
	pFor = pMis.pFor;
	M       	= pFor.Mesh
	omega   	= pFor.omega
	wavelet 	= pFor.WaveletCoef;
	gamma   	= pFor.gamma
	Q       	= pFor.Sources
	P       	= pFor.Receivers
	Ainv    	= pFor.ForwardSolver;
	batchSize 	= pFor.forwardSolveBatchSize;
	select  	= pFor.sourceSelection;
	An2cc = getNodalAverageMatrix(M);

	m, = pMis.modelfun(m)
	m = interpGlobalToLocal(m,pMis.gloc.PForInv,pMis.gloc.sigmaBackground);

	m = An2cc'*m;
	gamma = An2cc'*gamma;

	H = GetHelmholtzOperator(M,m,omega, gamma, true,useSommerfeldBC);
	Ainv.helmParam = HelmholtzParam(M,gamma,m,omega,true,useSommerfeldBC);
	H = H + GetHelmholtzShiftOP(m, omega,Ainv.shift[1]);
	H = sparse(H');

	return H;
end

function computeHinvTRec(pMisRF::RemoteChannel)
pMis = fetch(pMisRF)
HinvTP, = solveLinearSystem(spzeros(ComplexF64,0,0),complex(Matrix(pMis.pFor.Receivers)),pMis.pFor.ForwardSolver,1);
return HinvTP;
end

function computeHinvTRec(pMisRF::RemoteChannel, m)
pMis = fetch(pMisRF)
H = calculateShiftedH(pMis, m)
HinvTP, = solveLinearSystem(H,complex(Matrix(pMis.pFor.Receivers)),pMis.pFor.ForwardSolver,1);
return HinvTP;
end

function computeHinvTRec(pMisRF::RemoteChannel, x, m, doTranspose)
pMis = fetch(pMisRF)
if doTranspose == 0
	HinvTP, = solveLinearSystem(spzeros(ComplexF64,0,0),x,pMis.pFor.ForwardSolver,doTranspose);
	return complex(pMis.pFor.Receivers' * HinvTP);
else
	HinvTP, = solveLinearSystem(spzeros(ComplexF64,0,0),complex(pMis.pFor.Receivers * x),pMis.pFor.ForwardSolver,doTranspose);
	return  HinvTP;
end
end

function computeHinvTRecX(pMis::Array{RemoteChannel}, x, m, doTranspose=1)
HinvTRec = Array{Array{ComplexF64}}(undef,length(pMis))
@sync begin
		for k=1:length(pMis)
	@async begin
			HinvTRec[k] = remotecall_fetch(computeHinvTRec,pMis[k].where,pMis[k],x, m,doTranspose);
		end
	end
end
return HinvTRec;
end

function computeHinvTRecXarr(pMis::Array{RemoteChannel}, x, m, doTranspose=1)
HinvTRec = Array{Array{ComplexF64}}(undef,length(pMis))
@sync begin
	for k=1:length(pMis)
	@async begin
			HinvTRec[k] = remotecall_fetch(computeHinvTRec,pMis[k].where,pMis[k],x[k], m,doTranspose);
		end
	end
end
return HinvTRec;
end


module MCLightTransport

    using LinearAlgebra: norm
    using StaticArrays

    export Ray, PointSource, makephoton
    export Photon
    export CatesianVolume, RadialVolume
    export simulate!

    include("absorbers.jl")
    include("emitters.jl")
    include("photon.jl")
    include("simulate.jl")

end # module


B_data = [ -2.5   0.5   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   3.0          -2.0         1.0           0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0          0.0           0.0           0.0          0.0           0.0    0.0   0.0    0.0   0.0
1.0  -3.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   3.0          -2.0         1.0           0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0          0.0           0.0           0.0          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0  -2.5   0.0  -0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   2.0           0.0         0.0           0.0   0.0   0.0   0.0   0.0   0.0   0.5           0.0          0.0           0.0           0.0          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0  -0.0  -2.5   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           2.0         0.0           0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.5          0.0           0.0           0.0          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0  -0.0  -2.5   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         2.0           0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0          0.5           0.0           0.0          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0  -2.5   0.0  -0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         0.0           2.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0          0.0           0.5           0.0          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0  -0.0  -2.5   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         0.0           0.0   2.0   0.0   0.0   0.0   0.0   0.0           0.0          0.0           0.0           0.5          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0  -0.0  -2.5   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         0.0           0.0   0.0   2.0   0.0   0.0   0.0   0.0           0.0          0.0           0.0           0.0          0.5           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0  -2.5   0.0  -0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         0.0           0.0   0.0   0.0   2.0   0.0   0.0   0.0           0.0          0.0           0.0           0.0          0.0           0.5    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0  -0.0  -2.5   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         0.0           0.0   0.0   0.0   0.0   2.0   0.0   0.0           0.0          0.0           0.0           0.0          0.0           0.0    0.5   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0  -0.0  -2.5   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         0.0           0.0   0.0   0.0   0.0   0.0   2.0   0.0           0.0          0.0           0.0           0.0          0.0           0.0    0.0   0.5    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0  -2.5   0.0  -0.0   0.0   0.0   0.0   0.0   0.0   0.0   2.0           0.0         0.0           0.0   0.0   0.0   0.0   0.0   0.0   0.5           0.0          0.0           0.0           0.0          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0  -0.0  -2.5   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           2.0         0.0           0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.5          0.0           0.0           0.0          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0  -0.0  -2.5   0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         2.0           0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0          0.5           0.0           0.0          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0  -2.5   0.0  -0.0   0.0   0.0   0.0   0.0           0.0         0.0           2.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0          0.0           0.5           0.0          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0  -0.0  -2.5   0.0   0.0   0.0   0.0   0.0           0.0         0.0           0.0   2.0   0.0   0.0   0.0   0.0   0.0           0.0          0.0           0.0           0.5          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0  -0.0  -2.5   0.0   0.0   0.0   0.0           0.0         0.0           0.0   0.0   2.0   0.0   0.0   0.0   0.0           0.0          0.0           0.0           0.0          0.5           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0  -2.5   0.0  -0.0   0.0           0.0         0.0           0.0   0.0   0.0   2.0   0.0   0.0   0.0           0.0          0.0           0.0           0.0          0.0           0.5    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0  -0.0  -2.5   0.0   0.0           0.0         0.0           0.0   0.0   0.0   0.0   2.0   0.0   0.0           0.0          0.0           0.0           0.0          0.0           0.0    0.5   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0  -0.0  -2.5   0.0           0.0         0.0           0.0   0.0   0.0   0.0   0.0   2.0   0.0           0.0          0.0           0.0           0.0          0.0           0.0    0.0   0.5    0.0   0.0
0.0   0.0   1.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0  -5.0           5.0        -2.0           0.0   0.0   0.0   0.0   0.0   0.0   1.0           0.0          0.0           0.0           0.0          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   1.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0  -0.5          -2.0         0.5           0.0   0.0   0.0   0.0   0.0   0.0   0.0           1.0          0.0           0.0           0.0          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   1.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.5          -2.0        -3.5           4.5  -3.0   1.5   0.0   0.0   0.0   0.0           0.0          1.0           0.0           0.0          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   1.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         0.0          -5.0   5.0  -2.0   0.0   0.0   0.0   0.0           0.0          0.0           1.0           0.0          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   1.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         0.0          -0.5  -2.0   0.5   0.0   0.0   0.0   0.0           0.0          0.0           0.0           1.0          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   1.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         0.0           0.5  -2.0  -3.5   4.5  -3.0   1.5   0.0           0.0          0.0           0.0           0.0          1.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   1.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         0.0           0.0   0.0   0.0  -5.0   5.0  -2.0   0.0           0.0          0.0           0.0           0.0          0.0           1.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   1.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         0.0           0.0   0.0   0.0  -0.5  -2.0   0.5   0.0           0.0          0.0           0.0           0.0          0.0           0.0    1.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   1.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         0.0           0.0   0.0   0.0   0.5  -2.0  -3.5   0.0           0.0          0.0           0.0           0.0          0.0           1.5   -3.0   5.5    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   1.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   8.75         -4.5         2.25          0.0   0.0   0.0   0.0   0.0   0.0  -5.25         -3.0          0.75          0.0           0.0          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   1.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   2.81025e-16   2.0         9.36751e-17   0.0   0.0   0.0   0.0   0.0   0.0   0.75         -3.0         -0.75          0.0           0.0          0.0           0.0    0.0   0.0    0.0   0.0
-0.0  -0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   1.0   0.0   0.0   0.0   0.0   0.0   0.0  -1.8735e-15    1.249e-15   2.0           0.0   0.0   0.0   0.0   0.0   0.0  -3.0           7.5         -7.5           0.0           0.0          0.0           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   1.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         0.0           2.0   0.0   0.0   0.0   0.0   0.0   2.25         -4.5          6.75         -5.25         -3.0          0.75          0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   1.0   0.0   0.0   0.0   0.0   0.0           0.0         0.0           0.0   2.0   0.0   0.0   0.0   0.0   9.36751e-17  -1.8735e-16   2.81025e-16   0.75         -3.0         -0.75          0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   1.0   0.0   0.0   0.0   0.0           0.0         0.0           0.0   0.0   2.0   0.0   0.0   0.0  -6.245e-16     1.249e-15   -1.8735e-15   -3.0           7.5         -7.5           0.0    0.0   0.0    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   1.0   0.0   0.0   0.0           0.0         0.0           0.0   0.0   0.0   2.0   0.0   0.0   0.0           0.0          0.0           2.25         -4.5          6.75         -5.25  -3.0   0.75   0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   1.0   0.0   0.0           0.0         0.0           0.0   0.0   0.0   0.0   2.0   0.0   0.0           0.0          0.0           9.36751e-17  -1.8735e-16   2.81025e-16   0.75  -3.0  -0.75   0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   1.0   0.0           0.0         0.0           0.0   0.0   0.0   0.0   0.0   2.0   0.0           0.0          0.0          -6.245e-16     1.249e-15   -1.8735e-15   -3.0    7.5  -7.5    0.0   0.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         0.0           0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0          0.0           0.0           0.0          0.0           0.25  -0.5   0.75  -2.5   2.0
0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0         0.0           0.0   0.0   0.0   0.0   0.0   0.0   0.0           0.0          0.0           0.0           0.0          0.0           0.5   -1.0   1.5    1.0  -2.0]


@testset "4.4.9 trig^m (a+b cot^n+c cot^(2 n))^p" begin
    (a, b, c, d, e, x, ) = @variables a b c d e x

    #= ::Package:: =#

    #= ::Title:: =#
    #=Integrands*of*the*form*Trig(d+e*x)^m*(a+b*cot(d+e*x)^n+c*cot(d+e*x)^(2*n))^p=#

    #= ::Section:: =#
    #=Integrands*of*the*form*sin(d+e*x)^m*(a+b*cot(d+e*x)^n+c*cot(d+e*x)^(2*n))^p=#

    #= ::Section:: =#
    #=Integrands*of*the*form*cos(d+e*x)^m*(a+b*cot(d+e*x)^n+c*cot(d+e*x)^(2*n))^p=#

    #= ::Section::Closed:: =#
    #=Integrands*of*the*form*cot(d+e*x)^m*(a+b*cot(d+e*x)^n+c*cot(d+e*x)^(2*n))^p=#

    #= ::Subsection::Closed:: =#
    #=Integrands*of*the*form*cot(d+e*x)^m*(a+b*cot(d+e*x)+c*cot(d+e*x)^2)^(p/2)=#

    @test_int [cot(d + e*x)^5/sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2), x, 15, -((sqrt(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((a - c - sqrt(a^2 + b^2 - 2*a*c + c^2) + b*cot(d + e*x))/(sqrt(2)*sqrt(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*sqrt(a^2 + b^2 - 2*a*c + c^2)*e)) + (sqrt(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((a - c + sqrt(a^2 + b^2 - 2*a*c + c^2) + b*cot(d + e*x))/(sqrt(2)*sqrt(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*sqrt(a^2 + b^2 - 2*a*c + c^2)*e) - (b*atanh((b + 2*c*cot(d + e*x))/(2*sqrt(c)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(2*c^(3/2)*e) + (b*(5*b^2 - 12*a*c)*atanh((b + 2*c*cot(d + e*x))/(2*sqrt(c)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(16*c^(7/2)*e) + sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)/(c*e) - (cot(d + e*x)^2*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))/(3*c*e) - ((15*b^2 - 16*a*c - 10*b*c*cot(d + e*x))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))/(24*c^3*e)]
    @test_int [cot(d + e*x)^3/sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2), x, 11, (sqrt(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((a - c - sqrt(a^2 + b^2 - 2*a*c + c^2) + b*cot(d + e*x))/(sqrt(2)*sqrt(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*sqrt(a^2 + b^2 - 2*a*c + c^2)*e) - (sqrt(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((a - c + sqrt(a^2 + b^2 - 2*a*c + c^2) + b*cot(d + e*x))/(sqrt(2)*sqrt(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*sqrt(a^2 + b^2 - 2*a*c + c^2)*e) + (b*atanh((b + 2*c*cot(d + e*x))/(2*sqrt(c)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(2*c^(3/2)*e) - sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)/(c*e)]
    @test_int [cot(d + e*x)^1/sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2), x, 6, -((sqrt(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((a - c - sqrt(a^2 + b^2 - 2*a*c + c^2) + b*cot(d + e*x))/(sqrt(2)*sqrt(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*sqrt(a^2 + b^2 - 2*a*c + c^2)*e)) + (sqrt(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((a - c + sqrt(a^2 + b^2 - 2*a*c + c^2) + b*cot(d + e*x))/(sqrt(2)*sqrt(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*sqrt(a^2 + b^2 - 2*a*c + c^2)*e)]
    @test_int [tan(d + e*x)^1/sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2), x, 10, atanh((2*a + b*cot(d + e*x))/(2*sqrt(a)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)))/(sqrt(a)*e) + (sqrt(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((a - c - sqrt(a^2 + b^2 - 2*a*c + c^2) + b*cot(d + e*x))/(sqrt(2)*sqrt(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*sqrt(a^2 + b^2 - 2*a*c + c^2)*e) - (sqrt(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((a - c + sqrt(a^2 + b^2 - 2*a*c + c^2) + b*cot(d + e*x))/(sqrt(2)*sqrt(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*sqrt(a^2 + b^2 - 2*a*c + c^2)*e)]
    @test_int [tan(d + e*x)^3/sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2), x, 14, -(atanh((2*a + b*cot(d + e*x))/(2*sqrt(a)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)))/(sqrt(a)*e)) + ((3*b^2 - 4*a*c)*atanh((2*a + b*cot(d + e*x))/(2*sqrt(a)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(8*a^(5/2)*e) - (sqrt(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((a - c - sqrt(a^2 + b^2 - 2*a*c + c^2) + b*cot(d + e*x))/(sqrt(2)*sqrt(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*sqrt(a^2 + b^2 - 2*a*c + c^2)*e) + (sqrt(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((a - c + sqrt(a^2 + b^2 - 2*a*c + c^2) + b*cot(d + e*x))/(sqrt(2)*sqrt(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*sqrt(a^2 + b^2 - 2*a*c + c^2)*e) - (3*b*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)*tan(d + e*x))/(4*a^2*e) + (sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)*tan(d + e*x)^2)/(2*a*e)]

    @test_int [cot(d + e*x)^5*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2), x, 21, -((sqrt(a^2 + b^2 + c*(c + sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c + sqrt(a^2 + b^2 - 2*a*c + c^2)))*atan((b^2 + (a - c)*(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*sqrt(a^2 + b^2 - 2*a*c + c^2)*cot(d + e*x))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*sqrt(a^2 + b^2 + c*(c + sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c + sqrt(a^2 + b^2 - 2*a*c + c^2)))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*e)) - (b*atanh((b + 2*c*cot(d + e*x))/(2*sqrt(c)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(2*sqrt(c)*e) + (b*(b^2 - 4*a*c)*atanh((b + 2*c*cot(d + e*x))/(2*sqrt(c)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(16*c^(5/2)*e) - (b*(7*b^2 - 12*a*c)*(b^2 - 4*a*c)*atanh((b + 2*c*cot(d + e*x))/(2*sqrt(c)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(256*c^(9/2)*e) + (sqrt(a^2 + b^2 + c*(c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c - sqrt(a^2 + b^2 - 2*a*c + c^2)))*atanh((b^2 + (a - c)*(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2)) + b*sqrt(a^2 + b^2 - 2*a*c + c^2)*cot(d + e*x))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*sqrt(a^2 + b^2 + c*(c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c - sqrt(a^2 + b^2 - 2*a*c + c^2)))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*e) - sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)/e - (b*(b + 2*c*cot(d + e*x))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))/(8*c^2*e) + (b*(7*b^2 - 12*a*c)*(b + 2*c*cot(d + e*x))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))/(128*c^4*e) + (a + b*cot(d + e*x) + c*cot(d + e*x)^2)^(3/2)/(3*c*e) - (cot(d + e*x)^2*(a + b*cot(d + e*x) + c*cot(d + e*x)^2)^(3/2))/(5*c*e) - ((35*b^2 - 32*a*c - 42*b*c*cot(d + e*x))*(a + b*cot(d + e*x) + c*cot(d + e*x)^2)^(3/2))/(240*c^3*e)]
    @test_int [cot(d + e*x)^3*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2), x, 16, (sqrt(a^2 + b^2 + c*(c + sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c + sqrt(a^2 + b^2 - 2*a*c + c^2)))*atan((b^2 + (a - c)*(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*sqrt(a^2 + b^2 - 2*a*c + c^2)*cot(d + e*x))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*sqrt(a^2 + b^2 + c*(c + sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c + sqrt(a^2 + b^2 - 2*a*c + c^2)))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*e) + (b*atanh((b + 2*c*cot(d + e*x))/(2*sqrt(c)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(2*sqrt(c)*e) - (b*(b^2 - 4*a*c)*atanh((b + 2*c*cot(d + e*x))/(2*sqrt(c)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(16*c^(5/2)*e) - (sqrt(a^2 + b^2 + c*(c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c - sqrt(a^2 + b^2 - 2*a*c + c^2)))*atanh((b^2 + (a - c)*(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2)) + b*sqrt(a^2 + b^2 - 2*a*c + c^2)*cot(d + e*x))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*sqrt(a^2 + b^2 + c*(c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c - sqrt(a^2 + b^2 - 2*a*c + c^2)))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*e) + sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)/e + (b*(b + 2*c*cot(d + e*x))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))/(8*c^2*e) - (a + b*cot(d + e*x) + c*cot(d + e*x)^2)^(3/2)/(3*c*e)]
    @test_int [cot(d + e*x)^1*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2), x, 10, -((sqrt(a^2 + b^2 + c*(c + sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c + sqrt(a^2 + b^2 - 2*a*c + c^2)))*atan((b^2 + (a - c)*(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*sqrt(a^2 + b^2 - 2*a*c + c^2)*cot(d + e*x))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*sqrt(a^2 + b^2 + c*(c + sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c + sqrt(a^2 + b^2 - 2*a*c + c^2)))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*e)) - (b*atanh((b + 2*c*cot(d + e*x))/(2*sqrt(c)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(2*sqrt(c)*e) + (sqrt(a^2 + b^2 + c*(c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c - sqrt(a^2 + b^2 - 2*a*c + c^2)))*atanh((b^2 + (a - c)*(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2)) + b*sqrt(a^2 + b^2 - 2*a*c + c^2)*cot(d + e*x))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*sqrt(a^2 + b^2 + c*(c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c - sqrt(a^2 + b^2 - 2*a*c + c^2)))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*e) - sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)/e]
    @test_int [tan(d + e*x)^1*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2), x, 18, (sqrt(a^2 + b^2 + c*(c + sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c + sqrt(a^2 + b^2 - 2*a*c + c^2)))*atan((b^2 + (a - c)*(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*sqrt(a^2 + b^2 - 2*a*c + c^2)*cot(d + e*x))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*sqrt(a^2 + b^2 + c*(c + sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c + sqrt(a^2 + b^2 - 2*a*c + c^2)))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*e) + (sqrt(a)*atanh((2*a + b*cot(d + e*x))/(2*sqrt(a)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/e - (sqrt(a^2 + b^2 + c*(c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c - sqrt(a^2 + b^2 - 2*a*c + c^2)))*atanh((b^2 + (a - c)*(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2)) + b*sqrt(a^2 + b^2 - 2*a*c + c^2)*cot(d + e*x))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*sqrt(a^2 + b^2 + c*(c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c - sqrt(a^2 + b^2 - 2*a*c + c^2)))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*e)]
    @test_int [tan(d + e*x)^3*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2), x, 21, -((sqrt(a^2 + b^2 + c*(c + sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c + sqrt(a^2 + b^2 - 2*a*c + c^2)))*atan((b^2 + (a - c)*(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*sqrt(a^2 + b^2 - 2*a*c + c^2)*cot(d + e*x))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*sqrt(a^2 + b^2 + c*(c + sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c + sqrt(a^2 + b^2 - 2*a*c + c^2)))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*e)) - (sqrt(a)*atanh((2*a + b*cot(d + e*x))/(2*sqrt(a)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/e - ((b^2 - 4*a*c)*atanh((2*a + b*cot(d + e*x))/(2*sqrt(a)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(8*a^(3/2)*e) + (sqrt(a^2 + b^2 + c*(c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c - sqrt(a^2 + b^2 - 2*a*c + c^2)))*atanh((b^2 + (a - c)*(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2)) + b*sqrt(a^2 + b^2 - 2*a*c + c^2)*cot(d + e*x))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*sqrt(a^2 + b^2 + c*(c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - a*(2*c - sqrt(a^2 + b^2 - 2*a*c + c^2)))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(1/4)*e) + ((2*a + b*cot(d + e*x))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)*tan(d + e*x)^2)/(4*a*e)]

    @test_int [cot(d + e*x)^7/(a + b*cot(d + e*x) + c*cot(d + e*x)^2)^(3/2), x, 20, -((3*b*atanh((b + 2*c*cot(d + e*x))/(2*sqrt(c)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(2*c^(5/2)*e)) + (5*b*(7*b^2 - 12*a*c)*atanh((b + 2*c*cot(d + e*x))/(2*sqrt(c)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(16*c^(9/2)*e) + (sqrt(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 + (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((b^2 - (a - c)*(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*cot(d + e*x))/(sqrt(2)*sqrt(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 + (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(3/2)*e) - (sqrt(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 - (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((b^2 - (a - c)*(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*cot(d + e*x))/(sqrt(2)*sqrt(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 - (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(3/2)*e) - (2*(2*a + b*cot(d + e*x)))/((b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)) + (2*cot(d + e*x)^2*(2*a + b*cot(d + e*x)))/((b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)) - (2*cot(d + e*x)^4*(2*a + b*cot(d + e*x)))/((b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)) + (2*(a*(b^2 - 2*(a - c)*c) + b*c*(a + c)*cot(d + e*x)))/((b^2 + (a - c)^2)*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)) - ((7*b^2 - 16*a*c)*cot(d + e*x)^2*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))/(3*c^2*(b^2 - 4*a*c)*e) + (2*b*cot(d + e*x)^3*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))/(c*(b^2 - 4*a*c)*e) + ((3*b^2 - 8*a*c - 2*b*c*cot(d + e*x))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))/(c^2*(b^2 - 4*a*c)*e) - ((105*b^4 - 460*a*b^2*c + 256*a^2*c^2 - 2*b*c*(35*b^2 - 116*a*c)*cot(d + e*x))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))/(24*c^4*(b^2 - 4*a*c)*e)]
    @test_int [cot(d + e*x)^5/(a + b*cot(d + e*x) + c*cot(d + e*x)^2)^(3/2), x, 14, (3*b*atanh((b + 2*c*cot(d + e*x))/(2*sqrt(c)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(2*c^(5/2)*e) - (sqrt(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 + (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((b^2 - (a - c)*(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*cot(d + e*x))/(sqrt(2)*sqrt(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 + (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(3/2)*e) + (sqrt(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 - (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((b^2 - (a - c)*(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*cot(d + e*x))/(sqrt(2)*sqrt(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 - (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(3/2)*e) + (2*(2*a + b*cot(d + e*x)))/((b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)) - (2*cot(d + e*x)^2*(2*a + b*cot(d + e*x)))/((b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)) - (2*(a*(b^2 - 2*(a - c)*c) + b*c*(a + c)*cot(d + e*x)))/((b^2 + (a - c)^2)*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)) - ((3*b^2 - 8*a*c - 2*b*c*cot(d + e*x))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))/(c^2*(b^2 - 4*a*c)*e)]
    @test_int [cot(d + e*x)^3/(a + b*cot(d + e*x) + c*cot(d + e*x)^2)^(3/2), x, 10, (sqrt(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 + (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((b^2 - (a - c)*(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*cot(d + e*x))/(sqrt(2)*sqrt(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 + (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(3/2)*e) - (sqrt(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 - (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((b^2 - (a - c)*(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*cot(d + e*x))/(sqrt(2)*sqrt(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 - (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(3/2)*e) - (2*(2*a + b*cot(d + e*x)))/((b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)) + (2*(a*(b^2 - 2*(a - c)*c) + b*c*(a + c)*cot(d + e*x)))/((b^2 + (a - c)^2)*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))]
    @test_int [cot(d + e*x)^1/(a + b*cot(d + e*x) + c*cot(d + e*x)^2)^(3/2), x, 7, -((sqrt(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 + (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((b^2 - (a - c)*(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*cot(d + e*x))/(sqrt(2)*sqrt(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 + (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(3/2)*e)) + (sqrt(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 - (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((b^2 - (a - c)*(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*cot(d + e*x))/(sqrt(2)*sqrt(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 - (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(3/2)*e) - (2*(a*(b^2 - 2*(a - c)*c) + b*c*(a + c)*cot(d + e*x)))/((b^2 + (a - c)^2)*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))]
    @test_int [tan(d + e*x)^1/(a + b*cot(d + e*x) + c*cot(d + e*x)^2)^(3/2), x, 13, atanh((2*a + b*cot(d + e*x))/(2*sqrt(a)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)))/(a^(3/2)*e) + (sqrt(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 + (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((b^2 - (a - c)*(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*cot(d + e*x))/(sqrt(2)*sqrt(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 + (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(3/2)*e) - (sqrt(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 - (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((b^2 - (a - c)*(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*cot(d + e*x))/(sqrt(2)*sqrt(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 - (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(3/2)*e) - (2*(b^2 - 2*a*c + b*c*cot(d + e*x)))/(a*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)) + (2*(a*(b^2 - 2*(a - c)*c) + b*c*(a + c)*cot(d + e*x)))/((b^2 + (a - c)^2)*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))]
    @test_int [tan(d + e*x)^3/(a + b*cot(d + e*x) + c*cot(d + e*x)^2)^(3/2), x, 18, -(atanh((2*a + b*cot(d + e*x))/(2*sqrt(a)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)))/(a^(3/2)*e)) + (3*(5*b^2 - 4*a*c)*atanh((2*a + b*cot(d + e*x))/(2*sqrt(a)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2))))/(8*a^(7/2)*e) - (1/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(3/2)*e))*(sqrt(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 + (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((b^2 - (a - c)*(a - c + sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*cot(d + e*x))/(sqrt(2)*sqrt(2*a - 2*c - sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 + (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)))) + (1/(sqrt(2)*(a^2 + b^2 - 2*a*c + c^2)^(3/2)*e))*(sqrt(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 - (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*atanh((b^2 - (a - c)*(a - c - sqrt(a^2 + b^2 - 2*a*c + c^2)) - b*(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*cot(d + e*x))/(sqrt(2)*sqrt(2*a - 2*c + sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a^2 - b^2 - 2*a*c + c^2 - (a - c)*sqrt(a^2 + b^2 - 2*a*c + c^2))*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)))) + (2*(b^2 - 2*a*c + b*c*cot(d + e*x)))/(a*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)) - (2*(a*(b^2 - 2*(a - c)*c) + b*c*(a + c)*cot(d + e*x)))/((b^2 + (a - c)^2)*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)) - (b*(15*b^2 - 52*a*c)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)*tan(d + e*x))/(4*a^3*(b^2 - 4*a*c)*e) - (2*(b^2 - 2*a*c + b*c*cot(d + e*x))*tan(d + e*x)^2)/(a*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)) + ((5*b^2 - 12*a*c)*sqrt(a + b*cot(d + e*x) + c*cot(d + e*x)^2)*tan(d + e*x)^2)/(2*a^2*(b^2 - 4*a*c)*e)]

    #= ::Subsection::Closed:: =#
    #=Integrands*of*the*form*cot(d+e*x)^m*(a+b*cot(d+e*x)^2+c*cot(d+e*x)^4)^(p/2)=#

    @test_int [cot(d + e*x)^5/sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4), x, 8, atanh((2*a - b + (b - 2*c)*cot(d + e*x)^2)/(2*sqrt(a - b + c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*sqrt(a - b + c)*e) + ((b + 2*c)*atanh((b + 2*c*cot(d + e*x)^2)/(2*sqrt(c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(4*c^(3/2)*e) - sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)/(2*c*e)]
    @test_int [cot(d + e*x)^3/sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4), x, 7, -(atanh((2*a - b + (b - 2*c)*cot(d + e*x)^2)/(2*sqrt(a - b + c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*sqrt(a - b + c)*e)) - atanh((b + 2*c*cot(d + e*x)^2)/(2*sqrt(c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*sqrt(c)*e)]
    @test_int [cot(d + e*x)^1/sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4), x, 4, atanh((2*a - b + (b - 2*c)*cot(d + e*x)^2)/(2*sqrt(a - b + c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*sqrt(a - b + c)*e)]
    @test_int [tan(d + e*x)^1/sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4), x, 8, atanh((2*a + b*cot(d + e*x)^2)/(2*sqrt(a)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*sqrt(a)*e) - atanh((2*a - b + (b - 2*c)*cot(d + e*x)^2)/(2*sqrt(a - b + c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*sqrt(a - b + c)*e)]
    @test_int [tan(d + e*x)^3/sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4), x, 11, -(atanh((2*a + b*cot(d + e*x)^2)/(2*sqrt(a)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*sqrt(a)*e)) - (b*atanh((2*a + b*cot(d + e*x)^2)/(2*sqrt(a)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(4*a^(3/2)*e) + atanh((2*a - b + (b - 2*c)*cot(d + e*x)^2)/(2*sqrt(a - b + c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*sqrt(a - b + c)*e) + (sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)*tan(d + e*x)^2)/(2*a*e)]

    @test_int [cot(d + e*x)^5*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4), x, 9, (sqrt(a - b + c)*atanh((2*a - b + (b - 2*c)*cot(d + e*x)^2)/(2*sqrt(a - b + c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(2*e) - ((b^3 + 2*b^2*c - 4*b*(a - 2*c)*c - 8*c^2*(a + 2*c))*atanh((b + 2*c*cot(d + e*x)^2)/(2*sqrt(c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(32*c^(5/2)*e) + (((b - 2*c)*(b + 4*c) + 2*c*(b + 2*c)*cot(d + e*x)^2)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))/(16*c^2*e) - (a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)^(3/2)/(6*c*e)]
    @test_int [cot(d + e*x)^3*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4), x, 8, -((sqrt(a - b + c)*atanh((2*a - b + (b - 2*c)*cot(d + e*x)^2)/(2*sqrt(a - b + c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(2*e)) + ((b^2 + 4*b*c - 4*c*(a + 2*c))*atanh((b + 2*c*cot(d + e*x)^2)/(2*sqrt(c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(16*c^(3/2)*e) - ((b - 4*c + 2*c*cot(d + e*x)^2)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))/(8*c*e)]
    @test_int [cot(d + e*x)^1*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4), x, 8, (sqrt(a - b + c)*atanh((2*a - b + (b - 2*c)*cot(d + e*x)^2)/(2*sqrt(a - b + c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(2*e) - ((b - 2*c)*atanh((b + 2*c*cot(d + e*x)^2)/(2*sqrt(c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(4*sqrt(c)*e) - sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)/(2*e)]
    @test_int [tan(d + e*x)^1*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4), x, 10, (sqrt(a)*atanh((2*a + b*cot(d + e*x)^2)/(2*sqrt(a)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(2*e) - (sqrt(a - b + c)*atanh((2*a - b + (b - 2*c)*cot(d + e*x)^2)/(2*sqrt(a - b + c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(2*e) - (sqrt(c)*atanh((b + 2*c*cot(d + e*x)^2)/(2*sqrt(c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(2*e)]
    @test_int [tan(d + e*x)^3*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4), x, 22, -((sqrt(a)*atanh((2*a + b*cot(d + e*x)^2)/(2*sqrt(a)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(2*e)) + (b*atanh((2*a + b*cot(d + e*x)^2)/(2*sqrt(a)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(4*sqrt(a)*e) + (sqrt(a - b + c)*atanh((2*a - b + (b - 2*c)*cot(d + e*x)^2)/(2*sqrt(a - b + c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(2*e) + (b*atanh((b + 2*c*cot(d + e*x)^2)/(2*sqrt(c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(4*sqrt(c)*e) - ((b - 2*c)*atanh((b + 2*c*cot(d + e*x)^2)/(2*sqrt(c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(4*sqrt(c)*e) - (sqrt(c)*atanh((b + 2*c*cot(d + e*x)^2)/(2*sqrt(c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(2*e) + (sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)*tan(d + e*x)^2)/(2*e)]

    @test_int [cot(d + e*x)^7/(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)^(3/2), x, 8, -(atanh((2*a - b + (b - 2*c)*cot(d + e*x)^2)/(2*sqrt(a - b + c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*(a - b + c)^(3/2)*e)) - atanh((b + 2*c*cot(d + e*x)^2)/(2*sqrt(c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*c^(3/2)*e) - (a*(b^2 - a*(b + 2*c)) + (b^3 + 2*a^2*c - a*b*(b + 3*c))*cot(d + e*x)^2)/(c*(a - b + c)*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))]
    @test_int [cot(d + e*x)^5/(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)^(3/2), x, 6, atanh((2*a - b + (b - 2*c)*cot(d + e*x)^2)/(2*sqrt(a - b + c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*(a - b + c)^(3/2)*e) - (a*(2*a - b) + ((a - b)*b + 2*a*c)*cot(d + e*x)^2)/((a - b + c)*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))]
    @test_int [cot(d + e*x)^3/(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)^(3/2), x, 6, -(atanh((2*a - b + (b - 2*c)*cot(d + e*x)^2)/(2*sqrt(a - b + c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*(a - b + c)^(3/2)*e)) + (a*(b - 2*c) + (2*a - b)*c*cot(d + e*x)^2)/((a - b + c)*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))]
    @test_int [cot(d + e*x)^1/(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)^(3/2), x, 6, atanh((2*a - b + (b - 2*c)*cot(d + e*x)^2)/(2*sqrt(a - b + c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*(a - b + c)^(3/2)*e) - (b^2 - 2*a*c - b*c + (b - 2*c)*c*cot(d + e*x)^2)/((a - b + c)*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))]
    @test_int [tan(d + e*x)^1/(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)^(3/2), x, 12, atanh((2*a + b*cot(d + e*x)^2)/(2*sqrt(a)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*a^(3/2)*e) - atanh((2*a - b + (b - 2*c)*cot(d + e*x)^2)/(2*sqrt(a - b + c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*(a - b + c)^(3/2)*e) - (b^2 - 2*a*c + b*c*cot(d + e*x)^2)/(a*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)) + (b^2 - 2*a*c - b*c + (b - 2*c)*c*cot(d + e*x)^2)/((a - b + c)*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))]
    @test_int [tan(d + e*x)^3/(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)^(3/2), x, 16, -(atanh((2*a + b*cot(d + e*x)^2)/(2*sqrt(a)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*a^(3/2)*e)) - (3*b*atanh((2*a + b*cot(d + e*x)^2)/(2*sqrt(a)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4))))/(4*a^(5/2)*e) + atanh((2*a - b + (b - 2*c)*cot(d + e*x)^2)/(2*sqrt(a - b + c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)))/(2*(a - b + c)^(3/2)*e) + (b^2 - 2*a*c + b*c*cot(d + e*x)^2)/(a*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)) - (b^2 - 2*a*c - b*c + (b - 2*c)*c*cot(d + e*x)^2)/((a - b + c)*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)) - ((b^2 - 2*a*c + b*c*cot(d + e*x)^2)*tan(d + e*x)^2)/(a*(b^2 - 4*a*c)*e*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)) + ((3*b^2 - 8*a*c)*sqrt(a + b*cot(d + e*x)^2 + c*cot(d + e*x)^4)*tan(d + e*x)^2)/(2*a^2*(b^2 - 4*a*c)*e)]
    end

test/4 Trig functions/4.4 Cotangent/4.4.9 trig^m (a+b cot^n+c cot^(2 n))^p.jl