const dummy_function = ()->nothing

num_linkconstraints(modeledge::OptiEdge) = length(modeledge.linkconstraints)
function _caching_optimizer(modelnode::OptiNode)
    if isa(modelnode.model.moi_backend,MOIU.CachingOptimizer)
        return modelnode.model.moi_backend
    else
        return modelnode.model.moi_backend.optimizer
    end
end
moi_optimizer(modelnode::OptiNode) = _caching_optimizer(modelnode).optimizer.model
_caching_optimizer(model::Any) = model.moi_backend
function set_g_link!(linkedge::OptiEdge,l,gl,gu)
    cnt = 1
    for (ind,linkcon) in linkedge.linkconstraints
        l[cnt] = 0. # need to implement dual start later
        if linkcon.set isa MOI.EqualTo
            gl[cnt] = linkcon.set.value
            gu[cnt] = linkcon.set.value
        elseif linkcon.set isa MOI.GreaterThan
            gl[cnt] = linkcon.set.lower
            gu[cnt] = Inf
        elseif linkcon.set isa MOI.LessThan
            gl[cnt] = -Inf
            gu[cnt] = linkcon.set.upper
        else
            gl[cnt] = linkcon.set.lower
            gu[cnt] = linkcon.set.upper
        end
        cnt += 1
    end
end

function hessian_lagrangian_structure(graph::OptiGraph,I,J,ninds,nnzs_hess_inds,modelnodes)
    @blas_safe_threads for k=1:length(modelnodes)
        isempty(nnzs_hess_inds[k]) && continue
        offset = ninds[k][1]-1
        II = view(I,nnzs_hess_inds[k])
        JJ = view(J,nnzs_hess_inds[k])
        hessian_lagrangian_structure(
            moi_optimizer(modelnodes[k]),II,JJ)
        II.+= offset
        JJ.+= offset
    end
end

function jacobian_structure(linkedge::OptiEdge,I,J,ninds,x_index_map,g_index_map)
    offset=1
    for linkcon in link_constraints(linkedge)
        offset += jacobian_structure(linkcon,I,J,ninds,x_index_map,g_index_map,offset)
    end
end
function jacobian_structure(linkcon,I,J,ninds,x_index_map,g_index_map,offset)
    cnt = 0
    for var in get_vars(linkcon)
        I[offset+cnt] = g_index_map[linkcon]
        J[offset+cnt] = x_index_map[var]
        cnt += 1
    end
    return cnt
end

function jacobian_structure(
    graph::OptiGraph,I,J,ninds,minds,pinds,nnzs_jac_inds,nnzs_link_jac_inds,
    x_index_map,g_index_map,modelnodes,linkedges)

    @blas_safe_threads for k=1:length(modelnodes)
        isempty(nnzs_jac_inds[k]) && continue
        offset_i = minds[k][1]-1
        offset_j = ninds[k][1]-1
        II = view(I,nnzs_jac_inds[k])
        JJ = view(J,nnzs_jac_inds[k])
        jacobian_structure(
            moi_optimizer(modelnodes[k]),II,JJ)
        II.+= offset_i
        JJ.+= offset_j
    end

    @blas_safe_threads for q=1:length(linkedges)
        isempty(nnzs_link_jac_inds[q]) && continue
        II = view(I,nnzs_link_jac_inds[q])
        JJ = view(J,nnzs_link_jac_inds[q])
        jacobian_structure(
            linkedges[q],II,JJ,ninds,x_index_map,g_index_map)
    end
end
function eval_objective(graph::OptiGraph,x,ninds,x_index_map,modelnodes)
    obj = Threads.Atomic{Float64}(0.)
    @blas_safe_threads for k=1:length(modelnodes)
         Threads.atomic_add!(obj,eval_objective(
             moi_optimizer(modelnodes[k]),view(x,ninds[k])))
    end
    return obj.value + eval_function(graph.objective_function,x,ninds,x_index_map)
end
function eval_objective_gradient(graph::OptiGraph,f,x,ninds,modelnodes)
    @blas_safe_threads for k=1:length(modelnodes)
        eval_objective_gradient(moi_optimizer(modelnodes[k]),view(f,ninds[k]),view(x,ninds[k]))
    end
end

function eval_function(aff::GenericAffExpr,x,ninds,x_index_map)
    function_value = aff.constant
    for (var,coef) in aff.terms
        function_value += coef*x[x_index_map[var]]
    end
    return function_value
end
function eval_constraint(linkedge::OptiEdge,c,x,ninds,x_index_map)
    cnt = 1
    for linkcon in link_constraints(linkedge)
        c[cnt] = eval_function(get_func(linkcon),x,ninds,x_index_map)
        cnt += 1
    end
end
get_func(linkcon) = linkcon.func
function eval_constraint(graph::OptiGraph,c,x,ninds,minds,pinds,x_index_map,modelnodes,linkedges)
    @blas_safe_threads for k=1:length(modelnodes)
        eval_constraint(moi_optimizer(modelnodes[k]),view(c,minds[k]),view(x,ninds[k]))
    end
    @blas_safe_threads for q=1:length(linkedges)
        eval_constraint(linkedges[q],view(c,pinds[q]),x,ninds,x_index_map)
    end
end

function eval_hessian_lagrangian(graph::OptiGraph,hess,x,sig,l,
                                 ninds,minds,nnzs_hess_inds,modelnodes)
    @blas_safe_threads for k=1:length(modelnodes)
        isempty(nnzs_hess_inds) && continue
        eval_hessian_lagrangian(moi_optimizer(modelnodes[k]),
                                view(hess,nnzs_hess_inds[k]),view(x,ninds[k]),sig,
                                view(l,minds[k]))
    end
end

function eval_constraint_jacobian(linkedge::OptiEdge,jac,x)
    offset=0
    for linkcon in link_constraints(linkedge)
        offset+=eval_constraint_jacobian(linkcon,jac,offset)
    end
end
function eval_constraint_jacobian(linkcon,jac,offset)
    cnt = 0
    for coef in get_coeffs(linkcon)
        cnt += 1
        jac[offset+cnt] = coef
    end
    return cnt
end
get_vars(linkcon) = keys(linkcon.func.terms)
get_coeffs(linkcon) = values(linkcon.func.terms)

function eval_constraint_jacobian(graph::OptiGraph,jac,x,
                                  ninds,minds,nnzs_jac_inds,nnzs_link_jac_inds,modelnodes,linkedges)
    @blas_safe_threads for k=1:length(modelnodes)
        eval_constraint_jacobian(
            moi_optimizer(modelnodes[k]),view(jac,nnzs_jac_inds[k]),view(x,ninds[k]))
    end
    @blas_safe_threads for q=1:length(linkedges)
        eval_constraint_jacobian(linkedges[q],view(jac,nnzs_link_jac_inds[q]),x)
    end
end
get_nnz_link_jac(linkedge::OptiEdge) = sum(
    length(linkcon.func.terms) for (ind,linkcon) in linkedge.linkconstraints)

struct GraphModel{T} <: AbstractNLPModel{T,Vector{T}}
    ninds::Vector{UnitRange{Int}}
    minds::Vector{UnitRange{Int}}
    pinds::Vector{UnitRange{Int}}
    nnzs_jac_inds::Vector{UnitRange{Int}}
    nnzs_hess_inds::Vector{UnitRange{Int}}
    nnzs_link_jac_inds::Vector{UnitRange{Int}}

    x_index_map::Dict #
    g_index_map::Dict #

    modelnodes::Vector{OptiNode}
    linkedges::Vector{OptiEdge}

    meta::NLPModelMeta{T, Vector{T}}
    graph::OptiGraph
    counters::NLPModelsCounters
    ext::Dict{Symbol,Any}
end

obj(nlp::GraphModel, x::AbstractVector) =  eval_objective(nlp.graph,x,nlp.ninds,nlp.x_index_map,nlp.modelnodes)
grad!(nlp::GraphModel, x::AbstractVector, f::AbstractVector) =eval_objective_gradient(nlp.graph,f,x,nlp.ninds,nlp.modelnodes)
cons!(nlp::GraphModel, x::AbstractVector, c::AbstractVector) = eval_constraint(
    nlp.graph,c,x,nlp.ninds,nlp.minds,nlp.pinds,nlp.x_index_map,nlp.modelnodes,nlp.linkedges)
function hess_coord!(nlp::GraphModel,x::AbstractVector,l::AbstractVector,hess::AbstractVector;obj_weight=1.)
    eval_hessian_lagrangian(
        nlp.graph,hess,x,obj_weight,l,nlp.ninds,nlp.minds,
        nlp.nnzs_hess_inds,nlp.modelnodes,
    )
end
function jac_coord!(nlp::GraphModel,x::AbstractVector,jac::AbstractVector)
    eval_constraint_jacobian(
        nlp.graph,jac,x,nlp.ninds,nlp.minds,nlp.nnzs_jac_inds,nlp.nnzs_link_jac_inds,
        nlp.modelnodes,nlp.linkedges,
    )
end
function hess_structure!(nlp::GraphModel, I::AbstractVector{T}, J::AbstractVector{T}) where T
    hessian_lagrangian_structure(
        nlp.graph,I,J,nlp.ninds,nlp.nnzs_hess_inds,nlp.modelnodes,
    )
end
function jac_structure!(nlp::GraphModel, I::AbstractVector{T}, J::AbstractVector{T}) where T
    jacobian_structure(
        nlp.graph,I,J,nlp.ninds,nlp.minds,nlp.pinds,nlp.nnzs_jac_inds,nlp.nnzs_link_jac_inds,
        nlp.x_index_map,nlp.g_index_map,nlp.modelnodes,nlp.linkedges,
    )
end

function GraphModel(graph::OptiGraph)
    modelnodes = all_nodes(graph)
    linkedges = all_edges(graph)

    for modelnode in modelnodes
        num_variables(modelnode) == 0 && error("Empty node exist! Delete the empty nodes.")
    end

    @blas_safe_threads for k=1:length(modelnodes)
        set_optimizer(modelnodes[k],Optimizer)
        if modelnodes[k].model.nlp_data !== nothing
            MOI.set(modelnodes[k].model, MOI.NLPBlock(),
                    _create_nlp_block_data(modelnodes[k].model))
            empty!(modelnodes[k].model.nlp_data.nlconstr_duals)
        end
        MOIU.attach_optimizer(modelnodes[k].model)
        MOI.initialize(moi_optimizer(modelnodes[k]).nlp_data.evaluator,[:Grad,:Hess,:Jac])
    end

    K = length(modelnodes)
    ns= [num_variables(modelnode) for modelnode in modelnodes]
    n = sum(ns)
    ns_cumsum = cumsum(ns)
    ms= [num_constraints(modelnode) for modelnode in modelnodes]
    ms_cumsum = cumsum(ms)
    m = sum(ms)

    nnzs_hess = [get_nnz_hess(moi_optimizer(modelnode)) for modelnode in modelnodes]
    nnzs_hess_cumsum = cumsum(nnzs_hess)
    nnz_hess = sum(nnzs_hess)

    nnzs_jac = [get_nnz_jac(moi_optimizer(modelnode)) for modelnode in modelnodes]
    nnzs_jac_cumsum = cumsum(nnzs_jac)
    nnz_jac = sum(nnzs_jac)

    nnzs_link_jac = [get_nnz_link_jac(linkedge) for linkedge in linkedges]
    nnzs_link_jac_cumsum = cumsum(nnzs_link_jac)
    nnz_link_jac = isempty(nnzs_link_jac) ? 0 : sum(nnzs_link_jac)

    ninds = [(i==1 ? 0 : ns_cumsum[i-1])+1:ns_cumsum[i] for i=1:K]
    minds = [(i==1 ? 0 : ms_cumsum[i-1])+1:ms_cumsum[i] for i=1:K]
    nnzs_hess_inds = [(i==1 ? 0 : nnzs_hess_cumsum[i-1])+1:nnzs_hess_cumsum[i] for i=1:K]
    nnzs_jac_inds = [(i==1 ? 0 : nnzs_jac_cumsum[i-1])+1:nnzs_jac_cumsum[i] for i=1:K]

    Q = length(linkedges)
    ps= [num_linkconstraints(modeledge) for modeledge in linkedges]
    ps_cumsum =  cumsum(ps)
    p = sum(ps)
    pinds = [(i==1 ? m : m+ps_cumsum[i-1])+1:m+ps_cumsum[i] for i=1:Q]
    nnzs_link_jac_inds =
        [(i==1 ? nnz_jac : nnz_jac+nnzs_link_jac_cumsum[i-1])+1: nnz_jac + nnzs_link_jac_cumsum[i] for i=1:Q]

    x =Vector{Float64}(undef,n)
    xl=Vector{Float64}(undef,n)
    xu=Vector{Float64}(undef,n)

    l =Vector{Float64}(undef,m+p)
    gl=Vector{Float64}(undef,m+p)
    gu=Vector{Float64}(undef,m+p)

    @blas_safe_threads for k=1:K
        set_x!(moi_optimizer(modelnodes[k]),view(x,ninds[k]),view(xl,ninds[k]),view(xu,ninds[k]))
        set_g!(moi_optimizer(modelnodes[k]),view(l,minds[k]),view(gl,minds[k]),view(gu,minds[k]))
    end

    @blas_safe_threads for q=1:Q
        set_g_link!(linkedges[q],view(l,pinds[q]),view(gl,pinds[q]),view(gu,pinds[q]))
    end

    modelmap=Dict(modelnodes[k].model=> k for k=1:K)
    x_index_map = Dict(
        # var=>ninds[modelmap[var.model]][backend(var.model).optimizer.model_to_optimizer_map[var.index].value]
        var=>ninds[modelmap[var.model]][_caching_optimizer(getnode(var)).model_to_optimizer_map[var.index].value]
        for modelnode in modelnodes for var in all_variables(modelnode))
    cnt = 0
    g_index_map = Dict(con=> m + (cnt+=1) for linkedge in linkedges for (ind,con) in linkedge.linkconstraints)

    jac_constant = true
    hess_constant = true
    for node in modelnodes
        j,h = is_jac_hess_constant(moi_optimizer(node))
        jac_constant = jac_constant & j
        hess_constant = hess_constant & h
    end

    ext = Dict{Symbol,Any}(:n=>n,:m=>m,:p=>p,:ninds=>ninds,:minds=>minds,:pinds=>pinds,
                           :linkedges=>linkedges,:jac_constant=>jac_constant,:hess_constant=>hess_constant)

    return GraphModel(
        ninds,minds,pinds,nnzs_jac_inds,nnzs_hess_inds,nnzs_link_jac_inds,
        x_index_map,g_index_map,modelnodes,linkedges,
        NLPModelMeta(
            n,
            ncon = m+p,
            x0 = x,
            lvar = xl,
            uvar = xu,
            y0 = l,
            lcon = gl,
            ucon = gu,
            nnzj = nnz_jac + nnz_link_jac,
            nnzh = nnz_hess,
            minimize = true # graph.objective_sense == MOI.MIN_SENSE
        ),
        graph,NLPModelsCounters(),
        ext
    )

end

function get_part(graph::OptiGraph,nlp::GraphModel)
    n = nlp.ext[:n]
    m = nlp.ext[:m]
    p = nlp.ext[:p]

    ninds = nlp.ninds
    minds = nlp.minds
    pinds = nlp.pinds

    ind_ineq = findall(get_lcon(nlp).!=get_ucon(nlp))
    l = length(ind_ineq)

    part = Vector{Int}(undef,n+m+l+p)

    for k=1:length(ninds)
        part[ninds[k]].=k
    end
    for k=1:length(minds)
        part[minds[k].+n.+l].=k
    end
    cnt = 0

    for linkedge in nlp.ext[:linkedges]
        for (ind,con) in linkedge.linkconstraints
            cnt+=1
            attached_node_idx = graph.node_idx_map[con.attached_node]
            part[n+l+m+cnt] = attached_node_idx != nothing ? attached_node_idx : error("All the link constraints need to be attached to a node")
        end
    end

    cnt = 0
    for q in ind_ineq
        cnt+=1
        part[n+cnt] = part[n+l+q]
    end

    return part
end

function optimize!(graph::OptiGraph; option_dict = Dict{Symbol,Any}(), kwargs...)
    graph.objective_function.constant = 0.
    nlp = GraphModel(graph)

    K = num_all_nodes(graph)
    if (haskey(kwargs,:schwarz_custom_partition) && kwargs[:schwarz_custom_partition]) ||
        (haskey(option_dict,:schwarz_custom_partition) && option_dict[:schwarz_custom_partition])

        part= get_part(graph,nlp)
        option_dict[:schwarz_part] = part
        option_dict[:schwarz_num_parts] = num_all_nodes(graph)

    elseif (haskey(kwargs,:schur_custom_partition) && kwargs[:schur_custom_partition]) ||
        (haskey(option_dict,:schur_custom_partition) && option_dict[:schur_custom_partition])

        part= get_part(graph,nlp)
        part[part.>K].=0
        option_dict[:schur_part] = part
        option_dict[:schur_num_parts] = K
    end

    option_dict[:jacobian_constant] = nlp.ext[:jac_constant]
    option_dict[:hessian_constant] = nlp.ext[:hess_constant]
    ips = InteriorPointSolver(nlp;option_dict=option_dict,kwargs...)
    result = optimize!(ips)

    graph.optimizer = ips
    graph.objective_function.constant = graph.optimizer.obj_val

    @blas_safe_threads for k=1:K
        moi_optimizer(nlp.modelnodes[k]).result = MadNLPExecutionStats(
            ips.status,view(result.solution,nlp.ninds[k]),
            ips.obj_val,
            view(result.constraints,nlp.minds[k]),
            ips.inf_du, ips.inf_pr,
            view(result.multipliers,nlp.minds[k]),
            view(result.multipliers_L,nlp.ninds[k]),
            view(result.multipliers_U,nlp.ninds[k]),
            ips.cnt.k, ips.nlp.counters,ips.cnt.total_time)
            # TODO: quick hack to specify to JuMP that the
            # model is not dirty (so we do not run in `OptimizeNotCalled`
            # exception).
            nlp.modelnodes[k].model.is_model_dirty = false
    end
end


using ReinforcementLearningZoo
using Test
using ReinforcementLearningBase
using ReinforcementLearningCore
using ReinforcementLearningEnvironments
using Flux
using Statistics
using Random
using OpenSpiel

@testset "ReinforcementLearningZoo.jl" begin

    @testset "training" begin
        mktempdir() do dir
            for method in (:BasicDQN, :DQN, :PrioritizedDQN, :Rainbow, :IQN, :VPG)
                res = run(Experiment(
                    Val(:JuliaRL),
                    Val(method),
                    Val(:CartPole),
                    nothing;
                    save_dir = joinpath(dir, "CartPole", string(method)),
                ))
                @info "stats for $method" avg_reward = mean(res.hook[1].rewards) avg_fps =
                    1 / mean(res.hook[2].times)
            end

            for method in (:BasicDQN, :DQN)
                res = run(Experiment(
                    Val(:JuliaRL),
                    Val(method),
                    Val(:MountainCar),
                    nothing;
                    save_dir = joinpath(dir, "MountainCar", string(method)),
                ))
                @info "stats for $method" avg_reward = mean(res.hook[1].rewards) avg_fps =
                    1 / mean(res.hook[2].times)
            end

            for method in (:A2C, :A2CGAE, :PPO)
                res = run(Experiment(
                    Val(:JuliaRL),
                    Val(method),
                    Val(:CartPole),
                    nothing;
                    save_dir = joinpath(dir, "CartPole", string(method)),
                ))
                @info "stats for $method" avg_reward =
                    mean(Iterators.flatten(res.hook[1].rewards))
            end

            for method in (:DDPG, :SAC, :TD3)
                res = run(Experiment(
                    Val(:JuliaRL),
                    Val(method),
                    Val(:Pendulum),
                    nothing;
                    save_dir = joinpath(dir, "Pendulum", string(method)),
                ))
                @info "stats for $method" avg_reward =
                    mean(Iterators.flatten(res.hook[1].rewards))
            end
        end
    end

    @testset "run pretrained models" begin
        for x in ("JuliaRL_BasicDQN_CartPole",)
            e = Experiment(x)
            e.agent.policy = load_policy(x)
            Flux.testmode!(e.agent)
            run(e.agent, e.env, StopAfterEpisode(1), e.hook)
            @info "result of evaluating pretrained model: $x for once:" reward =
                e.hook[1].rewards[end]
        end
    end

    @testset "minimax" begin
        e = E`JuliaRL_Minimax_OpenSpiel(tic_tac_toe)`
        run(e)
        @test e.hook[1].rewards[end] == e.hook[2].rewards[end] == 0.0
    end

    @testset "TabularCFR" begin
        e = E`JuliaRL_TabularCFR_OpenSpiel(kuhn_poker)`
        run(e)
        @test isapprox(mean(e.hook[2].rewards), -1 / 18; atol = 0.01)
        @test isapprox(mean(e.hook[3].rewards), 1 / 18; atol = 0.01)

        reset!(e.env)
        expected_values = Dict(expected_policy_values(e.agent, e.env))
        @test isapprox(expected_values[get_role(e.agent[2])], -1 / 18; atol = 0.01)
        @test isapprox(expected_values[get_role(e.agent[3])], 1 / 18; atol = 0.01)
    end
end


# Types

export
    Interaction,
    GeneralInteraction,
    SpecificInteraction,
    Simulator,
    Thermostat,
    NeighbourFinder,
    Logger,
    Atom,
    Simulation

const DefaultFloat = Float64

"An interaction between atoms that contributes to forces on the atoms."
abstract type Interaction end

"""
A general interaction that will apply to all atom pairs.
Custom general interactions should sub-type this type.
"""
abstract type GeneralInteraction <: Interaction end

"""
A specific interaction between sets of specific atoms, e.g. a bond angle.
Custom specific interactions should sub-type this type.
"""
abstract type SpecificInteraction <: Interaction end

"""
A type of simulation to run, e.g. leap-frog integration or energy minimisation.
Custom simulators should sub-type this type.
"""
abstract type Simulator end

"""
A way to keep the temperature of a simulation constant.
Custom thermostats should sub-type this type.
"""
abstract type Thermostat end

"""
A way to find near atoms to save on simulation time.
Custom neighbour finders should sub-type this type.
"""
abstract type NeighbourFinder end

"""
A way to record a property, e.g. the temperature, throughout a simulation.
Custom loggers should sub-type this type.
"""
abstract type Logger end

"""
    Atom(; <keyword arguments>)

An atom and its associated information.
Properties unused in the simulation or in analysis can be left with their
default values.

# Arguments
- `attype::AbstractString=""`: the type of the atom.
- `name::AbstractString=""`: the name of the atom.
- `resnum::Integer=0`: the residue number if the atom is part of a polymer.
- `resname::AbstractString=""`: the residue name if the atom is part of a
    polymer.
- `charge::T=0.0`: the charge of the atom, used for electrostatic interactions.
- `mass::T=0.0`: the mass of the atom.
- `σ::T=0.0`: the Lennard-Jones finite distance at which the inter-particle
    potential is zero.
- `ϵ::T=0.0`: the Lennard-Jones depth of the potential well.
"""
struct Atom{T}
    attype::String
    name::String
    resnum::Int
    resname::String
    charge::T
    mass::T
    σ::T
    ϵ::T
end

# We define constructors rather than using Base.@kwdef as it makes conversion
#   more convenient with the parametric type
function Atom(;
                attype="",
                name="",
                resnum=0,
                resname="",
                charge=0.0,
                mass=0.0,
                σ=0.0,
                ϵ=0.0)
    return Atom{typeof(charge)}(attype, name, resnum, resname, charge, mass, σ, ϵ)
end

function Base.show(io::IO, a::Atom)
    print(io, "Atom{", typeof(a.charge), "} with name \"", a.name,
                "\", type \"", a.attype, "\", charge=", a.charge, ", mass=",
                a.mass, ", σ=", a.σ, ", ϵ=", a.ϵ)
end

"""
    Simulation(; <keyword arguments>)

The data needed to define and run a molecular simulation.
Properties unused in the simulation or in analysis can be left with their
default values.

# Arguments
- `simulator::Simulator`: the type of simulation to run.
- `atoms::Vector{A}`: the atoms, or atom equivalents, in the simulation. Can be
    of any type.
- `specific_inter_lists::SI=()`: the specific interactions in the simulation,
    i.e. interactions between specific atoms such as bonds or angles. Typically
    a `Tuple`.
- `general_inters::GI=()`: the general interactions in the simulation, i.e.
    interactions between all or most atoms such as electrostatics. Typically a
    `Tuple`.
- `coords::C`: the coordinates of the atoms in the simulation. Typically a
    `Vector` of `SVector`s of any dimension and type `T`, where `T` is an
    `AbstractFloat` type.
- `velocities::C=zero(coords)`: the velocities of the atoms in the simulation,
    which should be the same type as the coordinates. The meaning of the
    velocities depends on the simulator used, e.g. for the `VelocityFreeVerlet`
    simulator they represent the previous step coordinates for the first step.
- `temperature::T=0.0`: the temperature of the simulation.
- `box_size::T`: the size of the cube in which the simulation takes place.
- `neighbour_finder::NeighbourFinder=NoNeighbourFinder()`: the neighbour finder
    used to find close atoms and save on computation.
- `thermostat::Thermostat=NoThermostat()`: the thermostat which applies during
    the simulation.
- `loggers::Dict{String, <:Logger}=Dict()`: the loggers that record properties
    of interest during the simulation.
- `timestep::T=0.0`: the timestep of the simulation.
- `n_steps::Integer=0`: the number of steps in the simulation.
- `n_steps_made::Vector{Int}=[]`: the number of steps already made during the
    simulation. This is a `Vector` to allow the `struct` to be immutable.
"""
struct Simulation{T, A, C, GI, SI}
    simulator::Simulator
    atoms::Vector{A}
    specific_inter_lists::SI
    general_inters::GI
    coords::C
    velocities::C
    temperature::T
    box_size::T
    neighbour_finder::NeighbourFinder
    thermostat::Thermostat
    loggers::Dict{String, <:Logger}
    timestep::T
    n_steps::Int
    n_steps_made::Vector{Int}
end

function Simulation(;
                    simulator,
                    atoms,
                    specific_inter_lists=(),
                    general_inters=(),
                    coords,
                    velocities=zero(coords),
                    temperature=0.0,
                    box_size,
                    neighbour_finder=NoNeighbourFinder(),
                    thermostat=NoThermostat(),
                    loggers=Dict{String, Logger}(),
                    timestep=0.0,
                    n_steps=0,
                    n_steps_made=[0])
    T = typeof(timestep)
    A = eltype(atoms)
    C = typeof(coords)
    GI = typeof(general_inters)
    SI = typeof(specific_inter_lists)
    return Simulation{T, A, C, GI, SI}(
                simulator, atoms, specific_inter_lists, general_inters, coords,
                velocities, temperature, box_size, neighbour_finder, thermostat,
                loggers, timestep, n_steps, n_steps_made)
end

function Base.show(io::IO, s::Simulation)
    print(io, "Simulation with ", length(s.coords), " atoms, ",
                typeof(s.simulator), " simulator, ", s.timestep, " timestep, ",
                s.n_steps, " steps, ", first(s.n_steps_made), " steps made")
end


# interfacing with other packages

using LinearAlgebra
using LinearAlgebra: BlasFloat

Base.:(\)(A::Union{UpperTriangular{T, S},LowerTriangular{T, S}}, B::CuMatrix{T}) where {T<:BlasFloat, S<:AbstractCuSparseMatrix{T}}       = sm('N',A,B,'O')
Base.:(\)(transA::Transpose{T, UpperTriangular{T, S}}, B::CuMatrix{T}) where {T<:BlasFloat, S<:AbstractCuSparseMatrix{T}} = sm('T',parent(transA),B,'O')
Base.:(\)(transA::Transpose{T, LowerTriangular{T, S}}, B::CuMatrix{T}) where {T<:BlasFloat, S<:AbstractCuSparseMatrix{T}} = sm('T',parent(transA),B,'O')
Base.:(\)(adjA::Adjoint{T, UpperTriangular{T, S}},B::CuMatrix{T}) where {T<:BlasFloat, S<:AbstractCuSparseMatrix{T}} = sm('C',parent(adjA),B,'O')
Base.:(\)(adjA::Adjoint{T, LowerTriangular{T, S}},B::CuMatrix{T}) where {T<:BlasFloat, S<:AbstractCuSparseMatrix{T}} = sm('C',parent(adjA),B,'O')

LinearAlgebra.mul!(C::CuVector{T},A::CuSparseMatrix,B::CuVector) where {T} = mv!('N',one(T),A,B,zero(T),C,'O')
LinearAlgebra.mul!(C::CuVector{T},transA::Transpose{<:Any,<:CuSparseMatrix},B::CuVector) where {T} = mv!('T',one(T),parent(transA),B,zero(T),C,'O')
LinearAlgebra.mul!(C::CuVector{T},adjA::Adjoint{<:Any,<:CuSparseMatrix},B::CuVector) where {T} = mv!('C',one(T),parent(adjA),B,zero(T),C,'O')
LinearAlgebra.mul!(C::CuVector{T},A::HermOrSym{T,<:CuSparseMatrix{T}},B::CuVector{T}) where T = mv!('N',one(T),A,B,zero(T),C,'O')
LinearAlgebra.mul!(C::CuVector{T},transA::Transpose{<:Any, <:HermOrSym{T,<:CuSparseMatrix{T}}},B::CuVector{T}) where {T} = mv!('T',one(T),parent(transA),B,zero(T),C,'O')
LinearAlgebra.mul!(C::CuVector{T},adjA::Adjoint{<:Any, <:HermOrSym{T,<:CuSparseMatrix{T}}},B::CuVector{T}) where {T} = mv!('C',one(T),parent(adjA),B,zero(T),C,'O')

LinearAlgebra.mul!(C::CuMatrix{T},A::CuSparseMatrix{T},B::CuMatrix{T}) where {T} = mm2!('N','N',one(T),A,B,zero(T),C,'O')
LinearAlgebra.mul!(C::CuMatrix{T},A::CuSparseMatrix{T},transB::Transpose{<:Any, <:CuMatrix{T}})  where {T} = mm2!('N','T',one(T),A,parent(transB),zero(T),C,'O')
LinearAlgebra.mul!(C::CuMatrix{T},transA::Transpose{<:Any, <:CuSparseMatrix{T}},B::CuMatrix{T})  where {T} = mm2!('T','N',one(T),parent(transA),B,zero(T),C,'O')
LinearAlgebra.mul!(C::CuMatrix{T},transA::Transpose{<:Any, <:CuSparseMatrix{T}},transB::Transpose{<:Any, <:CuMatrix{T}}) where {T} = mm2!('T','T',one(T),parent(transA),parent(transB),zero(T),C,'O')
LinearAlgebra.mul!(C::CuMatrix{T},adjA::Adjoint{<:Any, <:CuSparseMatrix{T}},B::CuMatrix{T})  where {T} = mm2!('C','N',one(T),parent(adjA),B,zero(T),C,'O')

LinearAlgebra.mul!(C::CuMatrix{T},A::HermOrSym{<:Number, <:CuSparseMatrix},B::CuMatrix) where {T} = mm!('N',one(T),A,B,zero(T),C,'O')
LinearAlgebra.mul!(C::CuMatrix{T},transA::Transpose{<:Any, <:HermOrSym{<:Number, <:CuSparseMatrix}},B::CuMatrix) where {T} = mm!('T',one(T),parent(transA),B,zero(T),C,'O')
LinearAlgebra.mul!(C::CuMatrix{T},adjA::Adjoint{<:Any, <:HermOrSym{<:Number, <:CuSparseMatrix}},B::CuMatrix) where {T} = mm!('C',one(T),parent(adjA),B,zero(T),C,'O')

Base.:(\)(A::Union{UpperTriangular{T, S},LowerTriangular{T, S}}, B::CuVector{T}) where {T<:BlasFloat, S<:AbstractCuSparseMatrix{T}}       = sv2('N',A,B,'O')
Base.:(\)(transA::Transpose{T, UpperTriangular{T, S}},B::CuVector{T}) where {T<:BlasFloat, S<:AbstractCuSparseMatrix{T}} = sv2('T',parent(transA),B,'O')
Base.:(\)(transA::Transpose{T, LowerTriangular{T, S}},B::CuVector{T}) where {T<:BlasFloat, S<:AbstractCuSparseMatrix{T}} = sv2('T',parent(transA),B,'O')
Base.:(\)(adjA::Adjoint{T, UpperTriangular{T, S}},B::CuVector{T}) where {T<:BlasFloat, S<:AbstractCuSparseMatrix{T}}  = sv2('C',parent(adjA),B,'O')
Base.:(\)(adjA::Adjoint{T, LowerTriangular{T, S}},B::CuVector{T}) where {T<:BlasFloat, S<:AbstractCuSparseMatrix{T}}  = sv2('C',parent(adjA),B,'O')
Base.:(\)(A::AbstractTriangular{T,CuSparseMatrixHYB{T}},B::CuVector{T})       where T = sv('N',A,B,'O')
Base.:(\)(transA::Transpose{T, UpperTriangular{T, CuSparseMatrixHYB{T}}},B::CuVector{T}) where {T<:BlasFloat} = sv('T',parent(transA),B,'O')
Base.:(\)(transA::Transpose{T, LowerTriangular{T, CuSparseMatrixHYB{T}}},B::CuVector{T}) where {T<:BlasFloat} = sv('T',parent(transA),B,'O')
Base.:(\)(adjA::Adjoint{T, UpperTriangular{T, CuSparseMatrixHYB{T}}},B::CuVector{T}) where {T<:BlasFloat} = sv('C',parent(adjA),B,'O')
Base.:(\)(adjA::Adjoint{T, LowerTriangular{T, CuSparseMatrixHYB{T}}},B::CuVector{T}) where {T<:BlasFloat} = sv('C',parent(adjA),B,'O')

Base.:(+)(A::Union{CuSparseMatrixCSR,CuSparseMatrixCSC},B::Union{CuSparseMatrixCSR,CuSparseMatrixCSC}) = geam(A,B,'O','O','O')
Base.:(-)(A::Union{CuSparseMatrixCSR,CuSparseMatrixCSC},B::Union{CuSparseMatrixCSR,CuSparseMatrixCSC}) = geam(A,-one(eltype(A)),B,'O','O','O')


using XLSX, DataFrames
include("functions.jl")
#This file contains the exported functions
################################################################################
##########################  Default wind profile ###############################
df = DataFrame(XLSX.readtable("../cordoba/src/economics/input_data/data.xlsx", "wind_data")...)
wnd=wndF_wndPrf([getproperty(df,Symbol("Norther"))])
wind_module.save_wind4_module(wnd,"Norther")
################################################################################
########################### Base equipment functions  ##########################
################################################################################

#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#NOTE EENS is set to zero for flexlan !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

#Cost of HVAC cable of capacity mva and length km
function AC_cbl(mva,km)
    cbl=cable();cbl.mva=mva;cbl.wnd="Norther"
    ac_cbl=optimal_ac_cable(cbl,get_220kV_cables(),km,get_Cost_Data())
    if (ac_cbl.num<1); ac_cbl.costs.cpx_p=1e12; end
    return ac_cbl
end
#Cost of HVDC cable of capacity mva and length km

function DC_cbl(mva,km)
    cbl=cable();cbl.mva=mva;cbl.wnd="Norther"
    dc_cbl=optimal_hvdc_cable(cbl,get_300kV_cables(),km,get_Cost_Data())
    return dc_cbl
end
#Cost of AC platform of capacity mva and depth m
function AC_plat(mva, m::Float64=17.1)
    plat=platform();
    plat.mva=mva
    plat.wnd="Norther"
    plat=cost_ac_platform(plat,get_Cost_Data())
    plat=adjust_base_ac_platform(plat,get_Cost_Data())
    #adjusts costs based on depth default and neutral depth is 17.1m
    multiplier=0.0136*abs(m) + 0.7676
    plat.costs.cm=plat.costs.cm*multiplier
    plat.costs.cpx=plat.costs.cpx*multiplier
    plat.costs.ttl=plat.costs.cpx+plat.costs.cm
    return plat
end
#Cost of DC platform of capacity mva and depth m
function DC_plat(mva, m::Float64=17.1)
    plat=platform();
    plat.mva=mva
    plat.wnd="Norther"
    plat=cost_dc_platform(plat,get_Cost_Data())
    plat=adjust_base_dc_platform(plat,get_Cost_Data())
    #adjusts costs based on depth default and neutral depth is 17.1m
    multiplier=0.0136*abs(m) + 0.7676
    plat.costs.cm=plat.costs.cm*multiplier
    plat.costs.cpx=plat.costs.cpx*multiplier
    plat.costs.ttl=plat.costs.cpx+plat.costs.cm
    return plat
end
#offshore transformer of capacity mva
function off_xfm(mva)
    xfo_off=transformer()
    xfo_off.relia=get_offshore_xfo_failure_data(xfo_off)
    xfo_off.wnd="Norther"
    xfo_off.mva=mva
    xfo_off=xfo_oss(xfo_off,get_Cost_Data(),get_Xfo_Data())
    return xfo_off
end
#onshore transformer of capacity mva
function on_xfm(mva)
    xfo_on=transformer()
    xfo_on.relia=get_offshore_xfo_failure_data(xfo_on)
    xfo_on.wnd="Norther"
    xfo_on.mva=mva
    xfo_pcc(xfo_on,get_Cost_Data(),get_Xfo_Data())
    return xfo_on
end
#cost of offshore converter of size mva
function off_conv(mva)
    conv_off=converter()
    conv_off.relia=get_offshore_conv_failure_data(conv_off)
    conv_off.wnd="Norther"
    conv_off.mva=mva
    conv_off.elec.mva=mva
    conv_off.num=1
    conv_off=cost_hvdc_oss(conv_off,get_Cost_Data())
    conv_off=adjust_base_hvdc_offshore_converter(conv_off,get_Cost_Data())
    return conv_off
end
#cost of onshore converter of size mva
function on_conv(mva)
    conv_on=converter()
    conv_on.relia=get_onshore_conv_failure_data(conv_on)
    conv_on.wnd="Norther"
    conv_on.mva=mva
    conv_on.elec.mva=mva
    conv_on.num=1
    conv_on=cost_hvdc_pcc(conv_on,get_Cost_Data())
    conv_on=adjust_base_hvdc_onshore_converter(conv_on,get_Cost_Data())
    return conv_on
end
################################################################################
########################### Finding MV max distance ############################
################################################################################
#**
function find_max_mv_transmission(owpps,database)
    ks=get_Cost_Data()
    for owpp in owpps
        pnt0=0.001
        pnt1=deepcopy(database["cables"]["66.0"][string(owpp.mva)][length(database["cables"]["66.0"][string(owpp.mva)])].length)
        mv_cable_pnt0=deepcopy(mvac_cable(owpp.mva,pnt0,owpp.wnd,database["cables"]["66.0"][string(owpp.mva)],ks))
        mv_cable_pnt1=deepcopy(mvac_cable(owpp.mva,pnt1,owpp.wnd,database["cables"]["66.0"][string(owpp.mva)],ks))
        plat=platform();plat.costs.ttl=0.0
        plat.mva=owpp.mva
        plat.wnd=owpp.wnd
        plat=cost_ac_platform(plat,ks)
        plat=adjust_base_ac_platform(plat,ks)
        ####################################### Addition
        xfo_off=transformer()
        xfo_off.relia=get_offshore_xfo_failure_data(xfo_off)
        xfo_off.wnd=owpp.wnd
        xfo_off.mva=owpp.mva
        xfo_off=xfo_oss(xfo_off,ks,get_Xfo_Data())
        ########################################
        plat.costs.ttl=plat.costs.ttl+xfo_off.costs.ttl
        np=0
        if (database["bits"]["hvac"]==true || database["bits"]["mpc_ac"]==true)
            hv_cable_pnt0_220=deepcopy(hvac_cable(owpp.mva,pnt0,owpp.wnd,database["cables"]["220.0"][string(owpp.mva)],ks))
            hv_cable_pnt0_400=deepcopy(hvac_cable(owpp.mva,pnt0,owpp.wnd,database["cables"]["400.0"][string(owpp.mva)],ks))
            if (hv_cable_pnt0_220.costs.ttl<hv_cable_pnt0_400.costs.ttl)
                hv_cable_pnt0=hv_cable_pnt0_220
                hv_c="220.0"
            else
                hv_cable_pnt0=hv_cable_pnt0_400
                hv_c="400.0"
            end
        elseif (database["bits"]["hvdc"]==true)
            hv_c="300.0"
        end

        if (mv_cable_pnt0.costs.ttl<hv_cable_pnt0.costs.ttl+plat.costs.ttl)
            hv_cable_pnt1=deepcopy(hvac_cable(owpp.mva,pnt1,owpp.wnd,database["cables"][hv_c][string(owpp.mva)],ks))
            #if AC cable range is maxed out this backs it up to a feasible distance
            while (hv_cable_pnt1.costs.ttl==Inf)
                pnt1=pnt1-25
                hv_cable_pnt1=deepcopy(hvac_cable(owpp.mva,pnt1,owpp.wnd,database["cables"][hv_c][string(owpp.mva)],ks))
                mv_cable_pnt1=deepcopy(mvac_cable(owpp.mva,pnt1,owpp.wnd,database["cables"]["66.0"][string(owpp.mva)],ks))
            end
            np=pnt1
            if (mv_cable_pnt1.costs.ttl>hv_cable_pnt1.costs.ttl+plat.costs.ttl)
                while (pnt1-pnt0>1)
                    np=(pnt0+pnt1)/2
                    mv_cable_np=deepcopy(mvac_cable(owpp.mva,np,owpp.wnd,database["cables"]["66.0"][string(owpp.mva)],ks))
                    hv_cable_np=deepcopy(hvac_cable(owpp.mva,np,owpp.wnd,database["cables"][hv_c][string(owpp.mva)],ks))
                    if (mv_cable_np.costs.ttl<hv_cable_np.costs.ttl+plat.costs.ttl)
                        pnt0=deepcopy(np)
                    elseif (mv_cable_np.costs.ttl>hv_cable_np.costs.ttl+plat.costs.ttl)
                        pnt1=deepcopy(np)
                    else
                        break
                    end
                end
            end
        end
        ############### addition #########
        # Account for collector circuit###
        ###### 6MW/km square assumed######
        #### Diameter of area travelled###
        collector=2*sqrt((owpp.mva/6)/pi)
        np_minus_collector=np-collector
        if (np_minus_collector<0)
            #np_minus_collector=0
            np_minus_collector=0.1
        end
        owpp.mv_zone=deepcopy(np_minus_collector)
        ##################################
        #owpp.mv_zone=deepcopy(np)
        owpp.kV=66.0
    end
    return owpps
end

################################################################################
############################## Finding lookup table ############################
################################################################################
#**
function get_equipment_tables(km_mva_set,wnd,kv)
    cable_database=get_cable_table(km_mva_set,wnd)
    xfo_database,conv_database=get_xfoConv_table(km_mva_set,wnd)
    database=Dict{String, Dict{String,Any}}()
    push!(database,"cables"=>cable_database)
    push!(database,"transformers"=>xfo_database)
    push!(database,"converters"=>conv_database)

    #vheck if hvdc is to be included
    bits=Dict([("hvdc", true), ("mpc_ac", false), ("hvac", true)])
    push!(database,"bits"=>bits)
    hvdc_bit=check4hvdc(database,km_mva_set,wnd,kv)

    #check hvac connections
    hvac_bit=true
    if (hvdc_bit==true)
        hvac_bit=check4hvac(database,km_mva_set,wnd,kv)
    end
    mpc_bit=false

    #final bits sets
    bits=Dict([("hvdc", hvdc_bit), ("mpc_ac", mpc_bit), ("hvac", hvac_bit)])
    push!(database,"bits"=>bits)
    database["cables"]=set_cables_per_km_cost(database["cables"],km_mva_set)
    return database
end

################################################################################
######################## Optimal equipment using lookup table ##################
################################################################################
#finds the offshore transformer using the look up table
#when increasing meshed transformer xfo_oss_meshed is used
#**
function xfo_oss(xfo0,ks,xfo_data)
    xfo=transformer()
    xfo.costs.ttl=Inf
    xfo.mva=xfo0.mva
    for xd in xfo_data
        if ((xd+10<=xfo0.mva) && (6*xd>xfo0.mva))
            xfo0.num=1
            xfo0.elec.mva=xd
            while ((xfo0.num*xfo0.elec.mva)<xfo0.mva)
                xfo0.num=xfo0.num+1
            end
            xfo0=cost_xfo_oss(xfo0,ks)
            if (xfo0.costs.ttl<xfo.costs.ttl)
                xfo=deepcopy(xfo0)
            end
        end
    end
    return xfo
end

#finds the onshore transformer using the look up table
#**
function xfo_pcc(xfo0,ks,xfo_data)
    xfo=transformer()
    xfo.costs.ttl=Inf
    xfo.mva=xfo0.mva
    for xd in xfo_data
        if ((xd+10<=xfo0.mva) && ((3*xd>xfo0.mva) || ((xfo0.mva>2250) && (6*xd>xfo0.mva))))
            xfo0.num=1
            xfo0.elec.mva=xd
            while ((xfo0.num*xfo0.elec.mva)<xfo0.mva)
                xfo0.num=xfo0.num+1
            end
            xfo0=cost_xfo_pcc(xfo0,ks)
            if (xfo0.costs.ttl<xfo.costs.ttl)
                xfo=deepcopy(xfo0)
            end
        end
    end
    return xfo
end

#finds the hvdc cable using the look up table
#**
function hvdc_cable(mva,km,wnd,cable_array,ks)
    cbl=deepcopy(cable_array[1])
    cbl.wnd=wnd
    cbl.length=km
    cbl=cost_hvdc_cable(cbl,ks)
    return cbl
end

#finds the hvac cable using the look up table
#**
function hvac_cable(mva,km,wnd,cable_array,ks)
    cbl=cable()
    cbl.costs.ttl=Inf
    cbl.costs.grand_ttl=Inf
    for cbl0 in cable_array
        if (km<=cbl0.length+0.01)
            #+0.01 is just to be certain floating point error does not occur
            cbl=deepcopy(cbl0)
            break
        end
    end
    if (cbl.size!=0.0)
        cbl.wnd=wnd
        cbl.length=km
        cbl.elec.mva=get_newQ_Capacity(cbl.elec.freq,km,cbl.elec.volt,cbl.elec.farrad,cbl.elec.amp)
        cbl=get_cbl_failure_data(cbl)
        cbl=cost_hvac_cable(cbl,ks)
    end
    return cbl
end

#finds the mvac cable using the look up table
#**
function mvac_cable(mva,km,wnd,cable_array,ks)
    cbl=cable()
    cbl.costs.ttl=Inf
    cbl.costs.grand_ttl=Inf
    for cbl0 in cable_array
        if (km<=cbl0.length+0.001)#+0.001 is just to be certain floating point error does not occur
            cbl=deepcopy(cbl0)
            break
        end
    end
    max_in_parallel=12#if changing check economics/database/functions function optimal_ac_cable(cbl0,cbl_data,km,ks)
    if (cbl.num>max_in_parallel && cbl.size!=0.0)#66kv up to 2000MW eception
        cbl.mva=cbl.mva/2
        cbl.num=cbl.num/2
        cbl.costs.perkm_cpx=cbl.costs.perkm_cpx/2
        cbl.elec.mva=get_newQ_Capacity(cbl.elec.freq,km,cbl.elec.volt,cbl.elec.farrad,cbl.elec.amp)
        cbl.wnd=wnd
        cbl.length=km
        cbl=get_cbl_failure_data(cbl)
        cbl=cost_mvac_cable(cbl,ks)
        cbl.num=cbl.num*2
        cbl.mva=cbl.mva*2
        cbl.costs.perkm_cpx=cbl.costs.perkm_cpx*2
        cbl.costs.sg=cbl.costs.sg*2
        cbl.costs.cpx_p=cbl.costs.cpx_p*2
        cbl.costs.cpx_i=cbl.costs.cpx_i*2
        cbl.costs.rlc=cbl.costs.rlc*2
        cbl.costs.cm=cbl.costs.cm*2
        cbl.costs.eens=cbl.costs.eens*2
        cbl.costs.ttl=cbl.costs.ttl*2
        cbl.costs.grand_ttl=cbl.costs.ttl
    elseif (cbl.size!=0.0)
        cbl.elec.mva=get_newQ_Capacity(cbl.elec.freq,km,cbl.elec.volt,cbl.elec.farrad,cbl.elec.amp)
        cbl.wnd=wnd
        cbl.length=km
        cbl=get_cbl_failure_data(cbl)
        cbl=cost_mvac_cable(cbl,ks)
        if (cbl.costs.ttl==Inf)
            cbl.costs.perkm_ttl=10^9
            cbl.costs.grand_ttl=0.0
        else
            cbl.costs.grand_ttl=cbl.costs.ttl
        end
    end
    return cbl
end

#Converter
#calculates the cost of an offshore HVDC converter
#each line needing a converter shares the total before the fixed cost is added at the end
#**
function cost_hvdc_oss(conv,ks)
    #capex converter
    conv.costs.cpx=capex_hvdc(conv,ks)
    #cost of losses
    conv.costs.tlc=cost_tlc(conv,ks)
    #corrective maintenance
    conv.costs.cm=cost_cm(conv.costs.cpx,ks.opx_co)
    #eens calculation
    conv.costs.eens=cost_eens(conv,ks)
    #totals the xfo cost
    conv.costs.ttl=cost_conv_sum(conv)
    return conv
end

#once all incoming lines with converters are combined the fixed cost is added
#**
function adjust_base_hvdc_offshore_converter(conv,ks)
    #adjust to add base cost capex converter
    conv.costs.cpx=conv.costs.cpx+ks.conv_d
    #corrective maintenance
    conv.costs.cm=conv.costs.cm+ks.conv_d*ks.opx_co*npv_years()
    #totals the converter cost
    conv.costs.ttl=cost_conv_sum(conv)
    return conv
end

#calculates the cost of an onshore HVDC converter
#each line needing a converter shares the total before the fixed cost is added at the end
#**
function cost_hvdc_pcc(conv,ks)
    #capex converter
    conv.costs.cpx=capex_hvdc(conv,ks)
    #cost of losses
    conv.costs.tlc=cost_tlc(conv,ks)
    #corrective maintenance
    conv.costs.cm=cost_cm(conv.costs.cpx,ks.opx_cp)
    #eens calculation
    conv.costs.eens=cost_eens(conv,ks)
    #totals the xfo cost
    conv.costs.ttl=cost_conv_sum(conv)
    return conv
end

#once all incoming lines with converters are combined the fixed cost is added
#**
function adjust_base_hvdc_onshore_converter(conv,ks)
    #adjust to add base cost capex converter
    conv.costs.cpx=conv.costs.cpx+ks.conv_d
    #corrective maintenance
    conv.costs.cm=conv.costs.cm+ks.conv_d*ks.opx_cp*npv_years()
    #totals the converter cost
    conv.costs.ttl=cost_conv_sum(conv)
    return conv
end

################################### platform ###################################
#calculates incremental cost of an AC platform NOT including fixed cost
#**
function cost_ac_platform(plat,ks)
    #capex oss
    plat.costs.cpx=capex_plat_ac(plat,ks)
    #corrective maintenance
    plat.costs.cm=cost_cm(plat.costs.cpx,ks.opx_pl)
    #totals the plat cost
    plat.costs.ttl=plat.costs.cpx+plat.costs.cm
    return plat
end

#once all incoming lines established the fixed cost is added to the of incremental sum - AC
#**
function adjust_base_ac_platform(plat,ks)
    #adjust to add base cost capex converter
    plat.costs.cpx=plat.costs.cpx+ks.pac_f
    #corrective maintenance
    plat.costs.cm=plat.costs.cm+ks.pac_f*ks.opx_pl*npv_years()
    #totals the converter cost
    plat.costs.ttl=plat.costs.cpx+plat.costs.cm
    return plat
end

#calculates incremental cost of an DC platform NOT including fixed cost
#**
function cost_dc_platform(plat,ks)
    #capex oss
    plat.costs.cpx=capex_plat_dc(plat,ks)
    #corrective maintenance
    plat.costs.cm=cost_cm(plat.costs.cpx,ks.opx_pl)
    #totals the plat cost
    plat.costs.ttl=plat.costs.cpx+plat.costs.cm
    return plat
end

#once all incoming lines established the fixed cost is added to the of incremental sum - DC
#**
function adjust_base_dc_platform(plat,ks)
    #adjust to add base cost capex converter
    plat.costs.cpx=plat.costs.cpx+ks.pdc_h
    #corrective maintenance
    plat.costs.cm=plat.costs.cm+ks.pdc_h*ks.opx_pl*npv_years()
    #totals the converter cost
    plat.costs.ttl=plat.costs.cpx+plat.costs.cm
    return plat
end


function construct_device!(
    psi_container::PSIContainer,
    sys::PSY.System,
    model::DeviceModel{H, D},
    ::Type{S},
) where {
    H <: PSY.HydroGen,
    D <: AbstractHydroDispatchFormulation,
    S <: PM.AbstractPowerModel,
}
    devices = get_available_components(H, sys)

    if !validate_available_devices(H, devices)
        return
    end

    #Variables
    activepower_variables!(psi_container, devices)
    reactivepower_variables!(psi_container, devices)

    #Constraints
    activepower_constraints!(psi_container, devices, model, S, get_feedforward(model))
    reactivepower_constraints!(psi_container, devices, model, S, get_feedforward(model))
    feedforward!(psi_container, H, get_feedforward(model))

    #Cost Function
    cost_function(psi_container, devices, D, S)

    return
end

function construct_device!(
    psi_container::PSIContainer,
    sys::PSY.System,
    model::DeviceModel{H, HydroDispatchReservoirFlow},
    ::Type{S},
) where {H <: PSY.HydroGen, S <: PM.AbstractPowerModel}
    devices = get_available_components(H, sys)

    if !validate_available_devices(H, devices)
        return
    end

    #Variables
    activepower_variables!(psi_container, devices)
    reactivepower_variables!(psi_container, devices)

    #Constraints
    activepower_constraints!(psi_container, devices, model, S, get_feedforward(model))
    reactivepower_constraints!(psi_container, devices, model, S, get_feedforward(model))
    energy_limit_constraints!(psi_container, devices, model, S, get_feedforward(model))
    feedforward!(psi_container, H, get_feedforward(model))

    #Cost Function
    cost_function(psi_container, devices, HydroDispatchReservoirStorage, S)

    return
end

#=
# All Hydro UC formulations are currently not supported
function construct_device!(
    psi_container::PSIContainer,
    sys::PSY.System,
    model::DeviceModel{H,D},
    ::Type{S};

) where {H<:PSY.HydroGen,D<:AbstractHydroUnitCommitment,S<:PM.AbstractPowerModel}
    devices = get_available_components(H, sys)

    if !validate_available_devices(H, devices)
        return
    end

    #Variables
    activepower_variables!(psi_container, devices)
    reactivepower_variables!(psi_container, devices)

    #Initial Conditions
    initial_conditions!(psi_container, devices, model.formulation)

    #Constraints
    activepower_constraints!(psi_container, devices, model, S,get_feedforward(model))
    reactivepower_constraints!(psi_container, devices, model, S,get_feedforward(model))
    commitment_constraints!(psi_container, devices, model, S,get_feedforward(model))
    feedforward!(psi_container, H,get_feedforward(model))

    #Cost Function
    cost_function(psi_container, devices, D, S)

    return
end
=#

function construct_device!(
    psi_container::PSIContainer,
    sys::PSY.System,
    model::DeviceModel{H, D},
    ::Type{S},
) where {
    H <: PSY.HydroGen,
    D <: AbstractHydroDispatchFormulation,
    S <: PM.AbstractActivePowerModel,
}
    devices = get_available_components(H, sys)

    if !validate_available_devices(H, devices)
        return
    end

    #Variables
    activepower_variables!(psi_container, devices)

    #Constraints
    activepower_constraints!(psi_container, devices, model, S, get_feedforward(model))
    feedforward!(psi_container, H, get_feedforward(model))

    #Cost Function
    cost_function(psi_container, devices, D, S)

    return
end

function construct_device!(
    psi_container::PSIContainer,
    sys::PSY.System,
    model::DeviceModel{H, HydroDispatchReservoirFlow},
    ::Type{S},
) where {H <: PSY.HydroGen, S <: PM.AbstractActivePowerModel}
    devices = get_available_components(H, sys)

    if !validate_available_devices(H, devices)
        return
    end

    #Variables
    activepower_variables!(psi_container, devices)

    #Constraints
    activepower_constraints!(psi_container, devices, model, S, get_feedforward(model))
    energy_limit_constraints!(psi_container, devices, model, S, get_feedforward(model))
    feedforward!(psi_container, H, get_feedforward(model))

    #Cost Function
    cost_function(psi_container, devices, HydroDispatchReservoirFlow, S)

    return
end

function construct_device!(
    psi_container::PSIContainer,
    sys::PSY.System,
    model::DeviceModel{H, HydroDispatchReservoirStorage},
    ::Type{S},
) where {H <: PSY.HydroGen, S <: PM.AbstractActivePowerModel}
    devices = get_available_components(H, sys)

    if !validate_available_devices(H, devices)
        return
    end

    #Variables
    activepower_variables!(psi_container, devices)
    energy_variables!(psi_container, devices)
    spillage_variables!(psi_container, devices)

    #Initial Conditions
    storage_energy_init(psi_container.initial_conditions, devices)

    #Constraints
    activepower_constraints!(psi_container, devices, model, S, get_feedforward(model))
    energy_balance_constraint!(psi_container, devices, model, S, get_feedforward(model))
    feedforward!(psi_container, H, get_feedforward(model))

    #Cost Function
    cost_function(psi_container, devices, HydroDispatchReservoirStorage, S)

    return
end

#=
function construct_device!(
    psi_container::PSIContainer,
    sys::PSY.System,
    model::DeviceModel{H,D},
    ::Type{S};

) where {H<:PSY.HydroGen,D<:AbstractHydroUnitCommitment,S<:PM.AbstractActivePowerModel}

    devices = get_available_components(H, sys)

    if !validate_available_devices(H, devices)
        return
    end

    #Variables
    activepower_variables!(psi_container, devices)
    commitment_variables!(psi_container, devices)

    #Initial Conditions
    initial_conditions!(psi_container, devices, model.formulation)

    #Constraints
    activepower_constraints!(psi_container, devices, model, S,get_feedforward(model))
    commitment_constraints!(psi_container, devices, model, S,get_feedforward(model))
    feedforward!(psi_container, H,get_feedforward(model))

    #Cost Function
    cost_function(psi_container, devices, D, S)

    return
end
=#

function construct_device!(
    psi_container::PSIContainer,
    sys::PSY.System,
    model::DeviceModel{H, FixedOutput},
    ::Type{S},
) where {H <: PSY.HydroGen, S <: PM.AbstractPowerModel}
    devices = get_available_components(H, sys)

    if !validate_available_devices(H, devices)
        return
    end

    nodal_expression!(psi_container, devices, S)

    return
end

# Currently no Hydro device supports a Unit commiment formulation
#=
function construct_device!(
    psi_container::PSIContainer,
    sys::PSY.System,
    model::DeviceModel{PSY.HydroDispatch, D},
    ::Type{S};

) where {D <: AbstractHydroUnitCommitment, S <: PM.AbstractPowerModel}
    @warn("The Formulation $(D) only applies to Dispatchable Hydro, *
               Consider Changing the Device Formulation to FixedOutput")

    construct_device!(
        psi_container,
        DeviceModel(PSY.HydroDispatch, FixedOutput),
        S

    )
end
=#

function construct_device!(
    psi_container::PSIContainer,
    sys::PSY.System,
    model::DeviceModel{PSY.HydroDispatch, FixedOutput},
    ::Type{S},
) where {S <: PM.AbstractPowerModel}
    devices = get_available_components(PSY.HydroDispatch, sys)

    if !validate_available_devices(PSY.HydroDispatch, devices)
        return
    end

    nodal_expression!(psi_container, devices, S)

    return
end


#=

don't want to fall into the same trap as with JAMES!

APBE and Pygpe both are more suited for atom-structure-like sims,
don't seem to support simple dielectrics?
fenics requires it in a "weak form",

=#

using PhysicalConstants.CODATA2018

VacuumElectricPermittivity

Nx, Ny, Nz = 100

A = zeros(Nx,Ny,Nz)

dielectric = ones(Nx,Ny,Nz)
charge_distribution = zeros(Nx,Ny,Nz)

kappa_external =

function poisson_boltzmann()
    for x = 1:Nx-2
        for y = 1:Nx-2
            for z = 1:Nx-2
        (2.0 / )
        #wait, julia is 1 indexed?
        # wtf?
        a1 =
    end

end


mutable struct Chip
    gfx::Vector{UInt8}      # Total amount of pixels: 2048
    key::Vector{UInt8}

    pc::UInt16              # Program counter
    opcode::UInt16          # Current opcode
    I::UInt16               # Index register
    sp::UInt16              # Stack pointer

    V::Vector{UInt8}        # V-regs (V0-VF)
    stack::Vector{UInt16}   # Stack (16 levels)
    memory::Vector{UInt8}   # Memory (size=4k)

    delay_timer::Char       # Delay timer
    sound_timer::Char       # Sound timer

    drawFlag::Bool
end

"Construct Chip-8"
function Chip()
    pc = 0x201      # Program counter stars at 0x200 + 1 (1-based index)
    opcode = 0x00   # Reset current opcode
    I = 0x00        # Reset index register
    sp = 0x00       # Reset stack pointer

    # clear display
    gfx = zeros(UInt8, 2048)

    # clear stack
    stack = zeros(UInt16, 16)
    V = zeros(UInt8, 16)
    key = V

    # clear memory
    memory = zeros(UInt8, 4096)

    # load fontset
    memory[1:80] = [
        0xF0, 0x90, 0x90, 0x90, 0xF0, #0
        0x20, 0x60, 0x20, 0x20, 0x70, #1
        0xF0, 0x10, 0xF0, 0x80, 0xF0, #2
        0xF0, 0x10, 0xF0, 0x10, 0xF0, #3
        0x90, 0x90, 0xF0, 0x10, 0x10, #4
        0xF0, 0x80, 0xF0, 0x10, 0xF0, #5
        0xF0, 0x80, 0xF0, 0x90, 0xF0, #6
        0xF0, 0x10, 0x20, 0x40, 0x40, #7
        0xF0, 0x90, 0xF0, 0x90, 0xF0, #8
        0xF0, 0x90, 0xF0, 0x10, 0xF0, #9
        0xF0, 0x90, 0xF0, 0x90, 0x90, #A
        0xE0, 0x90, 0xE0, 0x90, 0xE0, #B
        0xF0, 0x80, 0x80, 0x80, 0xF0, #C
        0xE0, 0x90, 0x90, 0x90, 0xE0, #D
        0xF0, 0x80, 0xF0, 0x80, 0xF0, #E
        0xF0, 0x80, 0xF0, 0x80, 0x80  #F
    ]

    # reset timers
    delay_timer = 0x00
    sound_timer = 0x00

    Chip(gfx, key,
         pc, opcode, I, sp,
         V, stack, memory,
         delay_timer, sound_timer,
         true)
end

"Read in c8 file"
function loadApplication(c8::Chip, fname::String)
    i = 512 + 1
    open(fname) do f
        while !eof(f)
            buffer = read(f, UInt8)
            c8.memory[i] = buffer

            i += 1
        end
    end
end

"Start the emulation"
function emulateCycle(c8::Chip)
    # fetch the opcode
    c8.opcode = UInt16(c8.memory[c8.pc]) << 8 |
                c8.memory[c8.pc + 1]

    # get the first 4 bits
    first4 = c8.opcode & 0xF000

    # get X and Y
    X = c8.opcode & 0x0f00 >> 8 + 1
    Y = c8.opcode & 0x00f0 >> 4 + 1

    # process opcode
    if first4 == 0x0000
        # Clears the screen.
        if c8.opcode == 0x00e0
            fill!(c8.gfx, 0x00)
        # Returns from a subroutine.
        elseif c8.opcode == 0x00ee
            c8.sp -= 1
            c8.pc = c8.stack[c8.sp + 1]
            c8.pc += 2
        else
            warn("Unknown opcode")
        end
    # 0x1NNN Jumps to address NNN.
    elseif first4 == 0x1000
        c8.pc = c8.opcode & 0x0FFF + 0x0001
    # 0x2NNN Calls subroutine at NNN.
    elseif first4 == 0x2000
        c8.stack[c8.sp+1] = c8.pc
        c8.sp += 1
        c8.pc = c8.opcode & 0x0fff
    # 0x3XNN Skips the next instruction if VX equals NN.
    elseif first4 == 0x3000
        if c8.V[X] == c8.opcode & 0x00ff
            c8.pc += 4
        else
            c8.pc += 2
        end
    # 0x4XNN Skips the next instruction if VX doesn't equal NN.
    elseif first4 == 0x4000
        if c8.V[X] != c8.opcode & 0x00ff
            c8.pc += 4
        else
            c8.pc += 2
        end
    # 0x5XY0 Skips the next instruction if VX equals VY.
    elseif first4 == 0x5000
        if c8.V[X] == c8.V[Y]
            c8.pc += 4
        else
            c8.pc += 2
        end
    # 0x6XNN Sets VX to NN
    elseif first4 == 0x6000
        c8.V[X] = c8.opcode & 0x00ff
        c8.pc += 2
    # 0x7XNN Adds NN to VX
    elseif first4 == 0x7000
        c8.V[X] += c8.opcode & 0x00ff
        c8.pc += 2
    # 0x8XY* Manipulation of VX and VY
    elseif first4 == 0x8000
        last4 = c8.opcode & 0x000f

        # Sets VX to the value of VY.
        if last4 == 0x000
            c8.V[X] = c8.V[Y]
        # Sets VX to VX or VY. (Bitwise OR operation)
        elseif last4 == 0x0001
            c8.V[X] |= c8.V[Y]
        # Sets VX to VX and VY. (Bitwise AND operation)
        elseif last4 == 0x0002
            c8.V[X] &= c8.V[Y]
        # Sets VX to VX xor VY.
        elseif last4 == 0x0003
            c8.V[X] ⊻= c8.V[Y]
        # Adds VY to VX.
        # VF is set to 1 when there's a carry, and to 0 when there isn't.
        elseif last4 == 0x0004
            if c8.V[Y] > (0xff - c8.V[X])
                c8.V[16] = 1
            else
                c8.V[16] = 0
            end
            c8.V[X] += c8.V[Y]
        # VY is subtracted from VX.
        # VF is set to 0 when there's a borrow, and 1 when there isn't.
        elseif last4 == 0x0005
            if c8.V[Y] > c8.V[X]
                c8.V[16] = 0
            else
                c8.V[16] = 1
            end
            c8.V[X] -= c8.V[Y]
        # Shifts VX right by one
        # VF is set to the value of the least significant bit of VX
        # before the shift (modern implementation of 0x8XY6)
        elseif last4 == 0x0006
            c8.V[16] = c8.V[X] & 0x1
            c8.V[X] >>= 1
        # Sets VX to VY minus VX.
        # VF is set to 0 when there's a borrow, and 1 when there isn't.
        elseif last4 == 0x0007
            if c8.V[X] > c8.V[Y]
                c8.V[16] = 0
            else
                c8.V[16] = 1
            end
            c8.V[X] = c8.V[Y] - c8.V[X]
        # Shifts VX left by one
        # VF is set to the value of the most significant bit of VX
        # before the shift (modern implementation of 0x8XYE)
        elseif last4 == 0x000E
            c8.V[16] = c8.V[X] >> 7
            c8.V[X] <<= 1
        else
            warn("Unknown opcode [0x8000]")
        end

        c8.pc += 2
    # 0x9XY0 Skips the next instruction if VX doesn't equal VY.
    elseif first4 == 0x9000
        if c8.V[X] != c8.V[Y]
            c8.pc += 4
        else
            c8.pc += 2
        end
    # Sets I to the address NNN.
    elseif first4 == 0xa000
        I = c8.opcode & 0x0fff
        c8.pc += 2
    # Jumps to the address NNN plus V0.
    elseif first4 == 0xb000
        c8.pc = (c8.opcode & 0x0fff) + c8.V[1]
    # Sets VX to the result of a bitwise and operation on a
    # random number (Typically: 0 to 255) and NN.
    elseif first4 == 0xc000
        c8.V[X] = rand(0x00:0xff) & (c8.opcode * 0x00ff)
        c8.pc += 2
    # Draws a sprite at coordinate (VX, VY) that has a width of 8 pixels
    # and a height of N pixels. Each row of 8 pixels is read as
    # bit-coded starting from memory location I; I value doesn’t
    # change after the execution of this instruction.
    # As described above, VF is set to 1 if any screen pixels are
    # flipped from set to unset when the sprite is drawn, and to 0
    # if that doesn’t happen
    elseif first4 == 0xd000
        height = c8.opcode & 0x000f
        pixel = 0x0000

        V[16] = 0x00

        for yline in 0:(height-1)
            pixel = memory[c8.I + yline + 1]
            for xline in 0:7
                if (pixel & (0x80 >> (xline))) != 0
                    if(gfx[(V[X] + xline + ((V[Y] + yline) * 64)) + 1] == 1)
                        V[16] = 1
                        gfx[V[X] + xline + ((V[Y] + yline) * 64) + 1] ⊻= 1
                    end
                end
            end
        end

        c8.drawFlag = true
        c8.pc += 2
    elseif first4 == 0xe000
        # Skips the next instruction if the key stored in VX is pressed.
        if (c8.opcode & 0x00ff) == 0x009e
            if c8.key[c8.V[X]] != 0
                c8.pc += 4
            else
                c8.pc += 2
            end
        # Skips the next instruction if the key stored in VX isn't pressed
        elseif (c8.opcode & 0x00ff) == 0x00a1
            if c8.key[c8.V[X]] == 0
                c8.pc += 4
            else
                c8.pc += 2
            end
        else
            warn("Unknown opcode")
        end
    elseif first4 == 0xf000
        last4 = c8.opcode * 0x00ff
        # Sets VX to the value of the delay timer.
        if last4 == 0x0007
           c8.V[X] = c8.delay_timer
           c8.pc += 2
        # A key press is awaited, and then stored in VX.
        # (Blocking Operation. All instruction halted until next key event)
        elseif last4 == 0x000a
            keyPresss = false

            for i in 1:16
                if key[i] != 0
                    c8.V[X] = i
                    keyPress = true
                end
            end

            if !keyPress
                return
            end

            c8.pc += 2
        # Sets the delay timer to VX.
        elseif last4 == 0x0015
            c8.delay_timer = c8.V[X]
            c8.pc += 2
        # Sets the sound timer to VX.
        elseif last4 == 0x0018
            c8.sound_timer = c8.V[X]
            c8.pc += 2
        # Adds VX to I
        elseif last4 == 0x001e
            if c8.I + V[X] > 0xfff
                c8.V[16] = 1
            else
                c8.V[16] = 0
            end
            c8.I += c8.V[X]
            c8.pc += 2
        # Sets I to the location of the sprite for the character in VX.
        # Characters 0-F (in hexadecimal) are represented by a 4x5 font.
        elseif last4 == 0x0029
            c8.I = c8.V[X] * 0x5
            c8.pc += 2
        # Stores the Binary-coded decimal representation of VX at the addresses I, I plus 1, and I plus 2
        elseif last4 == 0x0033
            c8.memory[c8.I]     = V[X] / 100
            c8.memory[c8.I + 1] = (V[X] / 10) % 10
            c8.memory[c8.I + 2] = (V[X] % 100) % 10
            c8.pc += 2
        # Stores V0 to VX (including VX) in memory starting at address I.
        # I is increased by 1 for each value written.
        elseif last4 == 0x0055
            for i in 1:X
                c8.memory[I + i] = c8.V[i]
            end

            c8.I += X + 1
            c8.pc += 2
        # Fills V0 to VX (including VX) with values from memory starting at address I.
        # I is increased by 1 for each value written.
        elseif last4 == 0x0065
            for i in 1:X
                c8.V[i] = c8.memory[I + i]
            end

            c8.I += X + 1
            c8.pc += 2
        end
    # temporary for testing
    else
        warn("Unrecognized opcode")
        c8.pc += 2
    end
end