# PART 5 – ASSESSMENT OF LOCAL METAL LOSS

# Determine Asessment Applicability
#Determine the assessment applicability
# @doc DesignCodeCriteria
# @doc MaterialToughness
# @doc CyclicService
# @doc Part5ComponentType
print("Begin -- Assessment Applicability and Component Type Checks\n")
creep_range = CreepRangeTemperature("Carbon Steel (UTS ≤ 414MPa (60 ksi))"; design_temperature=100.0, units="lbs-in-psi")
design = DesignCodeCriteria("ASME B31.3 Piping Code")
toughness = MaterialToughness("Certain")
cyclic = CyclicService(100, "Meets Part 14")
x = Part5ComponentType("Straight Section of Piping, Elbow or Bend - No Structural Attachments", vessel_orientation="horizontal", material="Carbon and Low Alloy Steels", D=24.75,Lss=120.0,H=0.0, NPS=10.0, design_temperature=100.0, units="lbs-in-psi")
part5_applicability = Part5AsessmentApplicability(x,design,toughness,cyclic,creep_range)

# For all assessments - determine the inspection data grid
M1 = [0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300]
M2 = [0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.100, 0.220, 0.280, 0.250, 0.240, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300]
M3 = [0.300, 0.300, 0.300, 0.300, 0.300, 0.215, 0.255, 0.215, 0.145, 0.275, 0.170, 0.240, 0.250, 0.250, 0.280, 0.290, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300]
M4 = [0.300, 0.300, 0.300, 0.300, 0.300, 0.170, 0.270, 0.190, 0.190, 0.285, 0.250, 0.225, 0.275, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300]
M5 = [0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300]
M6 = [0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300, 0.300]
CTPGrid = hcat(M6,M5,M4,M3,M2,M1) # build in descending order
CTPGrid = rotl90(CTPGrid) # rotate to correct orientation

# Input variables
    annex2c_tmin_category = "Straight Pipes Subject To Internal Pressure" # ["Cylindrical Shell","Spherical Shell","Hemispherical Head","Elliptical Head","Torispherical Head","Conical Shell","Toriconical Head","Conical Transition","Nozzles Connections in Shells",
    # "Junction Reinforcement Requirements at Conical Transitions","Tubesheets","Flat head to cylinder connections","Bolted Flanges","Straight Pipes Subject To Internal Pressure","Boiler Tubes","Pipe Bends Subject To Internal Pressure",
    # "MAWP for External Pressure","Branch Connections","API 650 Storage Tanks"]
    equipment_group = "piping" # "vessel", "tank"
    flaw_location = "internal" # "External","Internal"
    pipe_code = "B31.8" # "B31.3", "B31.8"
    FCA_string = "internal"
    metal_loss_categorization = "LTA" # "LTA" or "Groove-Like Flaw"
    units = "lbs-in-psi" # "lbs-in-psi" or "nmm-mm-mpa"
    remaining_life = true
    tmm_forcing = true
    tmm = .949
    tnom = 1.0 # nominal or furnished thickness of the component adjusted for mill undertolerance as applicable.
    trd = 1.0 # uniform thickness away from the local metal loss location established by thickness measurements at the time of the assessment.
    FCAml = 0.1 # Future Corrosion Allowance applied to the region of metal loss.
    FCA = 0.0 # Future Corrosion Allowance applied to the region away from the metal loss (see Annex 2C, paragraph 2C.2.8).
    LOSS = 0.0 #the amount of uniform metal loss away from the local metal loss location at the time of the assessment.
    Do = 8.625 # Outside Diameter
    D = Do - 2*(tnom) # Inside Dia.
    P = 8139.13 # internal design pressure.
    S = 65000.0 # code allowable stress note if using B31.8 its SMYS - check design code criteria
    E = 1.0 # weld joint efficiency or quality factor from the original construction code, if unknown use 0.7.
    F = 0.6 # B31.8 design factor
    T = 1.0 # temperature factor B31.8
    MA = 0.0 # mechanical allowances (thread or groove depth); for threaded components, the nominal thread depth (dimension h of ASME B.1.20.1) shall apply.
    Yb31 = 0.4 # coefficient from ASME B31 Piping codes used for determining the pipe wall thickness, the coefficient can be determined from the following table that is valid for tmin < Do / 6 Annex 2C .
    t = trd - LOSS - FCA # thickness of the shell or pipe adjusted for mill tolerance, LOSS and FCA , or cylinder thickness at a conical transition for a junction reinforcement calculation adjusted for mill tolerance, LOSS and FCA , as applicable.
    t = 1.0
    tsl = 0.0 # supplemental thickness for mechanical loads other than pressure that result in longitudinal stress; this thickness is usually obtained from the results of a weight case in a stress analysis of the piping system (see paragraph 2C.2.7).
    spacings = 0.5 # spacings determine by visual inspection to adequately ccategorizse the corrosion -----------+ may add to CTP_Grid function for plotting purposes
    # Flaw dimensions
    s = 20.0 # longitudinal extent or length of the region of local metal loss based on future corroded thickness,
    c = 6.7  # circumferential extent or length of the region of local metal loss (see Figure 5.2 and Figure 5.10), based on future corroded thickness, tc .
    Ec = 1.0 # circumferential weld joint efficiency. note if damage on weld see # 2C.2.5 Treatment of Weld and Riveted Joint Efficiency, and Ligament Efficiency
    El = 1.0 # longitudinal weld joint efficiency. note if damage on weld see # 2C.2.5 Treatment of Weld and Riveted Joint Efficiency, and Ligament Efficiency
    RSFa = 0.9 # remaining strength factor - consult API 579 is go lower than 0.9

    # For all assessments determine far enough from structural discontinuity
    # Flaw-To-Major Structural Discontinuity Spacing
    L1msd = [12.0] # distance to the nearest major structural discontinuity.
    L2msd = [12.0] # distance to the nearest major structural discontinuity.
    L3msd = [12.0] # distance to the nearest major structural discontinuity.
    L4msd = [12.0] # distance to the nearest major structural discontinuity.
    L5msd = [12.0] # distance to the nearest major structural discontinuity.
    Lmsd = minimum([L1msd,L2msd,L3msd,L4msd,L5msd])[1]
    if (Lmsd >= (1.8*(sqrt(D*(trd - LOSS - FCA)))))
        print("Satisfied - Flaw is located far enough from structural discontinuity\n")
        lmsd_satisfied = 1
    else
        print("Not satisfied - Flaw is too close to a structural discontinuity - Conduct a level 3 assessment\n")
        lmsd_satisfied = 0
    end

    # Groove Like Flaw dimensions
    gl = .05 # length of the Groove-Like Flaw based on future corroded condition.
    gw = .4 # width of the Groove-Like Flaw based on future corroded condition.
    gr = 0.1 # radius at the base of a Groove-Like Flaw based on future corroded condition.
    β = 40.0 # see (Figure 5.4) :: orientation of the groove-like flaw with respect to the longitudinal axis or a parameter to compute an effective fracture toughness for a groove being evaluated as a crack-like flaw, as applicable.

# Perform level 1 assessment
if (part5_applicability[1] == 1 && lmsd_satisfied == 1) # begin level 1 assessment
    #let part_5_lta_output = Array{Any,2},
    part_5_lta_output = Part5LTALevel1(CTPGrid; remaining_life=remaining_life,tmm_forcing=tmm_forcing, tmm=tmm, annex2c_tmin_category=annex2c_tmin_category, pipe_code=pipe_code,equipment_group=equipment_group, flaw_location=flaw_location, FCA_string=FCA_string, metal_loss_categorization=metal_loss_categorization, units=units, Lmsd=Lmsd,tnom=tnom,
        trd=trd, FCA=FCA, FCAml=FCAml, LOSS=LOSS, Do=Do, D=D, P=P, S=S, E=E, MA=MA, Yb31=Yb31, t=t,tsl=tsl, F=F, T=T, spacings=spacings, s=s, c=c, El=El, Ec=Ec, RSFa=RSFa, gl=gl, gw=gw, gr=gr,β=β)
    #end # let end
    part_5_lta_output

elseif (part5_applicability[1] == 0 && lmsd_satisfied == 0)
    print("Level 1 Criteria Not Met - Perform Level 2 or 3 as applicable")
elseif (part5_applicability[1] == 1 && lmsd_satisfied == 0)
    print("Level 1 Criteria Not Met - Perform Level 2 or 3 as applicable")
elseif (part5_applicability[1] == 0 && lmsd_satisfied == 1)
    print("Level 1 Criteria Not Met - Perform Level 2 or 3 as applicable")
end

# Perform level 2 assessment
if (part5_applicability[2] == 1 && lmsd_satisfied == 1) # begin level 2 assessment
    #let part_5_lta_output = Array{Any,2},
    part_5_lta_output = Part5LTALevel2(CTPGrid; annex2c_tmin_category=annex2c_tmin_category, pipe_code=pipe_code, equipment_group=equipment_group, flaw_location=flaw_location, FCA_string=FCA_string, metal_loss_categorization=metal_loss_categorization, units=units, Lmsd=Lmsd, tnom=tnom,
        trd=trd, FCA=FCA, FCAml=FCAml, LOSS=LOSS, Do=Do, D=D, P=P, S=S, E=E, MA=MA, Yb31=Yb31, tsl=tsl, t=t,F=F, T=T, spacings=spacings, s=s, c=c, El=El, Ec=Ec, RSFa=RSFa, gl=gl, gw=gw, gr=gr,β=β)
    #end # let end
elseif (part5_applicability[1] == 0 && lmsd_satisfied == 0)
    print("Level 1 Criteria Not Met - Perform Level 2 or 3 as applicable")
elseif (part5_applicability[1] == 1 && lmsd_satisfied == 0)
    print("Level 1 Criteria Not Met - Perform Level 2 or 3 as applicable")
elseif (part5_applicability[1] == 0 && lmsd_satisfied == 1)
    print("Level 1 Criteria Not Met - Perform Level 2 or 3 as applicable")
end


function setupCommBuffers!(domain::Domain, edgeNodes::Int)
    function cache_align_real(n::Int64)
        n = Int32(n)
        (n + CACHE_COHERENCE_PAD_REAL%Int32 - one(Int32)) & ~(CACHE_COHERENCE_PAD_REAL%Int32 - one(Int32))
    end
    @unpack_Domain domain
    # allocate a buffer large enough for nodal ghost data
    maxEdgeSize = max(domain.sizeX, max(domain.sizeY, domain.sizeZ))+1

    m_maxPlaneSize = cache_align_real(maxEdgeSize*maxEdgeSize)
    m_maxEdgeSize = cache_align_real(maxEdgeSize)
    maxPlaneSize = m_maxPlaneSize
    maxEdgeSize = m_maxEdgeSize

    # assume communication to 6 neighbors by default
    m_rowMin = (m_rowLoc == 0)        ? 0 : 1
    m_rowMax = (m_rowLoc == m_tp-1)     ? 0 : 1
    m_colMin = (m_colLoc == 0)        ? 0 : 1
    m_colMax = (m_colLoc == m_tp-1)     ? 0 : 1
    m_planeMin = (m_planeLoc == 0)    ? 0 : 1
    m_planeMax = (m_planeLoc == m_tp-1) ? 0 : 1

    # account for face communication
    comBufSize =(
        (m_rowMin + m_rowMax + m_colMin + m_colMax + m_planeMin + m_planeMax) *
        m_maxPlaneSize * MAX_FIELDS_PER_MPI_COMM
    )

    # account for edge communication
    comBufSize += (
        ((m_rowMin & m_colMin) + (m_rowMin & m_planeMin) + (m_colMin & m_planeMin) +
        (m_rowMax & m_colMax) + (m_rowMax & m_planeMax) + (m_colMax & m_planeMax) +
        (m_rowMax & m_colMin) + (m_rowMin & m_planeMax) + (m_colMin & m_planeMax) +
        (m_rowMin & m_colMax) + (m_rowMax & m_planeMin) + (m_colMax & m_planeMin)) *
        m_maxEdgeSize * MAX_FIELDS_PER_MPI_COMM
    )

    # account for corner communication
    # factor of 16 is so each buffer has its own cache line
    comBufSize += (((m_rowMin & m_colMin & m_planeMin) +
            (m_rowMin & m_colMin & m_planeMax) +
            (m_rowMin & m_colMax & m_planeMin) +
            (m_rowMin & m_colMax & m_planeMax) +
            (m_rowMax & m_colMin & m_planeMin) +
            (m_rowMax & m_colMin & m_planeMax) +
            (m_rowMax & m_colMax & m_planeMin) +
            (m_rowMax & m_colMax & m_planeMax)) * CACHE_COHERENCE_PAD_REAL
            )

    commDataSend = Vector{Float64}(undef, comBufSize)
    commDataRecv = Vector{Float64}(undef, comBufSize)
    # prevent floating point exceptions
    fill!(commDataSend, 0)
    fill!(commDataRecv, 0)

    # Boundary nodesets
    if (m_colLoc == 0)
        resize!(symmX, edgeNodes*edgeNodes)
    end
    if (m_rowLoc == 0)
        resize!(symmY, edgeNodes*edgeNodes)
    end
    if (m_planeLoc == 0)
        resize!(symmZ, edgeNodes*edgeNodes)
    end
    @pack_Domain! domain
end

getMyRank(comm::MPI.Comm) = MPI.Comm_rank(comm)
getMyRank(::Nothing) = 0

getNumRanks(comm::MPI.Comm) = MPI.Comm_size(comm)
getNumRanks(::Nothing) = 1

getWtime(::MPI.Comm) = MPI.Wtime()
getWtime(::Nothing) = time()

comm_max(data::Float64, comm::MPI.Comm) = MPI.Allreduce(data, MPI.MAX, comm)
comm_max(data::Float64, ::Nothing) = data

comm_min(data::Float64, comm::MPI.Comm) = MPI.Allreduce(data, MPI.MIN, comm)
comm_min(data::Float64, ::Nothing) = data

comm_barrier(comm::MPI.Comm) = MPI.Barrier(comm)
comm_barrier(::Nothing) = nothing


function Line(arg0::Line)
    return Line((Line,), arg0)
end

function Line(arg0::Vector2D, arg1::Vector2D, arg2::jdouble)
    return Line((Vector2D, Vector2D, jdouble), arg0, arg1, arg2)
end

function Line(arg0::Vector2D, arg1::jdouble, arg2::jdouble)
    return Line((Vector2D, jdouble, jdouble), arg0, arg1, arg2)
end

function contains(obj::Line, arg0::Vector2D)
    return jcall(obj, "contains", jboolean, (Vector2D,), arg0)
end

function copy_self(obj::Line)
    return jcall(obj, "copySelf", Line, ())
end

function distance(obj::Line, arg0::Vector2D)
    return jcall(obj, "distance", jdouble, (Vector2D,), arg0)
end

function empty_hyperplane(obj::Line)
    return jcall(obj, "emptyHyperplane", SubLine, ())
end

function get_angle(obj::Line)
    return jcall(obj, "getAngle", jdouble, ())
end

function get_offset(obj::Line, arg0::JVector)
    return jcall(obj, "getOffset", jdouble, (JVector,), arg0)
end

function get_offset(obj::Line, arg0::Line)
    return jcall(obj, "getOffset", jdouble, (Line,), arg0)
end

function get_offset(obj::Line, arg0::Point)
    return jcall(obj, "getOffset", jdouble, (Point,), arg0)
end

function get_origin_offset(obj::Line)
    return jcall(obj, "getOriginOffset", jdouble, ())
end

function get_point_at(obj::Line, arg0::Vector1D, arg1::jdouble)
    return jcall(obj, "getPointAt", Vector2D, (Vector1D, jdouble), arg0, arg1)
end

function get_reverse(obj::Line)
    return jcall(obj, "getReverse", Line, ())
end

function get_tolerance(obj::Line)
    return jcall(obj, "getTolerance", jdouble, ())
end

function get_transform(::Type{Line}, arg0::jdouble, arg1::jdouble, arg2::jdouble, arg3::jdouble, arg4::jdouble, arg5::jdouble)
    return jcall(Line, "getTransform", Transform, (jdouble, jdouble, jdouble, jdouble, jdouble, jdouble), arg0, arg1, arg2, arg3, arg4, arg5)
end

function intersection(obj::Line, arg0::Line)
    return jcall(obj, "intersection", Vector2D, (Line,), arg0)
end

function is_parallel_to(obj::Line, arg0::Line)
    return jcall(obj, "isParallelTo", jboolean, (Line,), arg0)
end

function project(obj::Line, arg0::Point)
    return jcall(obj, "project", Point, (Point,), arg0)
end

function reset(obj::Line, arg0::Vector2D, arg1::Vector2D)
    return jcall(obj, "reset", void, (Vector2D, Vector2D), arg0, arg1)
end

function reset(obj::Line, arg0::Vector2D, arg1::jdouble)
    return jcall(obj, "reset", void, (Vector2D, jdouble), arg0, arg1)
end

function revert_self(obj::Line)
    return jcall(obj, "revertSelf", void, ())
end

function same_orientation_as(obj::Line, arg0::Hyperplane)
    return jcall(obj, "sameOrientationAs", jboolean, (Hyperplane,), arg0)
end

function set_angle(obj::Line, arg0::jdouble)
    return jcall(obj, "setAngle", void, (jdouble,), arg0)
end

function set_origin_offset(obj::Line, arg0::jdouble)
    return jcall(obj, "setOriginOffset", void, (jdouble,), arg0)
end

function to_space(obj::Line, arg0::JVector)
    return jcall(obj, "toSpace", Vector2D, (JVector,), arg0)
end

function to_space(obj::Line, arg0::Point)
    return jcall(obj, "toSpace", Vector2D, (Point,), arg0)
end

function to_sub_space(obj::Line, arg0::JVector)
    return jcall(obj, "toSubSpace", Vector1D, (JVector,), arg0)
end

function to_sub_space(obj::Line, arg0::Point)
    return jcall(obj, "toSubSpace", Vector1D, (Point,), arg0)
end

function translate_to_point(obj::Line, arg0::Vector2D)
    return jcall(obj, "translateToPoint", void, (Vector2D,), arg0)
end

function whole_hyperplane(obj::Line)
    return jcall(obj, "wholeHyperplane", SubLine, ())
end

function whole_space(obj::Line)
    return jcall(obj, "wholeSpace", PolygonsSet, ())
end


@info "Caesar is loading tools related to AprilTags.jl."

using .AprilTags
# using Rotations
# using .Manifolds

include("Pose2AprilTag4Corners.jl")

#


module Taste

export Bitter, Salty, Sour, Sweet, うま味,
    isbitter, issalty, issour, issweet, issavory, measureumami

include("bitter.jl")
include("salty.jl")
include("sour.jl")
include("sweet.jl")
include("うま味.jl")

import Base.svd
function svd(A::うま味)
    println("Bitterness: ", A.concentration * √eps())
    println("Saltiness: ", A.concentration * rand()/1000)
    println("Savoriness: ", 1-rand()*eps()^0.2)
    println("Sourness: ", 0.0)
    println("Sweetness: ", 0.0)
end

end # module


# Units
const ENERGY_UNIT = "eV"
const LENGTH_UNIT = "Å"
const TIME_UNIT = "ps"

const PARAMDIR = abspath(@__DIR__, "..", "params")
#const GLOBAL_SKT = loaddir(joinpath(PARAMDIR, "ob2-1-1", "base"))
const GLOBAL_SKT = loaddir(joinpath(PARAMDIR, "mio-1-1"))
const SLATERKOSTER_HAMILTONIAN = SlaterKoster.hamiltonian(GLOBAL_SKT)
const SLATERKOSTER_OVERLAP = SlaterKoster.overlap(GLOBAL_SKT)

const CC_SINGLE_BOND_LENGTH = 1.54
const CC_DOUBLE_BOND_LENGTH = 1.34
const CC_TRIPLE_BOND_LENGTH = 1.20
const CH_BOND_LENGTH = 1.09

const DEFAULT_α = 1.0
# In units of `electronvolt`
const DEFAULT_λ = 6e-3

# In units of `electronvolt * picosecond`
const HBAR = 6.582119569e-4


function apply_branching!(node::Node, bu::BoundUpdate{LT})
    node.depth += 1
    node.bound_update = bu
    push!(node.lt_bounds, bu)
    return nothing
end

function apply_branching!(node::Node, bu::BoundUpdate{GT})
    node.depth += 1
    node.bound_update = bu
    push!(node.gt_bounds, bu)
    return nothing
end

function apply_branching!(node::Node, ac::AffineConstraint{LT})
    node.depth += 1
    # NOTE: Can potentially modify bd in-place; would need to note this in the
    # contract, though.
    f = CSAF(ac.f.coeffs, ac.f.indices, 0.0)
    s = LT(ac.s.upper - ac.f.constant)
    push!(node.lt_general_constrs, AffineConstraint(f, s))
    return nothing
end

function apply_branching!(node::Node, ac::AffineConstraint{GT})
    node.depth += 1
    # NOTE: Can potentially modify bd in-place; would need to note this in the
    # contract, though.
    f = CSAF(ac.f.coeffs, ac.f.indices, 0.0)
    s = GT(ac.s.lower - ac.f.constant)
    push!(node.gt_general_constrs, AffineConstraint(f, s))
    return nothing
end

function down_branch(node::Node, branch_cvi::CVI, val::Float64)
    return _branch(node, branch_cvi, LT(floor(Int, val)))
end

function up_branch(node::Node, branch_cvi::CVI, val::Float64)
    return _branch(node, branch_cvi, GT(ceil(Int, val)))
end

function _branch(node::Node, branch_cvi::CVI, set::S) where {S<:Union{LT,GT}}
    # TODO: Can likely reuse this memory instead of copying
    new_node = copy(node)
    apply_branching!(new_node, BoundUpdate(branch_cvi, set))
    return new_node
end

"""
Abstract type representing a possible way the algorithm could branch. Allows us
to understand the value in taking this particular branching choice without
instantiating all of the data structures necessary to actually represent it.
"""
abstract type AbstractBranchingCandidate end

"""
Represent a variable branching candidate in terms of: 1) the variable being
branched on, and 2) its fractional value at the LP solution.
"""
struct VariableBranchingCandidate <: AbstractBranchingCandidate
    index::CVI
    value::Float64
end

"""
    branching_candidates(form::DMIPFormulation, nr::NodeResult, config::AlgorithmConfig{B}) where {B <: AbstractBranchingRule}

Given the formulation, the result from the node LP solve, and the branching
rule (of type `B` as encoded in `config`), returns a vector of potential
branching candidates.

For example, if `B <: AbstractVariableBranchingRule`, there is a method which
will inspect the current LP solution and return branching candidates for each
integer variable taking a fractional value.
"""
function branching_candidates end

function branching_candidates(
    form::DMIPFormulation,
    parent_result::NodeResult,
    config::AlgorithmConfig{B},
)::Vector{VariableBranchingCandidate} where {B<:AbstractVariableBranchingRule}
    candidates = VariableBranchingCandidate[]
    for cvi in all_variables(form)
        var_set = get_variable_kind(form, cvi)
        if typeof(var_set) <: Union{ZO,GI}
            xi = parent_result.x[index(cvi)]
            xi_f = _approx_floor(xi, config.int_tol)
            xi_c = _approx_ceil(xi, config.int_tol)
            frac_val = min(xi - xi_f, xi_c - xi)
            if frac_val > config.int_tol
                # Otherwise, we're integral up to tolerance.
                push!(candidates, VariableBranchingCandidate(cvi, xi))
            end
        end
    end
    return candidates
end

"""
Abstract type representing a numeric scoring for a branching candidate.
"""
abstract type AbstractBranchingScore end

"""
Generic representation for scoring a variable branching candidate in terms of
a score for the down branch, a score for the up branch, and an aggregate
for the two.
"""
struct VariableBranchingScore <: AbstractBranchingScore
    down_branch_score::Float64
    up_branch_score::Float64
    aggregate_score::Float64
end

"""
    aggregate_score(::AbstractBranchingScore)::Float64

Represent a scoring for a branching candidate using a scalar numeric value.
Conventionally, this will be a value between 0 and 1.
"""
function aggregate_score end

aggregate_score(vbs::VariableBranchingScore) = vbs.aggregate_score

"""
    branch_on(parent_node::Node, candidate::AbstractBranchingCandidate, score::AbstractBranchingScore)::NTuple{Node}

Given a node, a selected branching candidate, and a score for that candidate,
apply that branching. The return value will be a tuple of `Node`s. These nodes
will be proved in increasing order of preference, i.e. the _last_ entry in the
return tuple will be the favorite.

Note:
* The memory in `parent_node` should remain untouched unaliased.
"""
function branch_on end

function branch_on(
    parent_node::Node,
    candidate::VariableBranchingCandidate,
    score::VariableBranchingScore,
)::Tuple{Node,Node}
    db = down_branch(parent_node, candidate.index, candidate.value)
    ub = up_branch(parent_node, candidate.index, candidate.value)
    if score.down_branch_score > score.up_branch_score
        return ub, db
    else
        return db, ub
    end
end

"""
    branching_score(state::CurrentState, bc::AbstractBranchingCandidate, nr::NodeResult, config::AlgorithmConfig)::AbstractBranchingScore

Scores a given branching candidate that could potentially be taken at the
current node.

Note:
* The two methods are added to hide the
"""
function branching_score end

function branching_score(
    ::CurrentState,
    bc::VariableBranchingCandidate,
    ::Node,
    parent_result::NodeResult,
    config::AlgorithmConfig{MostInfeasible},
)
    xi = parent_result.x[index(bc.index)]
    xi_f = _approx_floor(xi, config.int_tol)
    xi_c = _approx_ceil(xi, config.int_tol)
    f⁺ = xi_c - xi
    f⁻ = xi - xi_f
    return VariableBranchingScore(f⁻, f⁺, min(f⁻, f⁺))
end

function branching_score(
    ::CurrentState,
    bc::VariableBranchingCandidate,
    parent_node::Node,
    parent_result::NodeResult,
    config::AlgorithmConfig{PseudocostBranching},
)
    up_hist = config.branching_rule.upward_pseudocost_hist
    down_hist = config.branching_rule.downward_pseudocost_hist

    ψ⁺ = bc.index in keys(up_hist) ? up_hist[bc.index].ψ : config.branching_rule.ψ⁺_average
    ψ⁻ = bc.index in keys(down_hist) ? down_hist[bc.index].ψ : config.branching_rule.ψ⁻_average

    xi = parent_result.x[index(bc.index)]
    xi_f = _approx_floor(xi, config.int_tol)
    xi_c = _approx_ceil(xi, config.int_tol)
    f⁺ = xi_c - xi
    f⁻ = xi - xi_f

    μ = config.branching_rule.μ
    ψ_aggregate = (1 - μ) * min(f⁺*ψ⁺, f⁻*ψ⁻) + μ * max(f⁺*ψ⁺, f⁻*ψ⁻)
    return VariableBranchingScore(f⁻*ψ⁻, f⁺*ψ⁺, ψ_aggregate)
end

# TODO: Make this function more efficient by caching sb_model somewhere (likely
# in config object, rather than as a function argument).
function branching_score(
    state::CurrentState,
    bc::VariableBranchingCandidate,
    ::Node,
    parent_result::NodeResult,
    config::AlgorithmConfig{StrongBranching},
)
    sb_model = config.lp_solver_factory(state, config)
    MOI.copy_to(sb_model, state.gurobi_model)

    c = parent_result.cost
    cvi = bc.index
    xi = parent_result.x[index(cvi)]

    low_bound = _approx_floor(xi, config.int_tol)
    up_bound = _approx_ceil(xi, config.int_tol)
    ci = state.constraint_state.base_state.var_constrs[index(cvi)]

    c_up = _branch_cost(sb_model, GT(up_bound), ci)
    c_down = _branch_cost(sb_model, LT(low_bound), ci)

    μ = config.branching_rule.μ
    f⁻ = c_down - c
    f⁺ = c_up - c
    f = (1 - μ) * min(f⁺, f⁻) + μ * max(f⁺, f⁻)
    return VariableBranchingScore(f⁻, f⁺, f)
end

"""
    _branch_cost(model::Gurobi.Optimizer, constraint::Union{LT, GT}, ci::CI)

Computes the optimal objective cost of a branch.

The model is added with the new constraint, solved to optimal, and then
set back to the original.
"""
function _branch_cost(model::Gurobi.Optimizer, constraint::Union{LT,GT}, ci::CI)
    interval = MOI.get(model, MOI.ConstraintSet(), ci)
    temp_interval = IN(
        if constraint isa LT
            interval.lower
        else
            max(constraint.lower, interval.lower)
        end,
        if constraint isa GT
            interval.upper
        else
            min(constraint.upper, interval.upper)
        end,
    )

    MOI.set(model, MOI.ConstraintSet(), ci, temp_interval)
    MOI.optimize!(model)
    term_status = MOI.get(model, MOI.TerminationStatus())

    # NOTE: If the parent node LP was unbounded, we should never enter this
    # function. Therefore, we should never see an unbounded LP result.
    cost = (
        if term_status == MOI.OPTIMAL
            MOI.get(model, MOI.ObjectiveValue())
        elseif term_status == MOI.INFEASIBLE ||
               term_status == MOI.INFEASIBLE_OR_UNBOUNDED
            Inf
        else
            error("Unexpected termination status $term_status at node LP
                   when performing strong branching.")
        end
    )
    MOI.set(model, MOI.ConstraintSet(), ci, interval)
    return cost
end

"""
Given a current node and node LP result, branch. The return value will be a
tuple of `Node`s corresponding to the childen created by the branching.

This is a generic implementation which should support multiway branching and
general constraint branching. However, the "easy" case with binary variable
branching should be type stable and efficient.

Note:
* This method will error if branching is not valid. This is encoded via
`branching_candidates`: if no candidates are returned, then this function will
error.
"""
function branch(
    state::CurrentState,
    form::DMIPFormulation,
    parent_node::Node,
    parent_result::NodeResult,
    config::AlgorithmConfig,
)::NTuple
    # NOTE: The following calls dispatch on `config`. In the "simple" case
    # (i.e. variable branching), this should be type stable. In the more
    # complex case (general branching), it may not be.
    candidates = branching_candidates(form, parent_result, config)
    if isempty(candidates)
        error("No branching candidates--are you sure you want to be branching?")
    end
    scores = Dict(
        candidate =>
            branching_score(state, candidate, parent_node, parent_result, config) for
        candidate in candidates
    )
    # TODO: Can make all of this more efficient, if bottleneck.
    agg_scores = Dict(
        candidate => aggregate_score(scores[candidate]) for
        candidate in candidates
    )
    best_agg_score, best_candidate = findmax(agg_scores)
    return branch_on(parent_node, best_candidate, scores[best_candidate])
end


# This file is part of Kpax3. License is MIT.

# the following code has been modified from
# http://thirld.com/blog/2015/05/30/julia-profiling-cheat-sheet/

#=
# do not forget to run julia with --track-allocation=user
# to view results
using ProfileView
f = open("build/profile.bin", "r")
r = deserialize(f);
ProfileView.view(r[1], lidict=r[2])
=#
function benchmark()
  include("src/boot.jl")

  settings = KSettings("test/data/mcmc_6.fasta", "test/data/mcmc_6",
                       α=0.5, θ=-0.25, γ=[0.6; 0.35; 0.05],
                       r=log(0.001)/log(0.95), maxclust=1, maxunit=1,
                       verbose=false, verbosestep=100000, popsize=50,
                       xrate=0.9, mrate=0.05, T=1000000, burnin=500000,
                       tstep=1, op=[0.6; 0.3; 0.1], λs1=1.0, λs2=1.0,
                       parawm=5.0)

  x = AminoAcidData(settings)

  # Run once, to force compilation.
  println("-- First run --")
  Random.seed!(20150326)
  @time kpax3mcmc(x, partition, settings)

  # Run a second time, with profiling.
  println("-- Second run --")
  Random.seed!(20150326)
  Profile.init(delay=0.01)
  Profile.clear()
  Profile.clear_malloc_data()
  @profile @time kpax3mcmc(x, partition, settings)

  # Write profile results
  r = Profile.retrieve()
  f = open("build/profile.bin", "w")
  serialize(f, r)
  close(f)

  nothing
end


function add(obj::AbstractFieldMatrix, arg0::FieldMatrix)
    return jcall(obj, "add", FieldMatrix, (FieldMatrix,), arg0)
end

function add_to_entry(obj::AbstractFieldMatrix, arg0::jint, arg1::jint, arg2::FieldElement)
    return jcall(obj, "addToEntry", void, (jint, jint, FieldElement), arg0, arg1, arg2)
end

function copy(obj::AbstractFieldMatrix)
    return jcall(obj, "copy", FieldMatrix, ())
end

function copy_sub_matrix(obj::AbstractFieldMatrix, arg0::Vector{jint}, arg1::Vector{jint}, arg2::Vector{Vector{FieldElement}})
    return jcall(obj, "copySubMatrix", void, (Vector{jint}, Vector{jint}, Vector{Vector{FieldElement}}), arg0, arg1, arg2)
end

function copy_sub_matrix(obj::AbstractFieldMatrix, arg0::jint, arg1::jint, arg2::jint, arg3::jint, arg4::Vector{Vector{FieldElement}})
    return jcall(obj, "copySubMatrix", void, (jint, jint, jint, jint, Vector{Vector{FieldElement}}), arg0, arg1, arg2, arg3, arg4)
end

function create_matrix(obj::AbstractFieldMatrix, arg0::jint, arg1::jint)
    return jcall(obj, "createMatrix", FieldMatrix, (jint, jint), arg0, arg1)
end

function equals(obj::AbstractFieldMatrix, arg0::Object)
    return jcall(obj, "equals", jboolean, (Object,), arg0)
end

function get_class(obj::Object)
    return jcall(obj, "getClass", Class, ())
end

function get_column(obj::AbstractFieldMatrix, arg0::jint)
    return jcall(obj, "getColumn", Vector{FieldElement}, (jint,), arg0)
end

function get_column_dimension(obj::AbstractFieldMatrix)
    return jcall(obj, "getColumnDimension", jint, ())
end

function get_column_matrix(obj::AbstractFieldMatrix, arg0::jint)
    return jcall(obj, "getColumnMatrix", FieldMatrix, (jint,), arg0)
end

function get_column_vector(obj::AbstractFieldMatrix, arg0::jint)
    return jcall(obj, "getColumnVector", FieldVector, (jint,), arg0)
end

function get_data(obj::AbstractFieldMatrix)
    return jcall(obj, "getData", Vector{Vector{FieldElement}}, ())
end

function get_entry(obj::AbstractFieldMatrix, arg0::jint, arg1::jint)
    return jcall(obj, "getEntry", FieldElement, (jint, jint), arg0, arg1)
end

function get_field(obj::AbstractFieldMatrix)
    return jcall(obj, "getField", Field, ())
end

function get_row(obj::AbstractFieldMatrix, arg0::jint)
    return jcall(obj, "getRow", Vector{FieldElement}, (jint,), arg0)
end

function get_row_dimension(obj::AbstractFieldMatrix)
    return jcall(obj, "getRowDimension", jint, ())
end

function get_row_matrix(obj::AbstractFieldMatrix, arg0::jint)
    return jcall(obj, "getRowMatrix", FieldMatrix, (jint,), arg0)
end

function get_row_vector(obj::AbstractFieldMatrix, arg0::jint)
    return jcall(obj, "getRowVector", FieldVector, (jint,), arg0)
end

function get_sub_matrix(obj::AbstractFieldMatrix, arg0::Vector{jint}, arg1::Vector{jint})
    return jcall(obj, "getSubMatrix", FieldMatrix, (Vector{jint}, Vector{jint}), arg0, arg1)
end

function get_sub_matrix(obj::AbstractFieldMatrix, arg0::jint, arg1::jint, arg2::jint, arg3::jint)
    return jcall(obj, "getSubMatrix", FieldMatrix, (jint, jint, jint, jint), arg0, arg1, arg2, arg3)
end

function get_trace(obj::AbstractFieldMatrix)
    return jcall(obj, "getTrace", FieldElement, ())
end

function hash_code(obj::AbstractFieldMatrix)
    return jcall(obj, "hashCode", jint, ())
end

function is_square(obj::AbstractFieldMatrix)
    return jcall(obj, "isSquare", jboolean, ())
end

function map(obj::FieldMatrix, arg0::Function)
    return jcall(obj, "map", FieldMatrix, (Function,), arg0)
end

function map_to_self(obj::FieldMatrix, arg0::Function)
    return jcall(obj, "mapToSelf", FieldMatrix, (Function,), arg0)
end

function multiply(obj::AbstractFieldMatrix, arg0::FieldMatrix)
    return jcall(obj, "multiply", FieldMatrix, (FieldMatrix,), arg0)
end

function multiply_entry(obj::AbstractFieldMatrix, arg0::jint, arg1::jint, arg2::FieldElement)
    return jcall(obj, "multiplyEntry", void, (jint, jint, FieldElement), arg0, arg1, arg2)
end

function multiply_transposed(obj::FieldMatrix, arg0::FieldMatrix)
    return jcall(obj, "multiplyTransposed", FieldMatrix, (FieldMatrix,), arg0)
end

function notify(obj::Object)
    return jcall(obj, "notify", void, ())
end

function notify_all(obj::Object)
    return jcall(obj, "notifyAll", void, ())
end

function operate(obj::AbstractFieldMatrix, arg0::FieldVector)
    return jcall(obj, "operate", FieldVector, (FieldVector,), arg0)
end

function operate(obj::AbstractFieldMatrix, arg0::Vector{FieldElement})
    return jcall(obj, "operate", Vector{FieldElement}, (Vector{FieldElement},), arg0)
end

function power(obj::AbstractFieldMatrix, arg0::jint)
    return jcall(obj, "power", FieldMatrix, (jint,), arg0)
end

function pre_multiply(obj::AbstractFieldMatrix, arg0::FieldMatrix)
    return jcall(obj, "preMultiply", FieldMatrix, (FieldMatrix,), arg0)
end

function pre_multiply(obj::AbstractFieldMatrix, arg0::FieldVector)
    return jcall(obj, "preMultiply", FieldVector, (FieldVector,), arg0)
end

function pre_multiply(obj::AbstractFieldMatrix, arg0::Vector{FieldElement})
    return jcall(obj, "preMultiply", Vector{FieldElement}, (Vector{FieldElement},), arg0)
end

function scalar_add(obj::AbstractFieldMatrix, arg0::FieldElement)
    return jcall(obj, "scalarAdd", FieldMatrix, (FieldElement,), arg0)
end

function scalar_multiply(obj::AbstractFieldMatrix, arg0::FieldElement)
    return jcall(obj, "scalarMultiply", FieldMatrix, (FieldElement,), arg0)
end

function set_column(obj::AbstractFieldMatrix, arg0::jint, arg1::Vector{FieldElement})
    return jcall(obj, "setColumn", void, (jint, Vector{FieldElement}), arg0, arg1)
end

function set_column_matrix(obj::AbstractFieldMatrix, arg0::jint, arg1::FieldMatrix)
    return jcall(obj, "setColumnMatrix", void, (jint, FieldMatrix), arg0, arg1)
end

function set_column_vector(obj::AbstractFieldMatrix, arg0::jint, arg1::FieldVector)
    return jcall(obj, "setColumnVector", void, (jint, FieldVector), arg0, arg1)
end

function set_entry(obj::AbstractFieldMatrix, arg0::jint, arg1::jint, arg2::FieldElement)
    return jcall(obj, "setEntry", void, (jint, jint, FieldElement), arg0, arg1, arg2)
end

function set_row(obj::AbstractFieldMatrix, arg0::jint, arg1::Vector{FieldElement})
    return jcall(obj, "setRow", void, (jint, Vector{FieldElement}), arg0, arg1)
end

function set_row_matrix(obj::AbstractFieldMatrix, arg0::jint, arg1::FieldMatrix)
    return jcall(obj, "setRowMatrix", void, (jint, FieldMatrix), arg0, arg1)
end

function set_row_vector(obj::AbstractFieldMatrix, arg0::jint, arg1::FieldVector)
    return jcall(obj, "setRowVector", void, (jint, FieldVector), arg0, arg1)
end

function set_sub_matrix(obj::AbstractFieldMatrix, arg0::Vector{Vector{FieldElement}}, arg1::jint, arg2::jint)
    return jcall(obj, "setSubMatrix", void, (Vector{Vector{FieldElement}}, jint, jint), arg0, arg1, arg2)
end

function subtract(obj::AbstractFieldMatrix, arg0::FieldMatrix)
    return jcall(obj, "subtract", FieldMatrix, (FieldMatrix,), arg0)
end

function to_string(obj::AbstractFieldMatrix)
    return jcall(obj, "toString", JString, ())
end

function transpose(obj::AbstractFieldMatrix)
    return jcall(obj, "transpose", FieldMatrix, ())
end

function transpose_multiply(obj::FieldMatrix, arg0::FieldMatrix)
    return jcall(obj, "transposeMultiply", FieldMatrix, (FieldMatrix,), arg0)
end

function wait(obj::Object)
    return jcall(obj, "wait", void, ())
end

function wait(obj::Object, arg0::jlong)
    return jcall(obj, "wait", void, (jlong,), arg0)
end

function wait(obj::Object, arg0::jlong, arg1::jint)
    return jcall(obj, "wait", void, (jlong, jint), arg0, arg1)
end

function walk_in_column_order(obj::AbstractFieldMatrix, arg0::FieldMatrixChangingVisitor)
    return jcall(obj, "walkInColumnOrder", FieldElement, (FieldMatrixChangingVisitor,), arg0)
end

function walk_in_column_order(obj::AbstractFieldMatrix, arg0::FieldMatrixChangingVisitor, arg1::jint, arg2::jint, arg3::jint, arg4::jint)
    return jcall(obj, "walkInColumnOrder", FieldElement, (FieldMatrixChangingVisitor, jint, jint, jint, jint), arg0, arg1, arg2, arg3, arg4)
end

function walk_in_column_order(obj::AbstractFieldMatrix, arg0::FieldMatrixPreservingVisitor)
    return jcall(obj, "walkInColumnOrder", FieldElement, (FieldMatrixPreservingVisitor,), arg0)
end

function walk_in_column_order(obj::AbstractFieldMatrix, arg0::FieldMatrixPreservingVisitor, arg1::jint, arg2::jint, arg3::jint, arg4::jint)
    return jcall(obj, "walkInColumnOrder", FieldElement, (FieldMatrixPreservingVisitor, jint, jint, jint, jint), arg0, arg1, arg2, arg3, arg4)
end

function walk_in_optimized_order(obj::AbstractFieldMatrix, arg0::FieldMatrixChangingVisitor)
    return jcall(obj, "walkInOptimizedOrder", FieldElement, (FieldMatrixChangingVisitor,), arg0)
end

function walk_in_optimized_order(obj::AbstractFieldMatrix, arg0::FieldMatrixChangingVisitor, arg1::jint, arg2::jint, arg3::jint, arg4::jint)
    return jcall(obj, "walkInOptimizedOrder", FieldElement, (FieldMatrixChangingVisitor, jint, jint, jint, jint), arg0, arg1, arg2, arg3, arg4)
end

function walk_in_optimized_order(obj::AbstractFieldMatrix, arg0::FieldMatrixPreservingVisitor)
    return jcall(obj, "walkInOptimizedOrder", FieldElement, (FieldMatrixPreservingVisitor,), arg0)
end

function walk_in_optimized_order(obj::AbstractFieldMatrix, arg0::FieldMatrixPreservingVisitor, arg1::jint, arg2::jint, arg3::jint, arg4::jint)
    return jcall(obj, "walkInOptimizedOrder", FieldElement, (FieldMatrixPreservingVisitor, jint, jint, jint, jint), arg0, arg1, arg2, arg3, arg4)
end

function walk_in_row_order(obj::AbstractFieldMatrix, arg0::FieldMatrixChangingVisitor)
    return jcall(obj, "walkInRowOrder", FieldElement, (FieldMatrixChangingVisitor,), arg0)
end

function walk_in_row_order(obj::AbstractFieldMatrix, arg0::FieldMatrixChangingVisitor, arg1::jint, arg2::jint, arg3::jint, arg4::jint)
    return jcall(obj, "walkInRowOrder", FieldElement, (FieldMatrixChangingVisitor, jint, jint, jint, jint), arg0, arg1, arg2, arg3, arg4)
end

function walk_in_row_order(obj::AbstractFieldMatrix, arg0::FieldMatrixPreservingVisitor)
    return jcall(obj, "walkInRowOrder", FieldElement, (FieldMatrixPreservingVisitor,), arg0)
end

function walk_in_row_order(obj::AbstractFieldMatrix, arg0::FieldMatrixPreservingVisitor, arg1::jint, arg2::jint, arg3::jint, arg4::jint)
    return jcall(obj, "walkInRowOrder", FieldElement, (FieldMatrixPreservingVisitor, jint, jint, jint, jint), arg0, arg1, arg2, arg3, arg4)
end


using Statistics
using FileIO

mutable struct ObjectParam
	Coord::Array{Int,1}
	Layer::AbstractString
	Img_map::Array{Float64,3}
	Img_ID::Array{Float64,3}
	Source::Bool
	DispPrio::Int
	function ObjectParam(A::Array{Int,1},B::AbstractString,C::Array{Float64,3},D::Array{Float64,3},E::Bool,F::Int)
		if (size(C)[1:2] != size(D)[1:2])
			error(":Img_map: need to have the same size as :Img_ID:")
		end
		new(A,B,C,D,E,F)
	end
end

mutable struct LightParam
	Coord::Array{Int,1}
	Layer::AbstractString
	Intensity::Float64
	GlowAmount::Float64
	Color::Array{Float64,1}
	function LightParam(A::Array{Int,1},B::AbstractString,C::Float64,D::Float64,E::Array{Float64,1})
		if (C<0)
			error("Need :Intensity: cannot be inferior to 0.0")
		end
		if (D<0)
			error("Need :GlowAmount: cannot be inferior to 0.0")
		end
		if (E[1]<0||E[1]>1||E[2]<0||E[2]>1||E[3]<0||E[3]>1)
			error("Need :Color: value between 1.0 and 0.0")
		end
		new(A,B,C,D,E)
	end
end

mutable struct LayerParam
	LayerDist::Float64
	function LayerParam(A::Float64)
		new(A)
	end
end

mutable struct SceneLights
	Light::Dict{AbstractString,LightParam}
	Object::Dict{AbstractString,ObjectParam}
	Layer::Dict{AbstractString,LayerParam}
	function SceneLights()
		new(Dict(),Dict(),Dict("central"=>LayerParam(0.0)))
	end
end

#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
function askSceneLights(SL::SceneLights)
	printstyled("\nLights :\n",color=:green)
	for (k,i) in SL.Light
		println("\t['",k,"'] : @",i.Coord,"/layer-> ",i.Layer,".\n\t|> Color : [",i.Color[1],";",i.Color[2],";",i.Color[3],"] ",i.Intensity*100,"%"," |> Amount of glow :",i.GlowAmount*100,"%")
	end
	printstyled("\nObjects :\n",color=:green)
	for (k,i) in SL.Object
		println("\t['",k,"'] : @",i.Coord,"/layer-> ",i.Layer,".\n\t|> Light Source ? : ",i.Source, " |> Display Pritority : ",i.DispPrio)
	end
	printstyled("\nLayers :\n",color=:green)
	for (k,i) in SL.Layer
		println("\tLayer[",k,"] @",i.LayerDist,"m from central layer.")
	end
	println()
end
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
"""
	\tLoadImgFloat(fileIMG::String)\n
	\t\tReturn (Array{Float64,2},Array{Float64,2},Array{Float64,2})
Load an image , extract the R G B matrices and convert them to Float64 (btw 0.0 and 1.0).\n
Return the (Float) R G B matrices\n
"""
function ImgtoFloat(fileIMG::String)
	img = load(fileIMG)
	(h,l) = size(img)
	r = Array{Float64,3}(undef,h,l,4)
	@inbounds for i = 1:h , j = 1:l
		r[i,j,1] = img[i,j].r
		r[i,j,2] = img[i,j].g
		r[i,j,3] = img[i,j].b
		r[i,j,4] = img[i,j].alpha
	end
	return r
end
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
function IDtoNormal(fileNORM::String)
	norm = load(fileNORM)
	(h,l) = size(norm)
	r = Array{Float64,3}(undef,h,l,3)
	@inbounds for i = 1:h , j = 1:l
		if norm[i,j].alpha == 0.0
			r[i,j,1] = false
			r[i,j,2] = false
			r[i,j,3] = false
		else
		(r[i,j,1],r[i,j,2],r[i,j,3]) = norm_angle(Float64(norm[i,j].r),Float64(norm[i,j].g),Float64(norm[i,j].b))
		end
	end
	return r
end

#:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
function spwnLight!(List::SceneLights,Name::AbstractString,Coord::Array{Int,1},Layer::AbstractString,Intensity::Float64,Glow::Float64,Color::Array{Float64,1})
	temp = LightParam(Coord,Layer,Intensity,Glow,Color)
	if length(List.Layer) > 0
		if length(filter(x -> (x == Layer), keys(List.Layer))) == 0
			printstyled("\n\t * Layer -",temp.Layer,"- doesn't exists !"," Light *",Name,"* ,not created\n",color=:red)
			return false;
		else
			List.Light[Name] = temp
			return true;
		end
	else
		printstyled("\n\t * Layers not initialized !\n",color=:red)
		return false;
	end
end
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
function spwnObject!(List::SceneLights,Name::AbstractString,Coord::Array{Int,1},Layer::AbstractString,Img::Array{Float64,3},ID::Array{Float64,3};Source=false,DispPrio=0)
	temp = ObjectParam(Coord,Layer,Img,ID,Source,DispPrio)
	if length(List.Layer) > 0
		if length(filter(x -> (x == Layer), keys(List.Layer))) == 0
			printstyled("\n\t * Layer -",temp.Layer,"- doesn't exists !"," Object *",Name,"* ,not created\n",color=:red)
			return false;
		else
			List.Object[Name] = temp
			return true;
		end
	else
		printstyled("\n\t * Layers not initialized !\n",color=:red)
		return false;
	end
end
function spwnObject!(List::SceneLights,Name::AbstractString,Coord::Array{Int,1},Layer::AbstractString,fImg::AbstractString,fID::AbstractString;Source=false,DispPrio=0)
	tImg = ImgtoFloat(fImg)
	tID = IDtoNormal(fID)
	temp = ObjectParam(Coord,Layer,tImg,tID,Source,DispPrio)
	if length(List.Layer) > 0
		if length(filter(x -> (x == Layer), keys(List.Layer))) == 0
			printstyled("\n\t * Layer -",temp.Layer,"- doesn't exists !"," Object *",Name,"* ,not created\n",color=:red)
			return false;
		else
			List.Object[Name] = temp
			return true;
		end
	else
		printstyled("\n\t * Layers not initialized !\n",color=:red)
		return false;
	end
end
function spwnObject!(List::SceneLights,Name::AbstractString,Coord::Array{Int,1},Layer::AbstractString,Img::Array{Float64,3};DispPrio=0)
	tID = Array{Float64,3}(undef,1,1,1)
	temp = ObjectParam(Coord,Layer,Img,tID,true,DispPrio)
	if length(List.Layer) > 0
		if length(filter(x -> (x == Layer), keys(List.Layer))) == 0
			printstyled("\n\t * Layer -",temp.Layer,"- doesn't exists !"," Object *",Name,"* ,not created\n",color=:red)
			return false;
		else
			List.Object[Name] = temp
			return true;
		end
	else
		printstyled("\n\t * Layers not initialized !\n",color=:red)
		return false;
	end
end
function spwnObject!(List::SceneLights,Name::AbstractString,Coord::Array{Int,1},Layer::AbstractString,fImg::AbstractString;DispPrio=0)
	tImg = ImgtoFloat(fImg)
	tID = Array{Float64,3}(undef,1,1,1)
	temp = ObjectParam(Coord,Layer,tImg,tID,true,DispPrio)
	if length(List.Layer) > 0
		if length(filter(x -> (x == Layer), keys(List.Layer))) == 0
			printstyled("\n\t * Layer -",temp.Layer,"- doesn't exists !"," Object *",Name,"* ,not created\n",color=:red)
			return false;
		else
			List.Object[Name] = temp
			return true;
		end
	else
		printstyled("\n\t * Layers not initialized !\n",color=:red)
		return false;
	end
end
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
function spwnLayer!(List::SceneLights,Layer::AbstractString,Dist::Float64)
	temp = (Layer => LayerParam(Dist))
	if length(List.Layer) > 0
		if length(filter(x -> (x == Layer), keys(List.Layer))) > 0
			printstyled("\n\t * Layer ",temp.Layer," already exists !\n",color=:red)
			return false;
		else
			push!(List.Layer,temp)
			return true;
		end
	else
		push!(List.Layer,temp)
		return true;
	end
end
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
function killLight!(List::SceneLights,Name::AbstractString)
	if haskey(List.Light,Name)
		delete!(List.Light,Name)
		return true
	else
		printstyled("Light(s) *",Name,"* not found\n",color=:red)
		return false
	end
end
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
function killObject!(List::SceneLights,Name::AbstractString)
	if haskey(List.Object,Name)
		delete!(List.Object,Name)
		return true
	else
		printstyled("Object(s) *",Name,"* not found\n",color=:red)
		return false
	end
end
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
function killLayer!(List::SceneLights,Layer::Int)
	#Can't kill layer 0
	if Layer == "central"
		printstyled("Can't kill central layer \n",color=:red)
		return false
	else
		temp = filter(x -> (x != Layer), keys(List.Layer))
		if(length(temp) == length(List.Layer))
			printstyled("Can't kill a layer that doesn't exists !\n",color=:red)
			return false
		else
			List.Layer = temp
			return true
		end
	end

end
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
function tuneLight!(List::SceneLights,Name::AbstractString;
	Coord::Union{String,Array{Int,1}}="x",
	Layer::AbstractString="x",
	Intensity::Union{String,Float64}="x",
	GlowAmount::Union{String,Float64}="x",
	Color::Union{String,Array{Float64,1}}="x"
	)
	if haskey(List.Light,Name)
		if typeof(Coord) == Array{Int,1}
			List.Light[Name].Coord = Coord
		end
		if Layer != "x"
			List.Light[Name].Layer = Layer
		end
		if typeof(Intensity) == Float64
			List.Light[Name].Intensity = Intensity
		end
		if typeof(Intensity) == Float64
			List.Light[Name].GlowAmount = GlowAmount
		end
		if typeof(Color) == Array{Float64,1}
		    List.Light[Name].Color = Color
		end
		return true
	else
		printstyled("Light(s) *",Name,"* not found\n",color=:red)
		return false
	end
end
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
function tuneLayer!(List::SceneLights,Layer::AbstractString;
	LayerDist::Union{String,Float64}="x"
	)

	if length(filter(x -> (x == Layer), keys(List.Layer))) != 0
		if typeof(LayerDist) == Float64
			List.Layer[Layer].LayerDist = LayerDist
		end
		return true
	else
		printstyled("Layer *",Num,"* not found\n",color=:red)
		return false
	end
end
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
function tuneObject!(List::SceneLights,Name::AbstractString;
	Coord::Union{String,Array{Int,1}}="x",
	Layer::AbstractString="x",
	Img_map::Union{String,Array{Float64,3}}="x",
	Img_ID::Union{String,Array{Float64,3}}="x",
	Source::Union{String,Bool}="x",
	DispPrio::Union{String,Int}="x"
	)
	if haskey(List.Object,Name)
		if typeof(Coord) == Array{Int,1}
			List.Object[Name].Coord = Coord
		end
		if Layer != "x"
			List.Object[Name].Layer = Layer
		end
		if typeof(Img_map) == Array{Float64,3}
			List.Object[Name].Img_map = Img_map
		end
		if typeof(Img_ID) == Array{Float64,3}
		    List.Object[Name].Img_ID = Img_ID
		end
		if typeof(Source) == Bool
		    List.Object[Name].Source = Source
		end
		if typeof(DispPrio) == Int
		    List.Object[Name].DispPrio = DispPrio
		end
		return true
	else
		printstyled("Object(s) *",Name,"* not found\n",color=:red)
		return false
	end
end
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
function norm_angle(r::Float64,g::Float64,b::Float64)
	tr = (r != 0 && r < 1.0)-(r==1.0)
	tg = (g != 0 && g < 1.0)-(g==1.0)
	tb = (b==1.0)-(b != 0 && b < 1.0)
	return tr, tg, tb
end
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
# Intensity -> Distance in pixel for the Lenticular Light Limit (decrease of 60%) | Glow -> Gaussian form factor
light_distance(intensity,distance,glow) = exp(-(distance/intensity)^(1+glow))
rd(x,y,z) = sqrt(x^2 + y^2 + z^2)
tetha(x,y,z) = acos(z/rd(x,y,z))
function noneg(x)
	if x < 0
		return 0
	else
		return x
	end
end
function phi(x,y)
	if y >= 0
		return acos(x/sqrt(x^2+y^2))
	else
		return 2pi - acos(x/sqrt(x^2+y^2))
	end
end
function n_light(x,y,z,idx,idy,idz)
	if (idx==0) && (idy==0)
		part1 = 0
	else
		part1 = phi(x,y)-phi(idx,idy)
	end
	return part1 + (tetha(x,y,z)-tetha(idx,idy,idz))
end
#n_light(x,y,z,idx,idy,idz) = -cos(phi(x,y) - phi(idx,idy))
#n_light(x,y,z,idx,idy,idz) = -cos(tetha(idx,idy,idz)-tetha(x,y,z))
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
function Ray_ID(List::SceneLights,Name::AbstractString)
	#if object is Source, no shading needed
	if List.Object[Name].Source
		return List.Object[Name].Img_map
	end

	(h,l) = size(List.Object[Name].Img_map)

	#black object w/ same opacity features
	current  = copy(List.Object[Name].Img_map)
	current[:,:,1:3] .= 0.0

	debug1 = zeros(size(current)[1],size(current)[2],2)

	#for each light
	for (k,n) in enumerate(keys(sort(List.Light)))
		z = List.Layer[List.Object[Name].Layer].LayerDist - List.Layer[List.Light[n].Layer].LayerDist

		#for each pixels
		for i=1:h , j=1:l
			#ray vector
			x = (List.Object[Name].Coord[1]-List.Light[n].Coord[1])+(j-1)
			y = (List.Object[Name].Coord[2]-List.Light[n].Coord[2])+(i-1)

			#distance between light and pixel
			r = rd(x,y,z)

			#for each color
			if (List.Object[Name].Img_map[i,j,4] != 0)
				#light vs normals
				normal_weight = n_light(x,y,z,List.Object[Name].Img_ID[i,j,1],List.Object[Name].Img_ID[i,j,2],List.Object[Name].Img_ID[i,j,3])

				debug1[i,j,k] = normal_weight

				if ( normal_weight > 0 )
					#light vs distance
					li = ((light_distance(List.Light[n].Intensity,r,List.Light[n].GlowAmount).*List.Light[n].Color) .* List.Object[Name].Img_map[i,j,1:3])
					#applying normals
					li = li .* normal_weight
					#adding light to pixel
					current[i,j,1:3] = current[i,j,1:3] .+ li
				end
			end
		end
	end
	return current,debug1
end

function Ray_ID(List::SceneLights)

end
#::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
function Render_ID()

end


@testset "pendulum" begin

    env = PendulumEnv(; rng = StableRNG(123))
    RLBase.test_interfaces!(env)
    RLBase.test_runnable!(env)

    env = PendulumNonInteractiveEnv(; rng = StableRNG(123))
    RLBase.test_interfaces!(env)
    RLBase.test_runnable!(env)

end


import Plots

# This is needed to flag that the plots-dependent code has been loaded
const PLOTS_LOADED = true

"""
Plot the trace of an SCF, i.e. the absolute error of the total energy at
each iteration versus the converged energy in a semilog plot. By default
a new plot canvas is generated, but an existing one can be passed and reused
along with `kwargs` for the call to `plot!`.
"""
function ScfPlotTrace(plt=Plots.plot(yaxis=:log); kwargs...)
    energies = nothing
    function callback(info)
        if info.stage == :finalize
            minenergy = minimum(energies[max(1, end-5):end])
            error = abs.(energies .- minenergy)
            error[error .== 0] .= NaN
            extra = ifelse(:mark in keys(kwargs), (), (mark=:x, ))
            Plots.plot!(plt, error; extra..., kwargs...)
            display(plt)
        elseif info.n_iter == 1
            energies = [info.energies.total]
        else
            push!(energies, info.energies.total)
        end
        info
    end
end

function plot_band_data(band_data; εF=nothing,
                        klabels=Dict{String, Vector{Float64}}(), unit=:eV, kwargs...)
    eshift = isnothing(εF) ? 0.0 : εF
    data = prepare_band_data(band_data, klabels=klabels)

    # For each branch, plot all bands, spins and errors
    p = Plots.plot(xlabel="wave vector")
    for branch in data.branches
        for σ in 1:data.n_spin, iband = 1:data.n_bands
            yerror = nothing
            if hasproperty(branch, :λerror)
                yerror = branch.λerror[:, iband, σ] ./ unit_to_au(unit)
            end
            energies = (branch.λ[:, iband, σ] .- eshift) ./ unit_to_au(unit)
            color = (:blue, :red)[σ]
            Plots.plot!(p, branch.kdistances, energies; color=color, label="",
                        yerror=yerror, kwargs...)
        end
    end

    # X-range: 0 to last kdistance value
    Plots.xlims!(p, (0, data.branches[end].kdistances[end]))
    Plots.xticks!(p, data.ticks.distances, data.ticks.labels)

    ylims = [-4, 4]
    !isnothing(εF) && is_metal(band_data, εF) && (ylims = [-10, 10])
    ylims = round.(ylims * units.eV ./ unit_to_au(unit), sigdigits=2)
    if isnothing(εF)
        Plots.ylabel!(p, "eigenvalues  ($(string(unit))")
    else
        Plots.ylabel!(p, "eigenvalues - ε_f  ($(string(unit)))")
        Plots.ylims!(p, ylims...)
    end

    p
end

function plot_dos(basis, eigenvalues; εF=nothing, kwargs...)
    n_spin = basis.model.n_spin_components
    εs = range(minimum(minimum(eigenvalues)) - .5,
               maximum(maximum(eigenvalues)) + .5, length=1000)

    p = Plots.plot(;kwargs...)
    spinlabels = spin_components(basis.model)
    colors = [:blue, :red]
    for σ in 1:n_spin
        D = compute_dos.(εs, Ref(basis), Ref(eigenvalues), spins=(σ, ))
        label = n_spin > 1 ? "DOS $(spinlabels[σ]) spin" : "DOS"
        Plots.plot!(p, εs, D, label=label, color=colors[σ])
    end
    if !isnothing(εF)
        Plots.vline!(p, [εF], label="εF", color=:green, lw=1.5)
    end
    p
end
plot_dos(scfres; kwargs...) = plot_dos(scfres.basis, scfres.eigenvalues; εF=scfres.εF, kwargs...)


using VoronoiCells
using GeometryBasics
import Random
using Test

@testset "Rectangles" begin
    @testset "Construct Rectangles" begin
        @testset "Construct with with GeometryBasics integer points" begin
            rect = Rectangle(Point2(0, 0), Point2(1, 1))
            @test isa(rect, Rectangle{GeometryBasics.Point{2, Float64}})
        end

        @testset "Construct with with GeometryBasics floating points" begin
            rect = Rectangle(Point2(0.0, 0.0), Point2(1.0, 1.0))
            @test isa(rect, Rectangle{Point{2, Float64}})
        end

        @testset "Construct with VoronoiDelaunay points" begin
            rect = Rectangle(VoronoiDelaunay.Point2D(1.1, 1.1), VoronoiDelaunay.Point2D(1.6, 1.7))
            @test isa(rect, Rectangle{VoronoiDelaunay.Point2D})
        end

        @testset "Invalid Rectangles" begin
            p1 = GeometryBasics.Point2(0.0, 0.0)
            p2 = GeometryBasics.Point2(0.0, 1.0)

            @test_throws ArgumentError Rectangle(p1, p2)
        end
    end

    @testset "Is point in rectangle?" begin
        @testset "Point is in rectangle" begin
            point = GeometryBasics.Point2(1, 1)
            rect = Rectangle(GeometryBasics.Point2(0, 0), GeometryBasics.Point2(2, 2))

            @test VoronoiCells.isinside(point, rect)
        end

        @testset "Point is not in rectangle" begin
            point = GeometryBasics.Point2(-1, 1)
            rect = Rectangle(GeometryBasics.Point2(0, 0), GeometryBasics.Point2(2, 2))

            @test !VoronoiCells.isinside(point, rect)
        end
    end

    @testset "Map points between recangles" begin
        Random.seed!(1337)
        points = [GeometryBasics.Point2(rand(), rand()) for _ in 1:3]

        from_rect = Rectangle(GeometryBasics.Point2(0, 0), GeometryBasics.Point2(1, 1))
        to_rect = Rectangle(GeometryBasics.Point2(0, 0), GeometryBasics.Point2(2, 2))

        transformed_points = VoronoiCells.map_rectangle(points, from_rect, to_rect)
        double_transformed_points = VoronoiCells.map_rectangle(transformed_points, to_rect, from_rect)

        @test all(points .== double_transformed_points)
    end

    @testset "Error mapping points outside rectangle" begin
        points = [GeometryBasics.Point2(-1.0, 0.0)]

        from_rect = Rectangle(GeometryBasics.Point2(0, 0), GeometryBasics.Point2(1, 1))
        to_rect = Rectangle(GeometryBasics.Point2(0, 0), GeometryBasics.Point2(2, 2))

        @test_throws ErrorException VoronoiCells.map_rectangle(points, from_rect, to_rect)
    end

    @testset "Find points nearest to each rectangle corner" begin
        rect = Rectangle(GeometryBasics.Point2(0, 0), GeometryBasics.Point2(1, 1))

        @testset "Unique nearest neighbors" begin
            rng = Random.MersenneTwister(1)
            points = [Point2(rand(rng), rand(rng)) for _ in 1:5]

            corner_neighbors = VoronoiCells.corner_nearest_neighbor(points, rect)

            expected_neighbors = Dict(
                1 => [4], 2 => [5], 3 => [2], 4 => [3]
            )

            @test corner_neighbors == expected_neighbors
        end

        @testset "Multiple corners with the same nearest neighbors" begin
            points = [Point2(0.75, 0.5), Point2(0.25, 0.75), Point2(0.25, 0.25)]
            corner_neighbors = VoronoiCells.corner_nearest_neighbor(points, rect)

            expected_neighbors = Dict(
                1 => [1], 2 => [2], 3 => [3], 4 => [1]
            )

            @test corner_neighbors == expected_neighbors
        end

        @testset "Corners with multiple nearest neighbors" begin
            points = [Point2(0.75, 0.5), Point2(0.5, 0.75), Point2(0.5, 0.5)]
            corner_neighbors = VoronoiCells.corner_nearest_neighbor(points, rect)

            expected_neighbors = Dict(
                1 => [1, 2], 2 => [2], 3 => [3], 4 => [1]
            )

            @test corner_neighbors == expected_neighbors
        end
    end
end


function create_amsg!(sender,receiver,template,model)
    mid = next_amid!(model)
    msg = deepcopy(template)
    msg.id =mid
    msg.sid = sender
    msg.rid = receiver
    msg.ticks = model.ticks
    return msg
end

function send_msg!(receiver::Int64,msg::AGMessage,model)
    rag = getindex(model,receiver)
    #TODO get delay of link in ticks
    g = rag.ctl_graph
    lv = to_local_vertex(g,msg.sid,:aid)
    lva = to_local_vertex(g,rag.id,:aid)
    #need index of nbs
    nbs = neighbors(g,lva)
    i = first(indexin(lv,nbs))
    # log_info(model.ticks,"From $(msg.sid) to  $(receiver) ==> $(msg) ~~~> $(size(rag.msgs_links,1)) --- $i")
    push!(rag.msgs_links[size(rag.msgs_links,1),i],msg)

end

function send_to_nbs!(msg_template::AGMessage,a::Agent,model)::Array{Int64}
    cg = a.ctl_graph
    nbs = neighbors(cg,to_local_vertex(cg,a.id,:aid))
    gid_nbs = [cg[v,:aid] for v in nbs]
    gid_nbs = [gid for gid in gid_nbs if ~(gid in msg_template.body[:trace]) ]
    random_nbs = rand(Binomial(1,model.prob_random_walks),length(gid_nbs))
    random_nbs = [rnb for rnb in random_nbs .* gid_nbs if rnb > 0]
    msgs_sent::Array{Int64} = []

    #disable msgs
    for nb in random_nbs
        if ~(nb in msg_template.body[:trace])
            body = deepcopy(msg_template.body)
            fw_msg = create_amsg!(a.id,nb,msg_template,model)
            send_msg!(nb,fw_msg,model)
            push!(msgs_sent,nb)
        end
    end

    return msgs_sent

end

"""
    Processing for MATCH_PATH msg
"""
function do_match!(msg::AGMessage,a::Agent,model)
    # log_info(model.ticks,a.id,"do_match! -> msg : $(msg)")

    query = msg.body[:query]
    new_path = msg.body[:path]# (tick,confidence,score,path)
    ces = get_controlled_assets(a.id,model)

    #elements this agent controls that are in path
    ces_in_path  = intersect(ces,last(new_path))

    #due diligence: check if path is invalid according to local Knowledge to avoid using/propagating rubbish.
    #however new check required when path query is done locally, as nodes
    # might have dropped since the time the path is received here.
    invalid_path = is_invalid_path(new_path,ces_in_path,model)
    # log_info(model.ticks,a.id,25,"invalidity check: $new_path ==> $invalid_path")
    if !invalid_path

        trace_bk = msg.body[:trace_bk]
        if length(trace_bk) > 1
            save_ctl_path!(a,reverse(trace_bk[1:end-1]))
        end
        # is this agent the original requester of path?
        if first(msg.body[:trace]) == a.id
            for ce in ces_in_path
                spath = last(new_path)[first(indexin(ce,last(new_path))):end]
                # log_info(model.ticks,a.id," procs match for $ce ===> spath: $(spath)")
                #only deals with the exact path, e.g. [7,3,1], not [3,1].
                for i=1:1#length(spath)-1
                    epaths = []
                    if haskey(a.paths,(spath[i],last(spath)))
                        epaths = a.paths[(spath[i],last(spath))]

                        # log_info(model.ticks,a.id," BEFORE: epaths: $epaths")
                        push!(epaths,new_path)
                        # log_info(model.ticks,a.id," AFTER: epaths: $epaths")

                        #sort by score, reverse = false
                        sort!(epaths,lt=isless_paths)

                        #Make sure only model.max_cache_paths are stored
                        if length(epaths) > model.max_cache_paths
                            pop!(epaths)
                        end

                        #TODO cases of older paths
                    else
                        epaths = [new_path]
                    end
                    a.paths[(spath[i],last(spath))] = epaths
                end
            end
            mark_reprocess_of_msg!(a,msg)
            clear_pending_query!(a,query)
            # log_info(model.ticks,a.id,"new path ENDING do_match: => $(a.paths) ++++++++===> $(a.pending)")
        else # This agent is not the original requester of the path
            #continue back propagation of msg
            msg.body[:trace_bk] = trace_bk[1:end-1]
            msg.rid = trace_bk[end-1]
            msg.sid = a.id
            send_msg!(trace_bk[end-1],msg,model)
        end
    else # the path is invalid
        #TODO
    end
end

"""
    Prepare MATCH reply
"""
function do_match!(found_path::Tuple{Int64,Float64,Float64,Array{Int64}},msg::AGMessage,a::Agent,model)
    # log_info(model.ticks,a.id,"*do_match! -> msg : $(msg) -> found: $(found_path)")
    query = msg.body[:query]
    trace = deepcopy(msg.body[:trace])
    append!(trace,a.id)
    trace_bk = deepcopy(trace)
    save_ctl_path!(a,reverse(trace_bk[1:end-1]))
    of_mid = msg.body[:of_mid]

    nbody = Dict(:of_mid=>of_mid,:query=>query,:trace=>trace,:trace_bk=>trace_bk[1:end-1],:path=>found_path)
    # log_info("[$(model.ticks)]{$(a.id)} =-do_match! -> receiver : $(trace_bk)")
    rpy_msg = AGMessage(next_amid!(model),model.ticks,a.id,trace_bk[end-1],MATCH_PATH,nbody)
    send_msg!(trace_bk[end-1],rpy_msg,model)
end

function process_msg!(a::Agent,msg::AGMessage,model)

    @match msg.reason begin
        AG_Protocol(1) =>
                        #Query coming from other agent
                        do_query!(msg,a,model)

        AG_Protocol(2) =>
                        do_match!(msg,a,model)

        AG_Protocol(3) =>
                        do_new_nb!(msg,a,model)
        AG_Protocol(4) =>
                        do_ne_down(a,msg,model)

        # AG_Protocol(5) =>
        #                 do_update_local_graph!(a,msg,model)

        _ => begin
                log_info("[$(model.ticks)]($(a.id)) -> match default!!")
            end
    end

    state = get_state(a)
    state.in_ag_msg+=1.0
    set_state!(a,state)
end

function do_new_nb!(msg::AGMessage,a::Agent,model)

    new_nbs = msg.body[:new_nbs]
    lg = a.ctl_graph
    ls = to_local_vertex(lg,a.id)
    for nb in new_nbs
        ld = to_local_vertex(lg,nb)
        if !has_edge(lg,ls,ld)
            add_edge!(lg,ls,ld)
        end
        if !has_edge(lg,a.id,nb)
            add_edge!(model.ctl_graph,a.id,nb)
        end
    end

end

 """
    It process a msg from a neighbor controller notifying a sne is down
 """
function do_ne_down(a::Agent,msg::AGMessage,model)
    remove_drop_sne!(a,msg.body[:dpid],model.ticks)
    if !isempty(msg.body[:ctl_trace])
        propagate_drop_sne!(a,msg.body[:dpid],msg.body[:ctl_trace],model)
    end
end

"""
 It simulates a NE-controller link down
 In reality this is the routine that checks heartbeats
 from controlled NEs.
"""
function controlled_sne_down!(a::Agent,dpn_id::Int,model)

    set_control_agent!(dpn_id,a.id*-1,model)
    # init_agent!(a,model)
    if isempty(get_live_snes(a,model))
        s = get_state(a)
        s.up = false
        set_state!(a,s)
    end

    lvb = to_local_vertex(a.base_ntw_graph,dpn_id)
    lvc = to_local_vertex(a.ntw_graph,dpn_id)
    a.base_ntw_graph = soft_remove_vertex(a.base_ntw_graph,lvb)
    a.ntw_graph = soft_remove_vertex(a.ntw_graph,lvc)

    #TODO implement when a control agent is down too
    # do_drop!(msg,a,model)
end

"""
 It simulates a NE-controller link up
 In reality this is the routine that checks heartbeats
 from controlled NEs.
"""

function controlled_sne_up!(a::Agent,rjn_id::Int,live_nbs::Array{Int64},model::ABM)

    set_control_agent!(rjn_id,a.id,model)

    if !is_up(a)
        set_up!(a)
    end

    lvb = to_local_vertex(a.base_ntw_graph,rjn_id)
    lvc = to_local_vertex(a.ntw_graph,rjn_id)
    # add_vertex!(a.base_ntw_graph)
    # add_vertex!(a.ntw_graph)

    # set_prop!(a.base_ntw_graph,nv(a.base_ntw_graph),:eid,rjn_id)
    # set_prop!(a.ntw_graph,nv(a.ntw_graph),:eid,rjn_id)

    #just added vertices, last id
    # lvb = nv(a.base_ntw_graph)
    # lvc = nv(a.ntw_graph)

    a.base_ntw_graph = add_edges_gids(a.base_ntw_graph,lvb,live_nbs,:eid)
    a.ntw_graph = add_edges_gids(a.ntw_graph,lvc,live_nbs,:eid)
end

function get_state(a::Agent)::State
    return last(a.state_trj)
end
function set_state!(a::Agent,new_state::ControlAgentState)
    pop!(a.state_trj)
    push!(a.state_trj,new_state)
end

# function get_throughput(a::Agent,model)
#     a.msgs_in
# end

function get_state_trj(a::Agent)::Vector{State}
    return a.state_trj
end

function get_throughput_up(a::Agent,model)
    v_msg_in = [ s.in_ag_msg for s in a.state_trj ]

    # log_info("[$(model.ticks)] ($(a.id)) msg in  ==> $v_msg_in")
    v_up = [ s.up for s in a.state_trj ]
    v_tpt = get_throughput(v_msg_in,model.:interval_tpt)
    return [ v_up[i] ? v_tpt[i] : 0.0   for i=1:length(v_tpt)]
end

"""
 Check if path is invalid according to local knowledge. It does not mean is valid as this agent might not have all info to check validity.
 Only flags invalid if 100% sure.
 cnes:controlled nes by this agent (a)
"""
function is_invalid_path(path,cnes,model)
    result = false

    for cne in cnes
        if last(last(path)) != cne
            sne = getindex(model,cne)
            cne_i = first(indexin(cne,last(path)))
            ports_path = filter(p->p[2]=="s$(last(path)[cne_i+1])",get_port_edge_list(sne))
            result = isempty(ports_path) ? true : result
        end
    end
    return result
end

function save_ctl_path!(a::Agent,path::Array{Int64})
    already_in = false
    for p in a.ctl_paths
        if isless(path,p) || p == path#is path a strict subset or equal to p ?
            already_in = true
        end
    end
    if !already_in
        push!(a.ctl_paths,path)
    end
end

function propagate_drop_sne!(a::Agent,dpid::Int64,ctl_trace::Array{Int64},model)
    sid = a.id
    rid = first(ctl_trace)
    rem_path = length(ctl_trace) > 1 ? ctl_trace[2:end] : []
    dmsg = AGMessage(next_amid!(model),model.ticks,sid,rid,NE_DOWN,Dict(:dpid=>dpid,:ctl_trace=>rem_path))
    send_msg!(rid,dmsg,model)
end

"""
    It removes a dropped sne node from local paths and graph
"""
function remove_drop_sne!(a::Agent,dpid::Int64,drop_time::Int64)
    # log_info(drop_time,a.id,"Existing paths: $(a.paths)")
    new_paths_dict = Dict()
    #delete pre-computed paths containing dropping node
    for path_k in keys(a.paths)
        v_paths = a.paths[path_k]
        new_paths = []
        for path in v_paths
            if !(dpid in last(path))
                push!(new_paths,path)
            end
        end
        if !isempty(new_paths)
            new_paths_dict[path_k] = new_paths
        end
    end
    #update agent pre-computed paths
    a.paths = new_paths_dict

    #update graphs used by the control agent accordingly

   #is dpid in base ntw graph?
   lvb = to_local_vertex(a.base_ntw_graph,dpid)
   lvc = to_local_vertex(a.ntw_graph,dpid)

   log_info(drop_time,a.id," Removing dpid: $dpid from local base: $lvb --- local curr: $lvc")

   if lvb != 0
    a.base_ntw_graph = soft_remove_vertex(a.base_ntw_graph,lvb)
   end
   if lvc != 0
        a.ntw_graph = soft_remove_vertex(a.ntw_graph,lvc)
   end

   state = get_state(a)
   new_ap = Dict()
   for p in pairs(state.active_paths)
        if !(dpid in last(p))
          new_ap[first(p)] = last(p)
        end
   end

   state.active_paths = new_ap
   set_state!(a,state)
end

"""
It deals with prediction of unavailability (for a given time window) of a set of NEs under control.

"""
function do_update_flows_from_changes!(a::Agent,ntw_changes::Vector{Int64},model::ABM)
    #TODO operation for other than centralised agent
    if get_state(a).up

        log_info(model.ticks,a.id,"Pred_Down: ntw_changes!!!!!! $(ntw_changes)")

        joining_nodes = filter(x->x>0,ntw_changes)
        dropping_nodes = -1 * filter(x->x<0,ntw_changes)

        query_graph = deepcopy(a.ntw_graph)

        #add nodes
        for jng_id in joining_nodes
            query_graph = soft_remove_vertex(query_graph,jng_id)

            base_g = a.base_ntw_graph

            lv = to_local_vertex(base_g,jng_id)
            nbs = neighbors(base_g,lv)

            #TODO check if nbs is up?

            for nb_id in nbs
                add_edge!(query_graph,nb_id,jng_id)
                add_edge!(query_graph,jng_id,nb_id)
            end
        end

        #remove nodes
        for dpn_id in dropping_nodes
            query_graph = soft_remove_vertex(query_graph,dpn_id)
        end

        query_time = model.ticks

        #Only trigger queries if maintenance policy is not predictive (where routes/flows come from the optimisation algorithm)
        queries = a.maintenance.policy != PredictiveM  ? model.ntw_services : []

        for query in queries
            query_paths = Dict{Tuple{Int64,Int64},Array{Tuple{Int64,Float64,Float64,Array{Int64}}}}()

            path = do_query(query_time,query,query_graph,query_paths)

            if isempty(path)
                 log_info(model.ticks,a.id,"No path found => $query")
            #     # if !haskey(a.previous_queries,query)
            #     #     sent_to = query_nbs!(a,msg,jg,query,trace,model)
            #     #     a.previous_queries[query] = (model.ticks,sent_to)
            #     # end
            else
            #     # do_match!(path,msg,a,model)
            #     # clear_pending_query!(a,query)
                do_update_flows_from_path!(a,last(path),model)
            end

        end

    end
end

"""
Update flows of the snes controlled by the agent a and for the path given,
"""
function do_update_flows_from_path!(a::Agent,path::Array{Int64,1},model::ABM)
    msg = OFMessage(-1, model.ticks,-1,0,OFPR_ADD_FLOW,[])
    # log_info(model.ticks,a.id,"active path: $path")
    install_flow!(a,path,model,msg)
    if length(path) > 1
        k = (first(path),last(path))
        # log_info(model.ticks,a.id,"key: $(k) ==> Ag Path: $spath")
        get_state(a).active_paths[k] = path
    end
end


function gen_current(props,myprop::Prop)

    if issubtype(typeof(props),Tuple)
        confields=Array(Expr,length(props))
        curfields=Array(Expr,length(props))
        initfields=Array(Expr,length(props))
        mylengths=0

        for i=1:length(props)
            confields[i]=speed_con(props[i],mylengths)
            curfields[i]=speed_cur(props[i],mylengths)
            initfields[i]=speed_init(props[i],mylengths)
            mylengths=length(fieldnames(props[i]))-1
        end
    else
        confields=[speed_con(props,0)]
        curfields=[speed_cur(props,0)]
        initfields=[speed_init(props,0)]
    end

    @eval begin

        function con!(p::$(typeof(myprop)),v::Array{Float64,1},internal::Array{Int64,1},dt=.025)

            @fastmath for i=1:length(internal)
                k=internal[i]
                $([confields[j] for j=1:length(confields)]...)
            end

            nothing
        end

        function cur!(p::$(typeof(myprop)),v::Array{Float64,1},im::Array{Float64,1},internal::Array{Int64,1})

            @fastmath for i=1:length(internal)
                k=internal[i]
                $([curfields[j] for j=1:length(curfields)]...)
            end

            nothing
        end

        function init!(p::$(typeof(myprop)),v::Array{Float64,1},internal::Array{Int64,1})
            @fastmath for i=1:length(internal)
                k=internal[i]
                $([initfields[j] for j=1:length(initfields)]...)
            end
        end
    end
end

const ena=50.0
const ek=-77.0


ackno(tr::TCPReceiver)::Union{Nothing,WrappingInt32} =
    !tr.has_syn ? nothing : wrap(UInt(stream_out(tr).bytes_written_count) + stream_out(tr).has_eof + 1, tr.isn)

window_size(tr::TCPReceiver) = tr.capacity - buffer_size(tr.reassembler |> stream_out)

unassembled_bytes(tr::TCPReceiver) = tr.reassembler.unassembled_bytes

stream_out(tr::TCPReceiver) = tr.reassembler |> stream_out

assembled_bytes(tr::TCPReceiver) = tr.reassembler.cur_idx

function segment_received!(tr::TCPReceiver, seg::TCPSegment)::Nothing
    header = seg.header
    paload = seg.payload
    if (header.syn)
        tr.has_syn && return
        tr.has_syn = true
        tr.isn = header.seqno
        tr.abs_seqno = 1
        (length_in_sequence_space(seg) == 1) && return
    elseif !tr.has_syn
        return
    else
        tr.abs_seqno = unwrap(header.seqno, tr.isn, tr.abs_seqno)
    end

    (length_in_sequence_space(seg) == 0) && return

    push_substring!(tr.reassembler, paload.storage[], tr.abs_seqno - 1, header.fin)
end