# This file contains functions for the Russell DEA model
"""
    RussellDEAModel
An data structure representing a Russell DEA model.
"""
struct RussellDEAModel <: AbstractTechnicalDEAModel
    n::Int64
    m::Int64
    s::Int64
    orient::Symbol
    rts::Symbol
    dmunames::Union{Vector{String},Nothing}
    eff::Vector
    thetaX::Matrix
    thetaY::Matrix
    slackX::Matrix
    slackY::Matrix
    lambda::SparseMatrixCSC{Float64, Int64}
    Xtarget::Matrix
    Ytarget::Matrix
end

"""
    dearussell(X, Y)
Compute the Russell model using data envelopment analysis for inputs X and outputs Y.

# Optional Arguments
- `orient=:Input`: chooses the Russell input mode. For the Russell output model choose `:Output`. For the Russell graph model choose `:Graph`.
- `rts=:CRS`: chooses constant returns to scale. For variable returns to scale choose `:VRS`.
- `slack=true`: computes input and output slacks.
- `Xref=X`: Identifies the reference set of inputs against which the units are evaluated.
- `Yref=Y`: Identifies the reference set of outputs against which the units are evaluated.
- `names`: a vector of strings with the names of the decision making units.

# Examples
```jldoctest
julia> X = [5 13; 16 12; 16 26; 17 15; 18 14; 23 6; 25 10; 27 22; 37 14; 42 25; 5 17];

julia> Y = [12; 14; 25; 26; 8; 9; 27; 30; 31; 26; 12];

julia> dearussell(X, Y, rts = :VRS)
Russell DEA Model 
DMUs = 11; Inputs = 2; Outputs = 1
Orientation = Input; Returns to Scale = VRS
───────────────────────────────────────────────
    efficiency     effX1     effX2      slackY1
───────────────────────────────────────────────
1     1.0       1.0       1.0       0.0
2     0.79386   0.587719  1.0       9.88357e-15
3     1.0       1.0       1.0       0.0
4     1.0       1.0       1.0       0.0
5     0.603175  0.277778  0.928571  4.0
6     1.0       1.0       1.0       3.78178e-16
7     1.0       1.0       1.0       0.0
8     1.0       1.0       1.0       0.0
9     1.0       1.0       1.0       0.0
10    0.485746  0.563492  0.408     0.0
11    0.882353  1.0       0.764706  0.0
───────────────────────────────────────────────
```
"""
function dearussell(X::Union{Matrix,Vector}, Y::Union{Matrix,Vector};
    orient::Symbol = :Input, rts::Symbol = :CRS, slack::Bool = true,
    Xref::Union{Matrix,Vector,Nothing} = nothing, Yref::Union{Matrix,Vector,Nothing} = nothing,
    names::Union{Vector{String},Nothing} = nothing,
    optimizer::Union{DEAOptimizer,Nothing} = nothing)::RussellDEAModel

    # Check parameters
    nx, m = size(X, 1), size(X, 2)
    ny, s = size(Y, 1), size(Y, 2)

    if Xref === nothing Xref = X end
    if Yref === nothing Yref = Y end

    nrefx, mref = size(Xref, 1), size(Xref, 2)
    nrefy, sref = size(Yref, 1), size(Yref, 2)

    if nx != ny
        throw(DimensionMismatch("number of rows in X and Y ($nx, $ny) are not equal"));
    end
    if nrefx != nrefy
        throw(DimensionMismatch("number of rows in Xref and Yref ($nrefx, $nrefy) are not equal"));
    end
    if m != mref
        throw(DimensionMismatch("number of columns in X and Xref ($m, $mref) are not equal"));
    end
    if s != sref
        throw(DimensionMismatch("number of columns in Y and Yref ($s, $sref) are not equal"));
    end

    # Default optimizer
    if optimizer === nothing 
        if orient == :Input || orient == :Output
            optimizer = DEAOptimizer(:LP)
        else
            optimizer = DEAOptimizer(:NLP)
        end
    end

    # Compute efficiency for each DMU
    n = nx
    nref = nrefx

    effi = zeros(n)
    thetaXi = zeros(n, m)
    thetaYi = zeros(n, s)
    lambdaeff = spzeros(n, nref)

    for i=1:n
        # Value of inputs and outputs to evaluate
        x0 = X[i,:]
        y0 = Y[i,:]

        # Create the optimization model
        if orient == :Input
            # Input orientation
            deamodel = newdeamodel(optimizer)
            
            mno0 = sum(x0 .!= 0)

            @variable(deamodel, lambda[1:nref] >= 0)
            @variable(deamodel, theta[1:m] <= 1)
            @objective(deamodel, Min, 1 / mno0 * sum(theta[t] for t in 1:m if x0[t] != 0 ))

            @constraint(deamodel, [j in 1:m], sum(Xref[t,j] * lambda[t] for t in 1:nref) <= theta[j] * x0[j])
            @constraint(deamodel, [j in 1:s], sum(Yref[t,j] * lambda[t] for t in 1:nref) >= y0[j])
        elseif orient == :Output
            # Output orientation
            deamodel = newdeamodel(optimizer)
            
            sno0 = sum(y0 .!= 0)

            @variable(deamodel, lambda[1:nref] >= 0)
            @variable(deamodel, theta[1:s] >= 1)
            @objective(deamodel, Max, 1 / sno0 * sum(theta[t] for t in 1:s if y0[t] != 0 ))

            @constraint(deamodel, [j in 1:m], sum(Xref[t,j] * lambda[t] for t in 1:nref) <= x0[j])
            @constraint(deamodel, [j in 1:s], sum(Yref[t,j] * lambda[t] for t in 1:nref) >= theta[j] * y0[j])
        elseif orient == :Graph
            # Graph orientation
            deamodel = newdeamodel(optimizer)
            set_silent(deamodel)            

            mno0 = sum(x0 .!= 0)
            sno0 = sum(y0 .!= 0)

            @variable(deamodel, lambda[1:nref] >= 0)
            @variable(deamodel, theta[1:m] <= 1)
            @variable(deamodel, phi[1:s] >= 1)

            @NLobjective(deamodel, Min, 1 / (mno0 + sno0) * (sum(theta[t] for t in 1:m if x0[t] != 0 ) + sum(1/phi[t] for t in 1:s if y0[t] != 0) ))

            @constraint(deamodel, [j in 1:m], sum(Xref[t,j] * lambda[t] for t in 1:nref) == theta[j] * x0[j])
            @constraint(deamodel, [j in 1:s], sum(Yref[t,j] * lambda[t] for t in 1:nref) == phi[j] * y0[j])
        else
            throw(ArgumentError("`orient` must be :Input, :Output or :Graph"));
        end

        # Add return to scale constraints
        if rts == :CRS
            # No contraint to add for constant returns to scale
        elseif rts == :VRS
            @constraint(deamodel, sum(lambda) == 1)
        else
            throw(ArgumentError("`rts` must be :CRS or :VRS"));
        end

        # Optimize and return results
        JuMP.optimize!(deamodel)

        effi[i]  = JuMP.objective_value(deamodel)
        lambdaeff[i,:] = JuMP.value.(lambda)

        if orient == :Input
            thetaXi[i,:] = JuMP.value.(theta)
        elseif orient == :Output
            thetaYi[i,:] = JuMP.value.(theta)
        elseif orient == :Graph
            thetaXi[i,:] = JuMP.value.(theta)
            thetaYi[i,:] = JuMP.value.(phi)
        end

        # Check termination status
        if (termination_status(deamodel) != MOI.OPTIMAL) && (termination_status(deamodel) != MOI.LOCALLY_SOLVED)
            @warn ("DMU $i termination status: $(termination_status(deamodel)). Primal status: $(primal_status(deamodel)). Dual status: $(dual_status(deamodel))")
        end

    end

    # Get first-stage X and Y targets
    if orient == :Input
        Xtarget = X .* thetaXi
        Ytarget = Y
    elseif orient == :Output
        Xtarget = X
        Ytarget = Y .* thetaYi
    elseif orient == :Graph
        Xtarget = X .* thetaXi
        Ytarget = Y .* thetaYi
    end

    # Compute slacks
    if (slack == true) && (orient != :Graph)

        # Use additive model with Russell efficient X and Y to get slacks
        russellSlacks = deaadd(Xtarget, Ytarget, :Ones, rts = rts, Xref = Xref, Yref = Yref, optimizer = optimizer)
        slackX = slacks(russellSlacks, :X)
        slackY = slacks(russellSlacks, :Y)

        # Get second-stage X and Y targets
        Xtarget = Xtarget - slackX
        Ytarget = Ytarget + slackY
    else
        if typeof(Xtarget) <: AbstractVector    Xtarget = Xtarget[:,:]  end
        if typeof(Ytarget) <: AbstractVector    Ytarget = Ytarget[:,:]  end

        slackX = Array{Float64}(undef, 0, 0)
        slackY = Array{Float64}(undef, 0, 0)
    end

    if orient == :Input
        thetaYi = Array{Float64}(undef, 0, 0)
    elseif orient == :Output
        thetaXi = Array{Float64}(undef, 0, 0)
    end

    return RussellDEAModel(n, m, s, orient, rts, names, effi, thetaXi, thetaYi, slackX, slackY, lambdaeff, Xtarget, Ytarget)

end

function Base.show(io::IO, x::RussellDEAModel)
    compact = get(io, :compact, false)

    n = nobs(x)
    m = ninputs(x)
    s = noutputs(x)
    dmunames = names(x)

    eff = efficiency(x)
    slackX = slacks(x, :X)
    slackY = slacks(x, :Y)
    hasslacks = ! isempty(slackX)

    if !compact
        print(io, "Russell DEA Model \n")
        print(io, "DMUs = ", n)
        print(io, "; Inputs = ", m)
        print(io, "; Outputs = ", s)
        print(io, "\n")
        print(io, "Orientation = ", string(x.orient))
        print(io, "; Returns to Scale = ", string(x.rts))
        print(io, "\n")
        if x.orient == :Input
            thetaX = x.thetaX
            if hasslacks == true
                show(io, CoefTable(hcat(eff, thetaX, slackY), ["efficiency"; ["effX$i" for i in 1:m ]; ["slackY$i" for i in 1:s ]], dmunames))
            else
                show(io, CoefTable(hcat(eff, thetaX), ["efficiency"; ["effX$i" for i in 1:m ] ], dmunames))
            end
        elseif x.orient == :Output
            thetaY = x.thetaY
            if hasslacks == true
                show(io, CoefTable(hcat(eff, thetaY, slackX), ["efficiency"; ["effY$i" for i in 1:s ]; ["slackX$i" for i in 1:m ]], dmunames))
            else
                show(io, CoefTable(hcat(eff, thetaY), ["efficiency"; ["effY$i" for i in 1:s ] ], dmunames))
            end
        elseif x.orient == :Graph
            thetaX = x.thetaX
            thetaY = x.thetaY
            show(io, CoefTable(hcat(eff, thetaX, thetaY), ["efficiency"; ["effX$i" for i in 1:m ]; ["effY$i" for i in 1:s ] ], dmunames))
        end
    end

end

function efficiency(model::RussellDEAModel, type::Symbol)::Matrix

    if (type == :X && (model.orient == :Input  || model.orient == :Graph)) return model.thetaX end
    if (type == :Y && (model.orient == :Output || model.orient == :Graph)) return model.thetaY end
        
    throw(ArgumentError("$(typeof(model)) with orienation $(model.orient) has no efficiency $(type)"));

end
