using Oceananigans.BoundaryConditions: FieldBoundaryConditions, regularize_field_boundary_conditions

#####
##### Velocity fields tuples
#####

"""
    VelocityFields(arch, grid, user_bcs = NamedTuple())

Return a `NamedTuple` with fields `u`, `v`, `w` initialized on the architecture `arch`
and `grid`. Boundary conditions `bcs` may be specified via a named tuple of
`FieldBoundaryCondition`s.
"""
function VelocityFields(arch, grid, user_bcs = NamedTuple())

    template = FieldBoundaryConditions()

    default_bcs = (
        u = regularize_field_boundary_conditions(template, grid, :u),
        v = regularize_field_boundary_conditions(template, grid, :v),
        w = regularize_field_boundary_conditions(template, grid, :w)
    )

    bcs = merge(default_bcs, user_bcs)

    u = XFaceField(arch, grid, bcs.u)
    v = YFaceField(arch, grid, bcs.v)
    w = ZFaceField(arch, grid, bcs.w)

    return (u=u, v=v, w=w)
end

#####
##### Tracer fields tuples
#####

"""
    TracerFields(tracer_names, arch, grid, user_bcs)

Return a `NamedTuple` with tracer fields specified by `tracer_names` initialized as
`CenterField`s on the architecture `arch` and `grid`. Boundary conditions `user_bcs`
may be specified via a named tuple of `FieldBoundaryCondition`s.
"""
function TracerFields(tracer_names, arch, grid, user_bcs)
    default_bcs = NamedTuple(name => FieldBoundaryConditions(grid, (Center, Center, Center)) for name in tracer_names)
    bcs = merge(default_bcs, user_bcs) # provided bcs overwrite defaults
    return NamedTuple(c => CenterField(arch, grid, bcs[c]) for c in tracer_names)
end

"""
    TracerFields(tracer_names, arch, grid; kwargs...)

Return a `NamedTuple` with tracer fields specified by `tracer_names` initialized as
`CenterField`s on the architecture `arch` and `grid`. Fields may be passed via optional
keyword arguments `kwargs` for each field.

This function is used by `OutputWriters.Checkpointer` and `TendencyFields`.
```
"""
TracerFields(tracer_names, arch, grid; kwargs...) =
    NamedTuple(c => c ∈ keys(kwargs) ? kwargs[c] : CenterField(arch, grid) for c in tracer_names)

# 'Nothing', or empty tracer fields
TracerFields(::Union{Tuple{}, Nothing}, arch, grid, bcs) = NamedTuple()

"Shortcut constructor for empty tracer fields."
TracerFields(::NamedTuple{(), Tuple{}}, arch, grid, bcs) = NamedTuple()

#####
##### Pressure fields tuples
#####

"""
    PressureFields(arch, grid, bcs::NamedTuple)

Return a `NamedTuple` with pressure fields `pHY′` and `pNHS` initialized as
`CenterField`s on the architecture `arch` and `grid`.  Boundary conditions `bcs`
may be specified via a named tuple of `FieldBoundaryCondition`s.
"""
function PressureFields(arch, grid, bcs=NamedTuple())

    default_pressure_boundary_conditions =
        (pHY′ = FieldBoundaryConditions(grid, (Center, Center, Center)),
         pNHS = FieldBoundaryConditions(grid, (Center, Center, Center)))

    bcs = merge(default_pressure_boundary_conditions, bcs)

    pHY′ = CenterField(arch, grid, bcs.pHY′)
    pNHS = CenterField(arch, grid, bcs.pNHS)

    return (pHY′=pHY′, pNHS=pNHS)
end

#=
function PressureFields(arch, grid::AbstractGrid{<:Any, <:Any, <:Any, <:Flat}, bcs=NamedTuple())
    default_pressure_boundary_conditions =
        (pHY′ = FieldBoundaryConditions(grid, (Center, Center, Center)),
         pNHS = FieldBoundaryConditions(grid, (Center, Center, Center)))

    bcs = merge(default_pressure_boundary_conditions, bcs)
    pNHS = CenterField(arch, grid, bcs.pNHS)

    return (; pNHS=pNHS)
end
=#

"""
    TendencyFields(arch, grid, tracer_names;
                   u = XFaceField(arch, grid),
                   v = YFaceField(arch, grid),
                   w = ZFaceField(arch, grid),
                   kwargs...)

Return a `NamedTuple` with tendencies for all solution fields (velocity fields and
tracer fields), initialized on the architecture `arch` and `grid`. Optional `kwargs`
can be specified to assign data arrays to each tendency field.
"""
function TendencyFields(arch, grid, tracer_names;
                        u = XFaceField(arch, grid),
                        v = YFaceField(arch, grid),
                        w = ZFaceField(arch, grid),
                        kwargs...)

    velocities = (u=u, v=v, w=w)

    tracers = TracerFields(tracer_names, arch, grid; kwargs...)

    return merge(velocities, tracers)
end

#####
##### Helper functions for NonhydrostaticModel constructor
#####

VelocityFields(::Nothing, arch, grid, bcs) = VelocityFields(arch, grid, bcs)
PressureFields(::Nothing, arch, grid, bcs) = PressureFields(arch, grid, bcs)

"""
    VelocityFields(proposed_velocities::NamedTuple{(:u, :v, :w)}, arch, grid, bcs)

Return a `NamedTuple` of velocity fields, overwriting boundary conditions
in `proposed_velocities` with corresponding fields in the `NamedTuple` `bcs`.
"""
function VelocityFields(proposed_velocities::NamedTuple{(:u, :v, :w)}, arch, grid, bcs)

    validate_field_tuple_grid("velocities", proposed_velocities, grid)

    u = XFaceField(arch, grid, bcs.u, proposed_velocities.u.data)
    v = YFaceField(arch, grid, bcs.v, proposed_velocities.v.data)
    w = ZFaceField(arch, grid, bcs.w, proposed_velocities.w.data)

    return (u=u, v=v, w=w)
end

"""
    TracerFields(proposed_tracers::NamedTuple, arch, grid, bcs)

Return a `NamedTuple` of tracers, overwriting boundary conditions
in `proposed_tracers` with corresponding fields in the `NamedTuple` `bcs`.
"""
function TracerFields(proposed_tracers::NamedTuple, arch, grid, bcs)

    validate_field_tuple_grid("tracers", proposed_tracers, grid)

    tracer_names = propertynames(proposed_tracers)
    tracer_fields = Tuple(CenterField(arch, grid, bcs[c], proposed_tracers[c].data) for c in tracer_names)

    return NamedTuple{tracer_names}(tracer_fields)
end

"""
    PressureFields(proposed_pressures::NamedTuple{(:pHY′, :pNHS)}, arch, grid, bcs)

Return a `NamedTuple` of pressure fields with, overwriting boundary conditions
in `proposed_tracer_fields` with corresponding fields in the `NamedTuple` `bcs`.
"""
function PressureFields(proposed_pressures::NamedTuple{(:pHY′, :pNHS)}, arch, grid, bcs)
    validate_field_tuple_grid("pressures", proposed_pressures, grid)

    pHY′ = CenterField(arch, grid, bcs.pHY′, proposed_pressures.pHY′.data)
    pNHS = CenterField(arch, grid, bcs.pNHS, proposed_pressures.pNHS.data)

    return (pHY′=pHY′, pNHS=pNHS)
end


#   This file is part of Maxima.jl. It is licensed under the MIT license
#   Copyright (c) 2016 Nathan Smith
export MExpr,
       @m_str,
       parse,
       mcall,
       convert,
       error,
       MaximaError,
       MaximaSyntaxError,
       ==,
       getindex

import Base: parse,
             convert,
             error,
             ==,
             getindex,
             *,
             split

struct MaximaError <: Exception
    errstr::String
end

Base.showerror(io::IO, err::MaximaError) = print(io, err.errstr)

struct MaximaSyntaxError <: Exception
    errstr::String
end

Base.showerror(io::IO, err::MaximaSyntaxError) = print(io, err.errstr)

const infix_ops = [:+, :-, :*, :/, :^]

isinfix(args) = args[1] in infix_ops && length(args) > 2
show_expr(io::IO, ex) = print(io, ex)

function show_expr(io::IO, expr::Expr)
    if expr.head == :call
        seperator = isinfix(expr.args) ? " $(expr.args[1]) " : ", "
        !isinfix(expr.args) && show_expr(io, expr.args[1])
        print(io, "(")
        args = expr.args[2:end]
        for (i, arg) in enumerate(args)
            show_expr(io, arg)
            i != lastindex(args) ? print(io, seperator) : print(io, ")")
        end
    elseif expr.head == :(=)
        show_expr(io,expr.args[1])
        print(io,": ")
        show_expr(io,expr.args[2])
    elseif expr.head == :function
        show_expr(io,expr.args[1])
        print(io," := block([], ")
        args = unparse(expr.args[2])
        for (i, arg) in enumerate(args)
            print(io,arg)
            i != endof(args) ? print(io, ", ") : print(io, ")")
        end
    elseif expr.head == :line
        nothing
    else
      error("Nested :$(expr.head) block structure not supported by Maxima.jl")
    end
end

function unparse(expr::Expr)
    str = String[]
    io = IOBuffer();
    if expr.head == :block
        for line ∈ expr.args
            show_expr(io,line)
            push!(str,takebuf_string(io))
        end
        return mtrim(str)
    else
        show_expr(io, expr)
        return push!(str,String(take!(copy(io))))
    end
end

"""

A Maxima expression

## Summary:

type MExpr <: Any

## Fields:

str :: String
"""
struct MExpr
    str::Vector{String}
    MExpr(m::Vector{String}) = new(m)
end

MExpr(m::Array{SubString{String},1}) = MExpr(convert(Array{String, 1}, m))
MExpr(str::String) = MExpr(push!(String[], str))
MExpr(m::Any) = MExpr("$m")

macro m_str(str)
    MExpr(str)
end

*(x::MExpr, y::String) = MExpr(push!(deepcopy(x.str), y))
*(x::String, y::MExpr) = MExpr(unshift!(deepcopy(y.str), x))
*(x::MExpr, y::MExpr) = MExpr(vcat(x.str..., y.str...))

function mtrim(m::Vector{String})
    n = String[]
    for h ∈ 1:length(m)
        !isempty(m[h]) && push!(n, m[h])
    end
    return n
end

function split(m::MExpr)
    n = String[]
    for i in 1:length(m.str)
        p = split(replace(m.str[i], r"\$" => ";"), ';')
        for j in 1:length(p)
            push!(n, p[i])
        end
    end
    return MExpr(n)
end

const m_to_jl = Dict(
    "%e"     => "ℯ",
    "%pi"    =>  "π",
    "%i"     =>  "im",
    "%gamma" => "eulergamma",
    "%phi"   =>  "φ",
    "inf"    =>  "Inf",
    "minf"   =>  "-Inf"
)

const jl_to_m = Dict(
    "ℯ"          => "%e",
    "pi"         => "%pi",
    "π"          => "%pi",
    "γ"          => "%gamma",
    "eulergamma" => "%gamma",
    "golden"     => "%phi",
    "φ"          => "%phi",
    "im"         => "%i",
    "Inf"        => "inf"
)

_subst(a, b, expr) = "subst($a, $b, '($expr))" |> MExpr |> mcall

"""
    MExpr(expr::Expr)

Convert Julia expression to Maxima expression

## Examples
```julia
julia> MExpr(:(sin(x*im) + cos(y*φ)))

                           cos(%phi y) + %i sinh(x)

```
"""
function MExpr(expr::Expr)
    # str = "$expr"
    str = unparse(expr)
    for h in 1:length(str)
        for key in keys(jl_to_m)
            str[h] = _subst(jl_to_m[key], key, str[h])
        end
    end
    return MExpr(str)
end

"""
    parse(mexpr::MExpr)

Parse a Maxima expression into a Julia expression

## Examples
```julia
julia> parse(m\"sin(%i*x)\")
:(im * sinh(x))

```
"""
function parse(m::MExpr)
    pexpr = Any[]
    for subexpr in split(m).str
        for key in keys(m_to_jl)
            subexpr = replace(subexpr, key => m_to_jl[key])
        end
        if occursin( ":=", subexpr)
            sp = split(subexpr, ":=")
            push!(pexpr, Expr(:function, parse(sp[1]), sp[2] |> String |> MExpr |> parse))
        elseif occursin( "block([], ", subexpr)
            rp = replace(subexpr, "block([],", "") |> chop
            sp = split(rp, ",")
            ep = Vector{Any}(length(sp))
            for j in 1:length(sp)
                ep[j] = sp[j] |> String |> MExpr |> parse
            end
            push!(pexpr,Expr(:block,ep...))
        elseif occursin( ":", subexpr)
            sp = split(subexpr, ":")
            push!(pexpr, Expr(:(=), parse(sp[1]), sp[2] |> String |> MExpr |> parse))
        else
            push!(pexpr, Meta.parse(subexpr))
        end
    end
    return length(pexpr) == 1 ? pexpr[1] : Expr(:block, pexpr...)
end

convert(::Type{MExpr}, m::MExpr) = m
convert(::Type{Array{String,1}}, m::MExpr) = m.str
convert(::Type{String}, m::MExpr) = join(m.str, "; ")
convert(::Type{T}, m::MExpr) where T = T <: Number ? eval(parse(m)) : parse(m)

"""
    mcall(m::MExpr)

Evaluate a Maxima expression.

## Examples
```julia
julia> m\"integrate(sin(x), x)\"

                             integrate(sin(x), x)

julia> mcall(ans)

                                   - cos(x)

```
"""
function mcall(m::MExpr)
    write(ms, replace(convert(String, m), r";" => "; print(ascii(3))\$ "))
    output = read(ms)
    if occursin(maxerr, output)
            write(ms.input, "errormsg()\$")
            write(ms.input, "print(ascii(4))\$")
            message = read(ms)
            throw(MaximaError(message))
    elseif occursin(synerr, output)
        throw(MaximaSyntaxError(output))
    else
        sp = split(output, '\x03')
        for k in 1:length(sp)
          sp[k] = replace(sp[k], '\n' => "")
          sp[k] = replace(sp[k], ' ' => "")
        end
        return MExpr(sp)
    end
end

"""
	mcall(expr::T) where T

Evaluate a Julia expression or string using the Maxima interpretor and convert
output back into the input type

## Examples
```julia
julia> mcall(\"integrate(sin(y)^2, y)\")
\"(y-sin(2*y)/2)/2\"

julia> mcall(:(integrate(1/(1+x^2), x)))
:(atan(x))

```
"""
function mcall(expr::T) where T
    mexpr = MExpr(expr)
    return convert(T, mcall(mexpr))
end

function ==(m::MExpr, n::MExpr)
    r = split(m).str
    s = split(n).str
    l = length(r)
    l != length(s) && (return false)
    b = true
    for j ∈ 1:l
        out = mcall("is($(r[j]) = $(s[j]))")
        b &= !occursin( "false", out)
    end
    return b
end

function getindex(m::MExpr, i)
    return MExpr("$m[$i]") |> mcall
end


module ReferenceFEsTests

using Test

@testset "Polytopes" begin include("PolytopesTests.jl") end

@testset "ExtrusionPolytopes" begin include("ExtrusionPolytopesTests.jl") end

@testset "Dofs" begin include("DofsTests.jl") end

@testset "MockDofs" begin include("MockDofsTests.jl") end

@testset "LagrangianDofBases" begin include("LagrangianDofBasesTests.jl") end

@testset "ReferenceFEInterfaces" begin include("ReferenceFEInterfacesTests.jl") end

@testset "LagrangianRefFEs" begin include("LagrangianRefFEsTests.jl") end

@testset "CLagrangianRefFEs" begin include("CLagrangianRefFEsTests.jl") end

@testset "SerendipityRefFEs" begin include("SerendipityRefFEsTests.jl") end

@testset "PDiscRefFEs" begin include("PDiscRefFEsTests.jl") end

@testset "Quadratures" begin include("QuadraturesTests.jl") end

@testset "TensorProductQuadratures" begin include("TensorProductQuadraturesTests.jl") end

@testset "DuffyQuadratures" begin include("DuffyQuadraturesTests.jl") end

@testset "StrangQuadratures" begin include("StrangQuadraturesTests.jl") end

@testset "RaviartThomasRefFEs" begin include("RaviartThomasRefFEsTests.jl") end

@testset "NedelecRefFEs" begin include("NedelecRefFEsTests.jl") end

@testset "CDLagrangianRefFEs" begin include("CDLagrangianRefFEsTests.jl") end

end # module


module TestMeasures

using MLJFair, Test

include(joinpath("..", "data", "data.jl"))

include("calcmetrics.jl")
include("boolmetrics.jl")

end  # module


# This file contans the common interface for LightGraphs.
# TODO 0.7: reevaluate use of errors here.

_NI(m) = error("Not implemented: $m")

"""
    AbstractEdge

An abstract type representing a single edge between two vertices of a graph.
"""
abstract type AbstractEdge{T} end

"""
    AbstractEdgeIter

An abstract type representing an edge iterator.
"""
abstract type AbstractEdgeIter end

"""
    AbstractGraph

An abstract type representing a graph.
"""
abstract type AbstractGraph{T} end

@traitdef IsDirected{G<:AbstractGraph}
@traitimpl IsDirected{G} <- is_directed(G)

#
# Interface for AbstractEdges
#

"""
    src(e)

Return the source vertex of edge `e`.

# Examples
```jldoctest
julia> using LightGraphs

julia> g = SimpleGraph(2);

julia> add_edge!(g, 1, 2);

julia> src(first(edges(g)))
1
```
"""
src(e::AbstractEdge) = _NI("src")

"""
    dst(e)

Return the destination vertex of edge `e`.

# Examples
```jldoctest
julia> using LightGraphs

julia> g = SimpleGraph(2);

julia> add_edge!(g, 1, 2);

julia> dst(first(edges(g)))
2
```
"""
dst(e::AbstractEdge) = _NI("dst")

Pair(e::AbstractEdge) = _NI("Pair")
Tuple(e::AbstractEdge) = _NI("Tuple")

"""
    reverse(e)

Create a new edge from `e` with source and destination vertices reversed.

# Examples
```jldoctest
julia> using LightGraphs

julia> g = SimpleDiGraph(2);

julia> add_edge!(g, 1, 2);

julia> reverse(first(edges(g)))
Edge 2 => 1
```
"""
reverse(e::AbstractEdge) = _NI("reverse")

==(e1::AbstractEdge, e2::AbstractEdge) = _NI("==")

#
# Interface for AbstractGraphs
#
"""
    edgetype(g)

Return the type of graph `g`'s edge
"""
edgetype(g::AbstractGraph) = _NI("edgetype")

"""
    eltype(g)

Return the type of the graph's vertices (must be <: Integer)
"""
eltype(g::AbstractGraph) = _NI("eltype")

"""
    nv(g)

Return the number of vertices in `g`.

# Examples
```jldoctest
julia> using LightGraphs

julia> nv(SimpleGraph(3))
3
```
"""
nv(g::AbstractGraph) = _NI("nv")

"""
    ne(g)

Return the number of edges in `g`.

# Examples
```jldoctest
julia> using LightGraphs

julia> g = PathGraph(3);

julia> ne(g)
2
```
"""
ne(g::AbstractGraph) = _NI("ne")

"""
    vertices(g)

Return (an iterator to or collection of) the vertices of a graph.

### Implementation Notes
A returned iterator is valid for one pass over the edges, and
is invalidated by changes to `g`.

# Examples
```jldoctest
julia> using LightGraphs

julia> collect(vertices(SimpleGraph(4)))
4-element Array{Int64,1}:
 1
 2
 3
 4
```
"""
vertices(g::AbstractGraph) = _NI("vertices")

"""
    edges(g)

Return (an iterator to or collection of) the edges of a graph.
For `AbstractSimpleGraph`s it returns a `SimpleEdgeIter`.
The expressions `e in edges(g)` and `e ∈ edges(ga)` evaluate as
calls to [`has_edge`](@ref).

### Implementation Notes
A returned iterator is valid for one pass over the edges, and
is invalidated by changes to `g`.

# Examples
```jldoctest
julia> using LightGraphs

julia> g = PathGraph(3);

julia> collect(edges(g))
2-element Array{LightGraphs.SimpleGraphs.SimpleEdge{Int64},1}:
 Edge 1 => 2
 Edge 2 => 3
```
"""
edges(g) = _NI("edges")

"""
    is_directed(g)

Return `true` if the graph is a directed graph; `false` otherwise.

# Examples
```jldoctest
julia> using LightGraphs

julia> is_directed(SimpleGraph(2))
false

julia> is_directed(SimpleDiGraph(2))
true
```
"""
is_directed(g) = _NI("is_directed")
is_directed(::Type{T}) where T = _NI("is_directed")

"""
    has_vertex(g, v)

Return true if `v` is a vertex of `g`.

# Examples
```jldoctest
julia> using LightGraphs

julia> has_vertex(SimpleGraph(2), 1)
true

julia> has_vertex(SimpleGraph(2), 3)
false
```
"""
has_vertex(x, v) = _NI("has_vertex")

"""
    has_edge(g, s, d)

Return true if the graph `g` has an edge from node `s` to node `d`.

An optional `has_edge(g, e)` can be implemented to check if an edge belongs
to a graph, including any data other than source and destination node.

`e ∈ edges(g)` or `e ∈ edges(g)` evaluate as
calls to `has_edge`, c.f. [`edges`](@ref).

# Examples
```jldoctest
julia> using LightGraphs

julia> g = SimpleDiGraph(2);

julia> add_edge!(g, 1, 2);

julia> has_edge(g, 1, 2)
true

julia> has_edge(g, 2, 1)
false
```
"""
has_edge(g, s, d) = _NI("has_edge")
has_edge(g, e) = has_edge(g, src(e), dst(e))

"""
    inneighbors(g, v)

Return a list of all neighbors connected to vertex `v` by an incoming edge.

### Implementation Notes
Returns a reference, not a copy. Do not modify result.

# Examples
```jldoctest
julia> g = SimpleDiGraph([0 1 0 0 0; 0 0 1 0 0; 1 0 0 1 0; 0 0 0 0 1; 0 0 0 1 0]);

julia> inneighbors(g, 4)
2-element Array{Int64,1}:
 3
 5
```
"""
inneighbors(x, v) = _NI("inneighbors")

"""
    outneighbors(g, v)

Return a list of all neighbors connected to vertex `v` by an outgoing edge.

# Implementation Notes
Returns a reference, not a copy. Do not modify result.

# Examples
```jldoctest
julia> g = SimpleDiGraph([0 1 0 0 0; 0 0 1 0 0; 1 0 0 1 0; 0 0 0 0 1; 0 0 0 1 0]);

julia> outneighbors(g, 4)
1-element Array{Int64,1}:
 5
```
"""
outneighbors(x, v) = _NI("outneighbors")

"""
    zero(g)

Return a zero-vertex, zero-edge version of the same type of graph as `g`.

# Examples
```jldoctest
julia> g = SimpleDiGraph([0 1 0 0 0; 0 0 1 0 0; 1 0 0 1 0; 0 0 0 0 1; 0 0 0 1 0]);

julia> zero(g)
{0, 0} directed simple Int64 graph
```
"""
zero(g::AbstractGraph) = _NI("zero")


#ДАНО: Робот - в произвольной клетке ограниченного прямоугольного поля

#РЕЗУЛЬТАТ: Робот - в исходном положении, и все клетки поля промакированы
function mark!(r)
    back!(r)
    putmarker!(r)
    while (!isborder(r,Sud) || !isborder(r,Nord)) && !isborder(r,Ost)
        for side in (Nord, Ost, Sud)
            if side == Ost
                putmarker!(r)
                move!(r,Ost)
            else
                while !isborder(r,side)
                    putmarker!(r)
                    move!(r,side)
                end
                if side == Sud && !isborder(r,Ost)
                    putmarker!(r)
                    move!(r,Ost)
                end
            end
        end
    end
    putmarker!(r)
end
function back!(r)
    for side in (West,Sud)
        while !isborder(r,side)
            move!(r,side)
        end
    end
end


module TestOptimization
# testing helpers for optimization of CovarianceFunctions hyper-parameters
using Test
using CovarianceFunctions
using CovarianceFunctions: EQ, RQ, Lengthscale
using LinearAlgebra
using Flux

# testing taking gradients of kernels and incrementing parameters with them
@testset "optimization" begin
    l = [1/2]
    k = Lengthscale(EQ(), l)
    p = Flux.params(k)
    x, y = randn(), randn()
    g = gradient(()->k(x, y), p) # should we use @forward?
    @test g[l][1] isa Real
end

end # TestOptimization


include("..\\src\\generate_decompositions.jl")

data_path = "..\\data\\OPF_instances"
instances = ["case1354pegase.m"]
algo = "cholesky" #OR "MD" or "cholesky_real"
repo_name = "test_kmax2"
FUSION = "YES"
fusion_type = "our_merging_heuristic"
kmax=2
size_max_IPmerging = 0
nb_times = 0

write_file_decompositions(data_path, instances, algo, repo_name, FUSION, fusion_type, size_max_IPmerging, nb_times, kmax)


using OmegaCore
using Test

@testset "named tuple" begin
  x = (a = 3, b = 4, c = 12)
  x_ = OmegaCore.Util.rmkey(x, Val{:a})
  @test :b ∈ keys(x_)
  @test :c ∈ keys(x_)
  @test :a ∉ keys(x_)
end


"""

Computing GMD contributions for the parts of the solute

"""

function gmdget(topology :: String, gmd :: Array{}, sel1 :: String, sel2 :: String; vmd="vmd" )

  # gmd is the array resulting from reading the gmd contributions per atom
  # for the solute, produced by the gmd.f90 program of mdanalysis

  # The first selection corresponds the the total solute, the second
  # selection to the section of the solute that will be considered here

  sel1 = M3GTools.select(topology,sel1,vmd=vmd)
  sel2 = M3GTools.select(topology,sel2,vmd=vmd)

  # To do so, define a vector that contains the index in "solute" of the atoms
  # of the "backbone" selection. We sum 2 because this will correspond directly
  # to the columns of the gmd_solute array.

  iatoms = similar(sel2);
  for i in 1:length(sel2)
    iatoms[i] = findfirst(isequal(sel2[i]),sel1) + 2
  end

  # Sum all contributions of the gmd_solute array for iatoms:
  gmd_get = zeros(length(gmd[:,1]));
  for i in 1:length(gmd[:,1])
    for j in 1:length(sel2)
      gmd_get[i] = gmd_get[i] + gmd[i,iatoms[j]]
    end
  end

  return gmd_get

end

gmdget(topology :: String, gmd :: Array{}; data = "none", get = "none", vmd="vmd" ) = gmdget(topology, gmd, data, get, vmd=vmd)

gmdget(simulation :: Simulation, gmd :: Array{}, data, get) = gmdget(simulation, gmd, data, get, vmd=simulation.vmd)
gmdget(simulation :: Simulation, gmd :: Array{}; data = "none", get = "none" ) = gmdget(simulation.psf, gmd, data, get, vmd=simulation.vmd)


mutable struct Intersection{T<:AbstractFloat,O<:Intersectable{T}}
    o::Union{O,Nothing}
    ray::Ray{T}
    t::T
    u::T
    v::T
end

Intersection(ray::Ray{T},::Type{O}) where {T,O} =
    Intersection{T,O}(nothing, ray, convert(T, Inf), zero(T), zero(T))

function normal(i::Intersection)
    p = i.ray.pos + i.t*i.ray.dir
    normal(i.o, p, i)
end

Base.isless(a::Intersection, b::Intersection) = (a.t < b.t)

is_hit(a::Intersection) = a.o != nothing

export Intersection, normal


# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/JuliaFEM.jl/blob/master/LICENSE.md

using JuliaFEM
using JuliaFEM.Testing

#= TODO: Fix test
function test_interpolate()
    el = get_element()
    @test isapprox(el("geometry", [0.0, 0.0]), [0.5, 0.5])
    @test isapprox(el("geometry", [0.0, 0.0], 0.0), [0.5, 0.5])
    @test isapprox(el([0.0, 0.0]), [0.25 0.25 0.25 0.25])
    @test isapprox(el([0.0, 0.0], Val{:grad}), [-0.5 0.5 0.5 -0.5; -0.5 -0.5 0.5 0.5])
    gradT = el("temperature", [0.0, 0.0], 1.0, Val{:grad})
    info("gradT = $gradT")
    X = [0.5, 0.5]
    gradT_expected = [1-2*X[2] 3-2*X[1]]
    info("gradT(expected) = $gradT_expected")
    @test isapprox(gradT, gradT_expected)

#   @test isapprox(el("temperature", [0.0, 0.0], 0.5), 1/2*gradT_expected)

#   gradT = el("temperature", [0.0, 0.0], 0.5, Val{:grad})
#   info("gradT = $gradT")
#   @test isapprox(gradT, 1/2*gradT_expected)
end
=#

#= TODO: Fix test
function test_calculate_normal_tangential_coordinates()
    el = Tri3([1, 2, 3])
    el["geometry"] = Vector{Float64}[
        [0.0, 0.0, 0.0],
        [1.0, 0.0, 0.0],
        [0.0, 1.0, 0.0]]
    calculate_normal_tangential_coordinates!(el, 0.0)
    n = [0.0 0.0 1.0]'
    t1 = [1.0 0.0 0.0]'
    t2 = [0.0 1.0 0.0]'
    R = [n t1 t2]
    @test isapprox(el("normal-tangential coordinates", [0.0, 0.0], 0.0), R)
end
=#

#= TODO: Fix test
function test_manifold_determinant()
    el = Quad4([1, 2, 3, 4])
    #el["geometry"] = Vector{Float64}[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]
    el["geometry"] = Vector{Float64}[[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 1.0], [0.0, 1.0, 1.0]]
    # mother element area = 2*2 = 4, this element is 1, determinant should be 1/4 everywhere
    d = det(el, [0.1, 0.2], 0.0)
    d_expected = 0.25
    @test d == d_expected
end
=#

#= TODO: Fix test
@testset "add new discrete constant time-variant field and interpolate it" begin
    element = Element(Quad4, [1, 2, 3, 4])
    element["my field"] = (0.0 => 0.0, 1.0 => 1.0)
    @test isapprox(element("my field", [0.0, 0.0], 0.5), 0.5)
    update!(element, "my field 2", 0.0 => 0.0, 1.0 => 1.0)
    @test isapprox(element("my field 2", [0.0, 0.0], 0.5), 0.5)
end
=#

@testset "add time dependent field to element" begin
    el = Element(Seg2, [1, 2])
    u1 = Vector{Float64}[[0.0, 0.0], [0.0, 0.0]]
    u2 = Vector{Float64}[[1.0, 1.0], [1.0, 1.0]]
    update!(el, "displacement", 0.0 => u1)
    update!(el, "displacement", 1.0 => u2)
    @test length(el["displacement"]) == 2
    @test isapprox(el("displacement", [0.0], 0.0), [0.0, 0.0])
    @test isapprox(el("displacement", [0.0], 0.5), [0.5, 0.5])
    @test isapprox(el("displacement", [0.0], 1.0), [1.0, 1.0])
    el2 = Element(Poi1, [1])
    update!(el2, "force 1", 0.0 => 1.0)
end

@testset "add CVTV field to element" begin
    el = Element(Seg2, [1, 2])
    f(xi, time) = xi[1]*time
    update!(el, "my field", f)
    v = el("my field", [1.0], 2.0)
    @test isapprox(v, 2.0)
end

@testset "add DCTI to element" begin
    el = Element(Quad4, [1, 2, 3, 4])
    update!(el, "displacement load", DCTI([4.0, 8.0]))
    @test isa(el["displacement load"], DCTI)
    @test !isa(el["displacement load"].data, DCTI)
    update!(el, "displacement load 2", [4.0, 8.0])
    @test isa(el["displacement load 2"], DCTI)
    update!(el, "temperature", [1.0, 2.0, 3.0, 4.0])
    @test isa(el["temperature"], DVTI)
    @test isapprox(el("displacement load", [0.0, 0.0], 0.0), [4.0, 8.0])
end

@testset "interpolate DCTI from element" begin
    el = Element(Seg2, [1, 2])
    update!(el, "foobar", 1.0)
    fb = el("foobar", [0.0], 0.0)
    @test isa(fb, Float64)
    @test isapprox(fb, 1.0)
end

#= unnecessary feature
@testset "add two time dependent fields to element at once" begin
    el = Element(Seg2, [1, 2])
    update!(el, "foo1", 1.0 => 1.0)
    update!(el, "foo1", 2.0 => 2.0)
    update!(el, "foo2", 1.0 => 1.0, 2.0 => 2.0)
    @test isapprox(el("foo1", 1.5), el("foo2", 1.5))
end
=#

@testset "add elements to elements" begin
    el1 = Element(Seg2, [1, 2])
    el2 = Element(Seg2, [3, 4])
    update!(el1, "master elements", [el2])
    lst = el1("master elements", 0.0)
    @test isa(lst, Vector)
end


module UniversalConstants
export getUniversalConstant,getUniversalConstantList

UniversalConstantDict = Dict{String,Real}("standardAccelerationOfGravity" => 9.80665,
"avogadroConstant" => 6.02214199e+23,
"boltzmannConstant" => 1.3806503e-23,
"molarGasConstant" => 8.314472,
"plankConstant" =>6.626070150e-34,
"speedOfLightInVacuum"=> 299792458,
"standardAccelerationOfGravity"=> 9.80665,
"idealGasStateReferencePressure"=> 101325)

getUniversalConstant(str::String) = UniversalConstantDict[str]
getUniversalConstantList()= collect(keys(UniversalConstantDict))
end

__precompile__()


"""
    Cartpole <: Environment

    classic system with one translational (cart) and one rotational (pole) degree of freedom
    https://underactuated.mit.edu/acrobot.html#cart_pole
"""
struct Cartpole end

function cartpole(;
    representation=:minimal,
    timestep=0.05,
    gravity=[0.0; 0.0; -9.81],
    seed=1,
    vis=Visualizer(),
    info=nothing,
    name=:robot,
    control_scaling=Diagonal(ones(1)),
    opts_step=SolverOptions(),
    opts_grad=SolverOptions(),
    T=Float64)

    mechanism = get_cartpole(
        timestep=timestep,
        gravity=gravity)
    initialize_cartpole!(mechanism)

    if representation == :minimal
        nx = minimal_dimension(mechanism)
    elseif representation == :maximal
        nx = maximal_dimension(mechanism)
    end
    nu = 1
    no = nx

    aspace = BoxSpace(nu,
        low=(-1.0e8 * ones(nu)),
        high=(1.0e8 * ones(nu)))
    ospace = BoxSpace(no,
        low=(-Inf * ones(no)),
        high=(Inf * ones(no)))

    rng = MersenneTwister(seed)

    z = get_maximal_state(mechanism)
    x = representation == :minimal ? maximal_to_minimal(mechanism, z) : z

    fx = zeros(nx, nx)
    fu = zeros(nx, nu)

    u_prev = zeros(nu)
    control_mask = [1.0 0.0]

    build_robot(mechanism, vis=vis, name=name)

    TYPES = [Cartpole, T, typeof(mechanism), typeof(aspace), typeof(ospace), typeof(info)]
    env = Environment{TYPES...}(mechanism, representation, aspace, ospace,
        x, fx, fu,
        u_prev, control_mask' * control_scaling,
        nx, nu, no,
        info,
        [rng], vis,
        opts_step, opts_grad)

    return env
end

function cartpole_nominal_max(;
    pendulum_length=1.0)

    # initial state
    x1b1 = [0.0; 0.0; 0.0]
    v1b1 = [0.0; 0.0; 0.0]
    q1b1 = [1.0; 0.0; 0.0; 0.0]
    ω1b1 = [0.0; 0.0; 0.0]
    z1b1 = [x1b1; v1b1; q1b1; ω1b1]

    x1b2 = [0.0; 0.0; -0.5 * pendulum_length]
    v1b2 = [0.0; 0.0; 0.0]
    q1b2 = [1.0; 0.0; 0.0; 0.0]
    ω1b2 = [0.0; 0.0; 0.0]
    z1b2 = [x1b2; v1b2; q1b2; ω1b2]

    z1 = [z1b1; z1b2]
end

function cartpole_goal_max(;
    pendulum_length=1.0)

    # target state
    xTb1 = [0.0; 0.0; 0.0]
    vTb1 = [0.0; 0.0; 0.0]
    qTb1 = [1.0; 0.0; 0.0; 0.0]
    ωTb1 = [0.0; 0.0; 0.0]
    zTb1 = [xTb1; vTb1; qTb1; ωTb1]

    xTb2 = [0.0; 0.0; 0.5 * pendulum_length]
    vTb2 = [0.0; 0.0; 0.0]
    qTb2 = [0.0; 1.0; 0.0; 0.0]
    ωTb2 = [0.0; 0.0; 0.0]
    zTb2 = [xTb2; vTb2; qTb2; ωTb2]

    zT = [zTb1; zTb2]
end

function visualize(env::Environment{Cartpole}, traj::Vector{Vector{T}};
    axes=false,
    grid=false,
    ee_traj=false) where T

	@assert size(traj[1]) == size(env.state)
    storage = generate_storage(env.mechanism, [env.representation == :minimal ? minimal_to_maximal(env.mechanism, x) : x for x in traj])
    visualize(env.mechanism, storage,
        vis=env.vis)
	set_camera!(env.vis, zoom=1.0, cam_pos=[2,0,0])
    setvisible!(env.vis["/Axes"], axes)
    setvisible!(env.vis["/Grid"], grid)
    # slider
    l2 = GeometryBasics.Cylinder(Point3f0(0.0, -5.0, 0.0),
		Point3f0(0.0, 5.0, 0.0),
		convert(Float32, 0.0125))
	setobject!(env.vis["slider"], l2, MeshPhongMaterial(color=Colors.RGBA(0.0, 0.0, 0.0, 1.0)))
end

function ghost(env::Environment{Cartpole}, z_sol;
    timesteps=[t for t = 1:length(z_sol)],
    line=false)

    for t in timesteps
        name = Symbol("robot_$t")
        build_robot(env.vis, env.mechanism,
            name=name,
            color=(t == length(z_sol) ? cyan : cyan_light))
        z = z_sol[t]
        if t == length(z_sol)
            z[1] -= 0.05
            z[14] -= 0.05
        end
        set_robot(env.vis, env.mechanism, z,
            name=name)
    end

	set_camera!(env.vis,
        zoom=1.0,
        cam_pos=[2,0,0])
    setvisible!(env.vis[:robot], false)
    l2 = GeometryBasics.Cylinder(Point3f0(0.0, -5.0, 0.0),
            Point3f0(0.0, 5.0, 0.0),
            convert(Float32, 0.0125))
    setobject!(env.vis["slider"], l2, MeshPhongMaterial(color=Colors.RGBA(0.0, 0.0, 0.0,1.0)))
    setvisible!(env.vis["/Axes"], false)
    setvisible!(env.vis["/Grid"], false)

    if line
        line_mat = LineBasicMaterial(
            color=color=RGBA(51.0 / 255.0, 1.0, 1.0, 1.0),
            linewidth=25.0)
        points = Vector{Point{3,Float64}}()
        for (i, xt) in enumerate(z_sol)
            z_pendulum = xt[13 .+ (1:13)]
            x = z_pendulum[1:3]
            q = Quaternion(z_pendulum[6 .+ (1:4)]...)
            k = x + vector_rotate([0.0; 0.0; -0.5], q)

            push!(points, Point(k[1], k[2], k[3]))

            setobject!(env.vis["ee_vertex_$i"], GeometryBasics.Sphere(Point3f0(0),
                convert(Float32, 0.001)),
                MeshPhongMaterial(color=RGBA(1.0, 153.0 / 255.0, 51.0 / 255.0, 1.0)))
                settransform!(env.vis["ee_vertex_$i"], MeshCat.Translation(points[i]))
        end
        setobject!(env.vis[:ee_traj], MeshCat.Line(points, line_mat))
    end

    open(env.vis)
end


module PhasedArrayTracking
    using DocStringExtensions, StaticArrays, Tracking
    import Unitful: Hz, ms
    import Tracking: prompt, dump!

    export prompt,
        init_tracking,
        TrackingInitials

    include("discriminators.jl")
    include("tracking_loop.jl")
end


function deal!{T<:Integer}(d::Deck, hlen::T)
    if hlen < length(d)
        hand = Deck(d.cards[1:hlen], d.design)
        d.cards = d.cards[hlen+1:end]
    else
        hand = d
        empty!(d)
    end
    return hand
end

function deal!(d::Deck)
    deal!(d, d.design.hlen)
end

function pretty(d::Deck)
    s = ""
    llen = d.design.rlen
    dlen = length(d)
    for i in 1:llen:dlen
        j = min(i+llen-1, dlen)
        s *= print(d[i:j])*"\n"
    end
    chop(s)
end


using Oceananigans.Operators: index_and_interp_dependencies
using Oceananigans.Utils: tupleit, user_function_arguments

import Oceananigans: location

"""
    ContinuousBoundaryFunction{X, Y, Z, I, F, P, D, N, ℑ} <: Function

A wrapper for the user-defined boundary condition function `func` at location
`X, Y, Z`. `I` denotes the boundary-normal index (`I=1` at western boundaries,
`I=grid.Nx` at eastern boundaries, etc). `F, P, D, N, ℑ` are, respectively, the
user-defined function, parameters, field dependencies, indices of the field dependencies
in `model_fields`, and interpolation operators for interpolating `model_fields` to the
location at which the boundary condition is applied.
"""
struct ContinuousBoundaryFunction{X, Y, Z, I, F, P, D, N, ℑ} <: Function
                          func :: F
                    parameters :: P
            field_dependencies :: D
    field_dependencies_indices :: N
     field_dependencies_interp :: ℑ

    """ Returns a location-less wrapper for `func`, `parameters`, and `field_dependencies`."""
    function ContinuousBoundaryFunction(func, parameters, field_dependencies)

        field_dependencies = tupleit(field_dependencies)

        return new{Nothing, Nothing, Nothing, Nothing,
                   typeof(func), typeof(parameters),
                   typeof(field_dependencies), Nothing, Nothing}(func, parameters, field_dependencies, nothing, nothing)
    end

    function ContinuousBoundaryFunction{X, Y, Z, I}(func, parameters, field_dependencies,
                                                    field_dependencies_indices, field_dependencies_interp) where {X, Y, Z, I}
        return new{X, Y, Z, I,
                   typeof(func),
                   typeof(parameters),
                   typeof(field_dependencies),
                   typeof(field_dependencies_indices),
                   typeof(field_dependencies_interp)}(func,
                                                      parameters,
                                                      field_dependencies,
                                                      field_dependencies_indices,
                                                      field_dependencies_interp)
    end
end

location(bc::ContinuousBoundaryFunction{X, Y, Z}) where {X, Y, Z} = X, Y, Z

#####
##### "Regularization" for IncompressibleModel setup
#####

regularize_boundary_condition(bc, X, Y, Z, I, model_field_names) = bc # fallback

"""
    regularize_boundary_condition(bc::BoundaryCondition{C, <:ContinuousBoundaryFunction},
                                  X, Y, Z, I, model_field_names) where C

Regularizes `bc.condition` for location `X, Y, Z`, boundary index `I`, and `model_field_names`,
returning `BoundaryCondition(C, regularized_condition)`.

The regularization of `bc.condition::ContinuousBoundaryFunction` requries

1. Setting the boundary location to `X, Y, Z`.
   The boundary-normal direction is tagged with `Nothing` location.

2. Setting the boundary-normal index `I` for indexing into `field_dependencies`.
   `I` is either `1` (for left boundaries) or
   `size(grid, n)` for a boundary in the `n`th direction where `n ∈ (1, 2, 3)` corresponds
   to `x, y, z`.

3. Determining the `indices` that map `model_fields` to `field_dependencies`.

4. Determining the `interps` functions that interpolate field_dependencies to the location
   of the boundary.
"""
function regularize_boundary_condition(bc::BoundaryCondition{C, <:ContinuousBoundaryFunction},
                                       LX, LY, LZ, I, model_field_names) where C
    boundary_func = bc.condition

    indices, interps = index_and_interp_dependencies(LX, LY, LZ,
                                                     boundary_func.field_dependencies,
                                                     model_field_names)

    regularized_boundary_func = ContinuousBoundaryFunction{LX, LY, LZ, I}(boundary_func.func,
                                                                       boundary_func.parameters,
                                                                       boundary_func.field_dependencies,
                                                                       indices, interps)

    return BoundaryCondition(C, regularized_boundary_func)
end

#####
##### Kernel functions
#####

@inline function (bc::ContinuousBoundaryFunction{Nothing, LY, LZ, i})(j, k, grid, clock, model_fields) where {LY, LZ, i}
    args = user_function_arguments(i, j, k, grid, model_fields, bc.parameters, bc)
    return bc.func(ynode(LY(), j, grid), znode(LZ(), k, grid), clock.time, args...)
end

@inline function (bc::ContinuousBoundaryFunction{LX, Nothing, LZ, j})(i, k, grid, clock, model_fields) where {LX, LZ, j}
    args = user_function_arguments(i, j, k, grid, model_fields, bc.parameters, bc)
    return bc.func(xnode(LX(), i, grid), znode(LZ(), k, grid), clock.time, args...)
end

@inline function (bc::ContinuousBoundaryFunction{LX, LY, Nothing, k})(i, j, grid, clock, model_fields) where {LX, LY, k}
    args = user_function_arguments(i, j, k, grid, model_fields, bc.parameters, bc)
    return bc.func(xnode(LX(), i, grid), ynode(LY(), j, grid), clock.time, args...)
end

# Don't re-convert ContinuousBoundaryFunctions passed to BoundaryCondition constructor
BoundaryCondition(TBC, condition::ContinuousBoundaryFunction) =
    BoundaryCondition{TBC, typeof(condition)}(condition)

Adapt.adapt_structure(to, bc::BoundaryCondition{C, <:ContinuousBoundaryFunction}) where C =
    BoundaryCondition(C, Adapt.adapt(to, bc.condition))

Adapt.adapt_structure(to, bf::ContinuousBoundaryFunction{LX, LY, LZ, I}) where {LX, LY, LZ, I} =
    ContinuousBoundaryFunction{LX, LY, LZ, I}(Adapt.adapt(to, bf.func),
                                           Adapt.adapt(to, bf.parameters),
                                           nothing,
                                           Adapt.adapt(to, bf.field_dependencies_indices),
                                           Adapt.adapt(to, bf.field_dependencies_interp))


function mul!(C::T, A::T, B::T, idx_A::String, idx_B::String, idx_C::String; nt=C_NULL) where T <: TTensor{T2} where T2 <: AbstractFloat
    tblis_tmul = dlsym(tblis,:tblis_tensor_mult)
    ccall(tblis_tmul,Cvoid,(Ptr{Nothing},Ptr{Nothing},
                            Ref{TTensor{T2}},Cstring,
                            Ref{TTensor{T2}},Cstring,
                            Ref{TTensor{T2}},Cstring),

          C_NULL,C_NULL,
          A,idx_A,
          B,idx_B,
          C,idx_C)
end

function add!(A::T, B::T, idx_A::String, idx_B::String) where T <: TTensor{T2} where T2 <: AbstractFloat
    tblis_tadd = dlsym(tblis,:tblis_tensor_add)
    ccall(tblis_tadd,Cvoid,(Ptr{Nothing},Ptr{Nothing},
                            Ref{TTensor{T2}},Cstring,
                            Ref{TTensor{T2}},Cstring),
          C_NULL,C_NULL,
          A,idx_A,
          B,idx_B)
end


"""
"""
function get_next_maintenance_due(sne::SimNE,model::ABM)
    time_maintenance = 0
    state = get_state(sne)
    #corrective
    # Do not care about maintenance due date, just wait for sne to break
    if !state.up
        time_maintenance = model.ticks
    end

    #basic preventive

    #?? calculate next maintenance from sne.eul and started rul

    return time_maintenance
end

# function trigger_maintenance(sne::SimNE)
#     state = get_state(sne)
#     # if state.maintenance_due == model.ticks

#     # end
# end

# """
# It triggers maintenance when the asset is brokendown

# """
# function do_corrective_man(sne::SimNE)
#     state = get_state(sne)
#     if !state.up
#         trigger_maintenance(sne)
#     end
# end

# """
# It triggers maintenance on due date assuming is before the asset breakdown
# """
# function do_preventive_man(sne::SimNE,model::ABM)
#     state = get_state(sne)
#     if state.maintenance_due == model.ticks
#         trigger_maintenance(sne)
#     end
# end

function init_maintenance!(sne::SimNE,model::ABM)
    state = get_state(sne)
    #init with according to factory estimated parameters
    state.rul_e = state.rul
    state.maintenance_due = state.rul_e - sne.maintenance.threshold
    set_state!(sne,state)
end

function start_mnt!(a::Agent,sne::SimNE,model::ABM)
    log_info(model.ticks,a.id,"Starting mnt... $(sne.id)")
    sne.maintenance.job_start = model.ticks
    state = get_state(sne)
    state.up = false
    #state.rul = 0
    state.on_maintenance = true
    set_state!(sne,state)
    drop_node!(sne,model)
    finish_mnt = model.ticks + a.maintenance.duration
    # log_info(model.ticks,a.id,"schedulling up of $(sne.id) for... $(finish_mnt)")
    if sne.id != 15
        schedule_event!(a,CTL_Event(3),finish_mnt,[sne.id])
    end
end

function stop_mnt!(a::Agent,sne::SimNE,model::ABM)
    log_info(model.ticks,a.id,"Stopping mnt...$(sne.id)")
    state = get_state(sne)
    state.up = true
    state.on_maintenance = false
    state.rul = sne.maintenance.eul
    state.maintenance_due = sne.maintenance.job_start + sne.maintenance.duration + sne.maintenance.eul - sne.maintenance.threshold
    sne.maintenance.job_start = -1
    set_state!(sne,state)
    rejoin_node!(model,sne.id)
    #Only if the agent's maintenance policy is corrective
    if a.maintenance.policy == CorrectiveM
        schedule_event!(a,CTL_Event(1), model.ticks + 2, [sne.id])
    end
end

function start_mnt!(sne::SimNE,time_start::Int64,mnt_policy::Type{CorrectiveM})
    log_info(time_start,sne.id,"Starting corrective maintenance...")

    start_mnt!(sne,time_start)

end

function start_mnt!(sne::SimNE,time_start::Int64,mnt_policy::Type{PreventiveM})
    log_info(time_start,sne.id,"Starting preventive maintenance..")
end

function stop_mnt!(sne::SimNE,mnt_policy::Type{CorrectiveM},model::ABM)
    log_info(model.ticks,sne.id,"Stopping corrective maintenance for $(nv(model.ntw_graph))...")
    rejoin_node!(model,sne.id)
    stop_mnt!(sne)
    log_info(model.ticks,sne.id,"Stopped maintenance for $(nv(model.ntw_graph))...")
end

function stop_mnt!(sne::SimNE,mnt_policy::Type{PreventiveM},model::ABM)
    log_info(model.ticks,sne.id,"Stopping preventive maintenance..")
end

function get_rul_predictions(sne::SimNE,current_time::Int64,window_size::Int64)::Vector{Float64}
    rul = get_state(sne).rul
    return [ lineal_d(sne.maintenance.deterioration_parameter,rul,t) for t = 1 : window_size  ]
end

# """
# Do corrective maintenance activities for the assets under control of a given agent.
# """
# function do_maintenance_step!(a::Agent,mnt_policy::Type{CorrectiveM},model::ABM)

#     sne_ids = get_controlled_assets(a.id,model)

#     for sne_id in sne_ids
#         sne = getindex(model,sne_id)
#         if is_start_mnt(sne,mnt_policy,model)
#             start_mnt!(sne,model.ticks,mnt_policy)
#         end
#         if sne.maintenance.job_start > 0 && sne.maintenance.job_start + sne.maintenance.duration == model.ticks
#             stop_mnt!(sne,mnt_policy,model)
#         end
#     end

# end

"""
Schedules an event of type ``type`` on the given agent ``a`` at time ``time``, affecting the snes with ids: ``snes``.
"""
function schedule_event!(a::Agent,type::CTL_Event,time::Int64,snes::Vector{Int64})
    if !haskey(a.events,time)
        a.events[time] = Array{ControlEvent,1}()
    end
    # log_info(time-1,a.id,"scheduled event $type at t: $time for snes: $(snes)")
    push!(a.events[time],ControlEvent(time,type,snes))
end

"""
Schedules an event of type ``type`` on the given agent ``a`` at time ``time``.
"""
function schedule_event!(a::Agent,type::CTL_Event,time::Int64)
    schedule_event!(a,type,time,Array{Int64,1}())
end

# function schedule_maintenance!(a::Agent,mnt_due::Int64,sne_id::Int64)
#     #sne.id => i, time => mnt_due
#             #schedule start and stop of maintenance job
#             if !haskey(a.maintenance.pending_jobs_start,mnt_due)
#                 a.maintenance.pending_jobs_start[mnt_due] = []
#             end
#             if !haskey(a.maintenance.pending_jobs_stop,mnt_due+a.maintenance.duration+1)
#                 a.maintenance.pending_jobs_stop[mnt_due+a.maintenance.duration+1] = []
#             end
#             push!(a.maintenance.pending_jobs_start[mnt_due],sne_id)
#             push!(a.maintenance.pending_jobs_stop[mnt_due+a.maintenance.duration+1],sne_id)
# end

function update_maintenance_plan!(a::Agent,mnt_policy::Type{PreventiveM},model::ABM)
    window_size = a.maintenance.prediction_window
    #use the maintenance window from the end - window size
    ruls = a.rul_predictions[:,size(a.rul_predictions,2)-window_size+1:size(a.rul_predictions,2)]
    #mnt_plan =
    for i=1:size(ruls,1)
        if !get_state(getindex(model,i)).on_maintenance
            #sum all where rul is <= threshold, if any > 0, then sum > 0
            threshold_reached = sum(ruls[i,size(ruls,2)-window_size+1:size(ruls,2)] .<= a.maintenance.threshold) > 0
            if threshold_reached#!isempty(threshold_reached)
                #negative indicates i goes down for maintenance
                #1st reroute traffic out of node
                schedule_event!(a,CTL_Event(1),model.ticks + minimum(threshold_reached),[-1*i])
                #2nd perform maintenance
                schedule_event!(a,CTL_Event(2),model.ticks + minimum(threshold_reached)+2,[-1*i])
            end
        end
    end
end

"""
  It processes a route received from optimisation algorithm
"""
function process_route!(a::Agent,rw,model::ABM)
    log_info(model.ticks,a.id,"full rw: $(rw)")
    path = Array{Int64,1}()
    services = model.ntw_services
    hd =  first(services[Int(rw[2])])
    route = rw[3:end]
    i_nxt = hd

    while true
        push!(path,i_nxt)
        i_nxt = Int(route[i_nxt])
        if i_nxt == 0
           # && i_nxt != last(model.ntw_services[rw[2]])
           break
        end
    end

    snes = [ getindex(model,Int(sid)) for sid in path ]
    time = maximum([ !is_up(sne) ? sne.maintenance.job_start + sne.maintenance.duration : model.ticks + Int(rw[1]) for sne in snes ])

    # log_info(model.ticks,a.id," $(rw[1:3]) event scheduled for t=$(time) ==> path: $path ")
    schedule_event!(a,CTL_Event(5),time,path)
end

function update_maintenance_plan!(a::Agent,mnt_policy::Type{PredictiveM},model::ABM)
    window_size = a.maintenance.prediction_window * 2
    ruls = a.rul_predictions[:,size(a.rul_predictions,2)-window_size+1:size(a.rul_predictions,2)]

    #optimal plan is precomputed offline for large networks
    if nv(model.ntw_graph) > 90
        if model.ticks == 10 # It only does this one time as the plans and routes are precomputed
            mnt_plan, routes = ln_mnt_plan, ln_routes
        else
            mnt_plan, routes = [],[]
        end

    else
        #data conversion to py, minus 1 as indexes in py start in 0
        services_py = model.py_integration.np.matrix(model.ntw_services) .- 1
        ruls_py = model.py_integration.np.matrix(ruls)
        #pycall to optimisation function
        log_info(model.ticks,a.id,"srvs=>$(services_py)")
        log_info(model.ticks,a.id,"ruls_py=>$(ruls_py)")
        mnt_plan, routes = model.py_integration.opt_run.maintenance_planning(model.ticks,services_py, ruls_py)
    end

    if !isempty(routes) && !isempty(mnt_plan)
        #when necessary, convert routes output to matrix, where size(ruls,1) + 2 is length of each routes matrix row.
        if size(routes,2) != size(ruls,1) + 2
            routes = transpose(reshape(routes,size(ruls,1) + 2,:))
        end

        log_info(model.ticks,a.id," From Alena's algo PLAN: $(mnt_plan)")
        log_info(model.ticks,a.id," From Alena's algo=> ROUTES: $(routes)")

        #convert routes from py to julia indexes
        routes = routes .+ 1

        if size(routes,2) > 1
            for rw in eachrow(routes)
                process_route!(a,rw,model)
            end
        elseif size(routes,2) == 1
            process_route!(a,routes,model)
        end

        for sne_id in 1:length(mnt_plan)
            if mnt_plan[sne_id] > 0
                # negative indicates that sne_id goes down for maintenance
                log_info(model.ticks,a.id," event for: $(mnt_plan[sne_id])")
                schedule_event!(a,CTL_Event(2),Int(model.ticks+mnt_plan[sne_id]),[sne_id])
            end
        end
    end
end

"""
It processes scheduled events
"""
function do_events_step!(a::Agent,model::ABM)
    # log_info(model.ticks,a.id,17,"processing events: $(a.events) ")
    if haskey(a.events,model.ticks)
        evs = a.events[model.ticks]
        ntw_changes = Array{Int64,1}()

        for e in evs
            # log_info(model.ticks,a.id,"Triggering event: $e")
            @match e.type begin
                CTL_Event(1) =>
                            for nid in e.snes
                                sne = getindex(model,abs(nid))
                                push!(ntw_changes,nid)
                            end
                CTL_Event(2) =>
                            for nid in e.snes
                                sne = getindex(model,abs(nid))
                                start_mnt!(a,sne,model)
                            end
                CTL_Event(3) =>
                            for nid in e.snes
                                sne = getindex(model,nid)
                                stop_mnt!(a,sne,model)
                            end
                CTL_Event(4) =>
                            do_rul_predictions!(a,model)

                CTL_Event(5) =>
                            do_update_flows_from_path!(a,e.snes,model)

                _ => log_info(model.ticks,a.id,"Control event not recognised: $e")
            end

        end

        if !isempty(ntw_changes)
            do_update_flows_from_changes!(a,ntw_changes,model)
        end

        delete!(a.events,model.ticks)
    end
end

"""
Run RUL predictions for the assets controlled by agent ``a``
"""
function do_rul_predictions!(a::Agent,model::ABM)
    window_size = a.maintenance.prediction_window * 2

    #sort snes by id, works either for centralised (all assets one control agent or decentralised 1 asset per agent) #TODO decentralised with more than 1 asset per agent.
    # sne_ids = sort(collect(get_controlled_assets(a.id,model)))
    sne_ids = collect(1:nv(a.base_ntw_graph))

    snes = getindex.([model],sne_ids)

    #log_info(model.ticks,a.id,"sne_ids: $(sne_ids)")

    #arrange predictions in a matrix of dims: length(snes) x window_size.
    ruls_pred = permutedims(hcat(get_rul_predictions.(snes,[model.ticks],[window_size])...))
    a.rul_predictions = length(a.rul_predictions) > 0 ? hcat(a.rul_predictions,ruls_pred) : ruls_pred
    # log_info(model.ticks,a.id," length: $(size(a.rul_predictions)) rul pred: $(a.rul_predictions)")
    # log_info(model.ticks,a.id,"Pred Maint=> services: $(model.ntw_services))")

    update_maintenance_plan!(a,a.maintenance.policy,model)
    #collect(get_controlled_assets(a.id,model))
    #run next prediction
    schedule_event!(a,CTL_Event(4),model.ticks+a.maintenance.predictive_freq)
end

"""
Do preventive maintenance activities for the assets controlled by the given agent.
"""
# function do_maintenance_step!(a::Agent,mnt_policy::Type{T},model::ABM) where T<:MaintenanceType

#     #prediction
#     if model.ticks%a.maintenance.predictive_freq == 0
#         window_size = a.maintenance.prediction_window
#         #sort snes by id, works either for centralised (all assets one control agent or decentralised 1 asset per agent) #TODO decentralised with more than 1 asset per agent.
#         # sne_ids = sort(collect(get_controlled_assets(a.id,model)))
#         sne_ids = collect(1:nv(a.params[:base_ntw_graph]))

#         snes = getindex.([model],sne_ids)

#         log_info(model.ticks,a.id,"sne_ids: $(sne_ids)")

#         #arrange predictions in a matrix of dims: length(snes) x window_size.
#         ruls_pred = permutedims(hcat(get_rul_predictions.(snes,[model.ticks],[window_size])...))
#         a.rul_predictions = length(a.rul_predictions) > 0 ? hcat(a.rul_predictions,ruls_pred) : ruls_pred
#         # log_info(model.ticks,a.id," length: $(size(a.rul_predictions)) rul pred: $(a.rul_predictions)")
#         log_info(model.ticks,a.id,"Pred Maint=> services: $(model.ntw_services))")

#         update_maintenance_plan!(a,mnt_policy,model)
#     end

#     #check a.pending_jobs if any job for the current tick, if so start
#     ntw_changes::Vector{Int64} = []
#     if haskey(a.maintenance.pending_jobs_start,model.ticks)
#         for nid in a.maintenance.pending_jobs_start[model.ticks]
#             push!(ntw_changes,-1 * nid )
#             sne = getindex(model,nid)
#             start_mnt!(sne,model.ticks)
#         end
#         delete!(a.maintenance.pending_jobs_start,model.ticks)
#     end

#     if haskey(a.maintenance.pending_jobs_stop,model.ticks)
#         for nid in a.maintenance.pending_jobs_stop[model.ticks]
#             push!(ntw_changes,nid)
#             sne = getindex(model,nid)
#             stop_mnt!(sne)
#         end
#         delete!(a.maintenance.pending_jobs_stop,model.ticks)
#     end

#     if !isempty(ntw_changes)
#         do_update_flows!(a,ntw_changes,model)
#     end

#     ##create ag message for predicted nes down

#     #check duration against job start
#     #for stop
#     ## create message for predicted nes_down? (should be renamed)
#     ## retrigger query and install of paths with maintained assets

# end

function is_start_mnt(sne::SimNE,mnt_policy::Type{CorrectiveM},model::ABM)
    return !get_state(sne).up && !get_state(sne).on_maintenance
end

function is_start_mnt(sne::SimNE,mnt_policy::Type{PredictiveM},model::ABM)
    return get_state(sne).maintenance_due - sne.maintenance.threshold == model.ticks
end

function is_start_mnt(sne::SimNE,mnt_policy::Type{PreventiveM},model::ABM)
    return get_state(sne).maintenance_due == model.ticks
end

function MaintenanceInfoCorrective(deterioration::Float64,model)
    # TODO adjust to multiple eul. For time being, always 100.
    return MaintenanceInfo(CorrectiveM,100,-1,model.mnt_wc_duration,model.mnt_wc_cost,0,0,0,deterioration,model.mnt_wc_duration,model.mnt_wc_cost)
end
function MaintenanceInfoCorrective(model)
    MaintenanceInfoCorrective(1.,model)
end
function MaintenanceInfoPreventive(deteriation::Float64,model)
    return MaintenanceInfo(PreventiveM,100,-1,model.mnt_bc_duration,model.mnt_bc_cost,10,10,10,deteriation,model.mnt_wc_duration,model.mnt_wc_cost)
end
function MaintenanceInfoPreventive(model)
    MaintenanceInfoPreventive(1.,model)
end
function MaintenanceInfoPredictive(deterioration::Float64,model)
    return MaintenanceInfo(PredictiveM,100,-1,model.mnt_bc_duration,model.mnt_bc_cost,20,10,10,deterioration,model.mnt_wc_duration,model.mnt_wc_cost)
end
function MaintenanceInfoPredictive(model)
    MaintenanceInfoPredictive(1.,model)
end

### TODO REVIEW ##
#maintenance cost
cost(av) = (1 - av) * 100

"""
Maintenance cost at a given time step:

rul_mnt: rul of an asset (sne) of the network that is undergoing maintenance. If rul == 0., then it is corrective maintenance, if rul > 0, then it is preventive maintenance. Otherwise (-1) no maintenance ongoing.

is_start: indicates if maintenance has just started so time-independent costs are added

is_active: indicates if the asset was active in the network before maintenance started, i.e. was part of an active flow.

dt_cost: downtime cost, cost of service flow/production loss per time step, time-dependent. Added when corrective maintenance.

l_cost: labour cost per time step, time-dependent.

p_cost: parts costs for maintenance regardless of the time it takes, time-independent, only added when maintenance starts.

r_cost: cost of loss of remaining life. Added when preventive maintenance.
"""
function maintenance_cost(rul_mnt,is_start,is_active,dt_cost,l_cost,p_cost,r_cost)

    #parts costs
    mnt_cost = is_start ? p_cost : 0.

    #labour costs
    mnt_cost += rul_mnt >= 0. ? l_cost : 0.

    #downtime costs
    mnt_cost += rul_mnt == 0. && is_active ? dt_cost : 0.

    #loss of life costs
    mnt_cost += is_start ? rul_mnt * r_cost : 0.

end


# adapted from http://pcjericks.github.io/py-gdalogr-cookbook/geometry.html
using Test
import GeoInterface
import GDAL, ArchGDAL; const AG = ArchGDAL

@testset "Create a Point" begin

    x, y = 1198054.34, 648493.09
    wktpoint = "POINT (1198054.34 648493.09)"

    # Method 1
    AG.createpoint(x, y) do point
        @test AG.toWKT(point) == wktpoint
    end
    AG.createpoint((x, y)) do point
        @test AG.toWKT(point) == wktpoint
    end

    # Method 2
    AG.createpoint() do point
        AG.addpoint!(point, x, y)
        @test AG.toWKT(point) == wktpoint
    end

    # Method 3
    AG.creategeom(GDAL.wkbPoint) do point
        AG.addpoint!(point, x, y)
        @test AG.toWKT(point) == wktpoint
    end

    # Method 4
    point = AG.creategeom(GDAL.wkbPoint)
    AG.addpoint!(point, x, y)
    @test AG.toWKT(point) == wktpoint

    # Method 5
    @test AG.toWKT(AG.createpoint(x, y)) == wktpoint
end

@testset "Create a LineString" begin
    # Method 1
    wktline = "LINESTRING (1116651.43937912 637392.696988746," *
    "1188804.01084985 652655.740953707,1226730.36252036 634155.081602239,1281307.30760719 636467.664021172)"
    AG.createlinestring([(1116651.439379124,  637392.6969887456),
                         (1188804.0108498496, 652655.7409537067),
                         (1226730.3625203592, 634155.0816022386),
                         (1281307.30760719,   636467.6640211721)]) do line
        @test AG.toWKT(line) == wktline
    end

    # Method 2
    AG.createlinestring() do line
        AG.addpoint!(line, 1116651.439379124,  637392.6969887456)
        AG.addpoint!(line, 1188804.0108498496, 652655.7409537067)
        AG.addpoint!(line, 1226730.3625203592, 634155.0816022386)
        AG.addpoint!(line, 1281307.30760719,   636467.6640211721)
        @test AG.toWKT(line) == wktline
    end

    # Method 3
    AG.creategeom(GDAL.wkbLineString) do line
        AG.addpoint!(line, 1116651.439379124,  637392.6969887456)
        AG.addpoint!(line, 1188804.0108498496, 652655.7409537067)
        AG.addpoint!(line, 1226730.3625203592, 634155.0816022386)
        AG.addpoint!(line, 1281307.30760719,   636467.6640211721)
        @test AG.toWKT(line) == wktline
    end

    # Method 4
    line = AG.creategeom(GDAL.wkbLineString)
    AG.addpoint!(line, 1116651.439379124,  637392.6969887456)
    AG.addpoint!(line, 1188804.0108498496, 652655.7409537067)
    AG.addpoint!(line, 1226730.3625203592, 634155.0816022386)
    AG.addpoint!(line, 1281307.30760719,   636467.6640211721)
    @test AG.toWKT(line) == wktline

    # Method 5
    @test AG.toWKT(AG.createlinestring([
        (1116651.439379124,  637392.6969887456),
        (1188804.0108498496, 652655.7409537067),
        (1226730.3625203592, 634155.0816022386),
        (1281307.30760719,   636467.6640211721)
    ])) == wktline
end

@testset "Create a Polygon" begin

    wktpoly = "POLYGON ((1179091.16469033 712782.883845978,1161053.02182265 667456.268434881," *
    "1214704.9339419 641092.828859039,1228580.42845551 682719.312399842," *
    "1218405.0658122 721108.180554139,1179091.16469033 712782.883845978))"

    # Method 1
    AG.createpolygon([(1179091.1646903288, 712782.8838459781),
                      (1161053.0218226474, 667456.2684348812),
                      (1214704.933941905,  641092.8288590391),
                      (1228580.428455506,  682719.3123998424),
                      (1218405.0658121984, 721108.1805541387),
                      (1179091.1646903288, 712782.8838459781)]) do poly
        @test AG.toWKT(poly) == wktpoly
    end

    # Method 2
    AG.createpolygon() do poly
        ring = AG.createlinearring(
                    [(1179091.1646903288, 712782.8838459781),
                     (1161053.0218226474, 667456.2684348812),
                     (1214704.933941905,  641092.8288590391),
                     (1228580.428455506,  682719.3123998424),
                     (1218405.0658121984, 721108.1805541387),
                     (1179091.1646903288, 712782.8838459781)])
        AG.addgeom!(poly, ring)
    end

    AG.createlinearring(
                    [(1179091.1646903288, 712782.8838459781),
                     (1161053.0218226474, 667456.2684348812),
                     (1214704.933941905,  641092.8288590391),
                     (1228580.428455506,  682719.3123998424),
                     (1218405.0658121984, 721108.1805541387),
                     (1179091.1646903288, 712782.8838459781)]) do ring
        AG.createpolygon() do poly
            AG.addgeom!(poly, ring)
            @test AG.toWKT(poly) == wktpoly
        end
    end

    # Method 3
    AG.creategeom(GDAL.wkbLinearRing) do ring
        AG.addpoint!(ring, 1179091.1646903288, 712782.8838459781)
        AG.addpoint!(ring, 1161053.0218226474, 667456.2684348812)
        AG.addpoint!(ring, 1214704.933941905, 641092.8288590391)
        AG.addpoint!(ring, 1228580.428455506, 682719.3123998424)
        AG.addpoint!(ring, 1218405.0658121984, 721108.1805541387)
        AG.addpoint!(ring, 1179091.1646903288, 712782.8838459781)

        AG.creategeom(GDAL.wkbPolygon) do poly
            AG.addgeom!(poly, ring)
            @test AG.toWKT(poly) == wktpoly
        end
    end

    # Method 4
    ring = AG.creategeom(GDAL.wkbLinearRing)
        AG.addpoint!(ring, 1179091.1646903288, 712782.8838459781)
        AG.addpoint!(ring, 1161053.0218226474, 667456.2684348812)
        AG.addpoint!(ring, 1214704.933941905, 641092.8288590391)
        AG.addpoint!(ring, 1228580.428455506, 682719.3123998424)
        AG.addpoint!(ring, 1218405.0658121984, 721108.1805541387)
        AG.addpoint!(ring, 1179091.1646903288, 712782.8838459781)

    poly = AG.creategeom(GDAL.wkbPolygon)
    AG.addgeom!(poly, ring)
    @test AG.toWKT(poly) == wktpoly

    @test AG.toWKT(AG.createpolygon([
        (1179091.1646903288, 712782.8838459781),
        (1161053.0218226474, 667456.2684348812),
        (1214704.933941905,  641092.8288590391),
        (1228580.428455506,  682719.3123998424),
        (1218405.0658121984, 721108.1805541387),
        (1179091.1646903288, 712782.8838459781)
    ])) == wktpoly
end

@testset "Create a Polygon with holes" begin

    wktpoly = "POLYGON ((1154115.27456585 686419.444270136,1154115.27456585 653118.257437493," *
    "1165678.18666051 653118.257437493,1165678.18666051 686419.444270136," *
    "1154115.27456585 686419.444270136)," *
    "(1149490.10972798 691044.609108003,1149490.10972798 648030.57611584," *
    "1191579.10975257 648030.57611584,1191579.10975257 691044.609108003," *
    "1149490.10972798 691044.609108003))"

    # Method 1
    AG.createpolygon([# outring
                      [(1154115.274565847,  686419.4442701361),
                       (1154115.274565847,  653118.2574374934),
                       (1165678.1866605144, 653118.2574374934),
                       (1165678.1866605144, 686419.4442701361),
                       (1154115.274565847,  686419.4442701361)],
                      # innerring(s)
                      [(1149490.1097279799, 691044.6091080031),
                       (1149490.1097279799, 648030.5761158396),
                       (1191579.1097525698, 648030.5761158396),
                       (1191579.1097525698, 691044.6091080031),
                       (1149490.1097279799, 691044.6091080031)]
                     ]) do poly
        @test AG.toWKT(poly) == wktpoly
    end

    # Method 2
    AG.createpolygon() do poly
        outring = AG.creategeom(GDAL.wkbLinearRing)
            AG.addpoint!(outring, 1154115.274565847, 686419.4442701361)
            AG.addpoint!(outring, 1154115.274565847, 653118.2574374934)
            AG.addpoint!(outring, 1165678.1866605144, 653118.2574374934)
            AG.addpoint!(outring, 1165678.1866605144, 686419.4442701361)
            AG.addpoint!(outring, 1154115.274565847, 686419.4442701361)

        innerring = AG.creategeom(GDAL.wkbLinearRing)
            AG.addpoint!(innerring, 1149490.1097279799, 691044.6091080031)
            AG.addpoint!(innerring, 1149490.1097279799, 648030.5761158396)
            AG.addpoint!(innerring, 1191579.1097525698, 648030.5761158396)
            AG.addpoint!(innerring, 1191579.1097525698, 691044.6091080031)
            AG.addpoint!(innerring, 1149490.1097279799, 691044.6091080031)

        AG.addgeom!(poly, outring)
        AG.addgeom!(poly, innerring)
        @test AG.toWKT(poly) == wktpoly
    end

    # Method 3
    AG.creategeom(GDAL.wkbPolygon) do poly
        outring = AG.creategeom(GDAL.wkbLinearRing)
            AG.addpoint!(outring, 1154115.274565847, 686419.4442701361)
            AG.addpoint!(outring, 1154115.274565847, 653118.2574374934)
            AG.addpoint!(outring, 1165678.1866605144, 653118.2574374934)
            AG.addpoint!(outring, 1165678.1866605144, 686419.4442701361)
            AG.addpoint!(outring, 1154115.274565847, 686419.4442701361)

        innerring = AG.creategeom(GDAL.wkbLinearRing)
            AG.addpoint!(innerring, 1149490.1097279799, 691044.6091080031)
            AG.addpoint!(innerring, 1149490.1097279799, 648030.5761158396)
            AG.addpoint!(innerring, 1191579.1097525698, 648030.5761158396)
            AG.addpoint!(innerring, 1191579.1097525698, 691044.6091080031)
            AG.addpoint!(innerring, 1149490.1097279799, 691044.6091080031)

        AG.addgeom!(poly, outring)
        AG.addgeom!(poly, innerring)
        @test AG.toWKT(poly) == wktpoly
    end

    # Method 4
    poly = AG.creategeom(GDAL.wkbPolygon)
        outring = AG.creategeom(GDAL.wkbLinearRing)
            AG.addpoint!(outring, 1154115.274565847, 686419.4442701361)
            AG.addpoint!(outring, 1154115.274565847, 653118.2574374934)
            AG.addpoint!(outring, 1165678.1866605144, 653118.2574374934)
            AG.addpoint!(outring, 1165678.1866605144, 686419.4442701361)
            AG.addpoint!(outring, 1154115.274565847, 686419.4442701361)
        AG.addgeom!(poly, outring)

        innerring = AG.creategeom(GDAL.wkbLinearRing)
            AG.addpoint!(innerring, 1149490.1097279799, 691044.6091080031)
            AG.addpoint!(innerring, 1149490.1097279799, 648030.5761158396)
            AG.addpoint!(innerring, 1191579.1097525698, 648030.5761158396)
            AG.addpoint!(innerring, 1191579.1097525698, 691044.6091080031)
            AG.addpoint!(innerring, 1149490.1097279799, 691044.6091080031)
        AG.addgeom!(poly, innerring)
    @test AG.toWKT(poly) == wktpoly

    # Method 5
    @test AG.toWKT(AG.createpolygon([
        # outerring
        [(1154115.274565847,  686419.4442701361),
         (1154115.274565847,  653118.2574374934),
         (1165678.1866605144, 653118.2574374934),
         (1165678.1866605144, 686419.4442701361),
         (1154115.274565847,  686419.4442701361)],
        # innerring(s)
        [(1149490.1097279799, 691044.6091080031),
         (1149490.1097279799, 648030.5761158396),
         (1191579.1097525698, 648030.5761158396),
         (1191579.1097525698, 691044.6091080031),
         (1149490.1097279799, 691044.6091080031)]
    ])) == wktpoly
end

@testset "Create a MultiPoint" begin

    wktpoints = "MULTIPOINT (1251243.73616105 598078.795866876," *
    "1240605.85703396 601778.927737169,1250318.70319348 606404.092575036)"

    # Method 1
    AG.createmultipoint([(1251243.7361610543, 598078.7958668759),
                         (1240605.8570339603, 601778.9277371694),
                         (1250318.7031934808, 606404.0925750365)]) do multipoint
        @test AG.toWKT(multipoint) == wktpoints
    end

    # Method 2
    AG.createmultipoint() do multipoint
        point1 = AG.createpoint(1251243.7361610543, 598078.7958668759)
        point2 = AG.createpoint(1240605.8570339603, 601778.9277371694)
        point3 = AG.createpoint(1250318.7031934808, 606404.0925750365)
        AG.addgeom!(multipoint, point1)
        AG.addgeom!(multipoint, point2)
        AG.addgeom!(multipoint, point3)
        @test AG.toWKT(multipoint) == wktpoints
    end

    # Method 3
    AG.creategeom(GDAL.wkbMultiPoint) do multipoint
        point1 = AG.createpoint(1251243.7361610543, 598078.7958668759)
        point2 = AG.createpoint(1240605.8570339603, 601778.9277371694)
        point3 = AG.createpoint(1250318.7031934808, 606404.0925750365)
        AG.addgeom!(multipoint, point1)
        AG.addgeom!(multipoint, point2)
        AG.addgeom!(multipoint, point3)
        @test AG.toWKT(multipoint) == wktpoints
    end

    # Method 4
    multipoint = AG.creategeom(GDAL.wkbMultiPoint)
        point1 = AG.createpoint(1251243.7361610543, 598078.7958668759)
        point2 = AG.createpoint(1240605.8570339603, 601778.9277371694)
        point3 = AG.createpoint(1250318.7031934808, 606404.0925750365)
        AG.addgeom!(multipoint, point1)
        AG.addgeom!(multipoint, point2)
        AG.addgeom!(multipoint, point3)
    @test AG.toWKT(multipoint) == wktpoints

    # Method 5
    @test AG.toWKT(AG.createmultipoint([
        (1251243.7361610543, 598078.7958668759),
        (1240605.8570339603, 601778.9277371694),
        (1250318.7031934808, 606404.0925750365)
    ])) == wktpoints
end

@testset "Create a MultiLineString" begin

    wktline = "MULTILINESTRING ((1214242.41745812 617041.971702131,1234593.14274473 629529.916764372)," *
    "(1184641.36249577 626754.817861651,1219792.61526356 606866.609058823))"

    # Method 1
    AG.createmultilinestring(Vector{Tuple{Float64,Float64}}[
                              [(1214242.4174581182, 617041.9717021306),
                               (1234593.142744733,  629529.9167643716)],
                              [(1184641.3624957693, 626754.8178616514),
                               (1219792.6152635587, 606866.6090588232)]
                             ]) do multiline
        @test GeoInterface.geotype(multiline) == :MultiLineString
        @test AG.toWKT(multiline) == wktline
    end

    # Method 2
    AG.createmultilinestring() do multiline
        AG.addgeom!(multiline, AG.createlinestring(
            [(1214242.4174581182, 617041.9717021306),
             (1234593.142744733,  629529.9167643716)]))
        AG.addgeom!(multiline, AG.createlinestring(
            [(1184641.3624957693, 626754.8178616514),
             (1219792.6152635587, 606866.6090588232)]))
        @test AG.toWKT(multiline) == wktline
    end

    # Method 3
    AG.creategeom(GDAL.wkbMultiLineString) do multiline
        line = AG.creategeom(GDAL.wkbLineString)
            AG.addpoint!(line, 1214242.4174581182, 617041.9717021306)
            AG.addpoint!(line, 1234593.142744733, 629529.9167643716)
        AG.addgeom!(multiline, line)

        line = AG.creategeom(GDAL.wkbLineString)
            AG.addpoint!(line, 1184641.3624957693, 626754.8178616514)
            AG.addpoint!(line, 1219792.6152635587, 606866.6090588232)
        AG.addgeom!(multiline, line)

        @test AG.toWKT(multiline) == wktline
    end

    # Method 4
    multiline = AG.creategeom(GDAL.wkbMultiLineString)
        line = AG.creategeom(GDAL.wkbLineString)
            AG.addpoint!(line, 1214242.4174581182, 617041.9717021306)
            AG.addpoint!(line, 1234593.142744733, 629529.9167643716)
        AG.addgeom!(multiline, line)
        line = AG.creategeom(GDAL.wkbLineString)
            AG.addpoint!(line, 1184641.3624957693, 626754.8178616514)
            AG.addpoint!(line, 1219792.6152635587, 606866.6090588232)
        AG.addgeom!(multiline, line)

    @test AG.toWKT(multiline) == wktline

    # Method 5
    @test AG.toWKT(AG.createmultilinestring(Vector{Tuple{Float64,Float64}}[
        [(1214242.4174581182, 617041.9717021306),
         (1234593.142744733,  629529.9167643716)],
        [(1184641.3624957693, 626754.8178616514),
         (1219792.6152635587, 606866.6090588232)]
    ])) == wktline
end

@testset "Create a MultiPolygon" begin

    wktmultipolygon = "MULTIPOLYGON " *
    "(((1204067.05481481 634617.598086025,1204067.05481481 620742.103572424," *
    "1215167.45042569 620742.103572424,1215167.45042569 634617.598086025," *
    "1204067.05481481 634617.598086025))," *
    "((1179553.68117412 647105.543148266,1179553.68117412 626292.301377865," *
    "1194354.20865529 626292.301377865,1194354.20865529 647105.543148266," *
    "1179553.68117412 647105.543148266)))"

    # Method 1
    AG.createmultipolygon_noholes(Vector{Tuple{Float64,Float64}}[
             [(1204067.0548148106, 634617.5980860253),
              (1204067.0548148106, 620742.1035724243),
              (1215167.4504256917, 620742.1035724243),
              (1215167.4504256917, 634617.5980860253),
              (1204067.0548148106, 634617.5980860253)],
             [(1179553.6811741155, 647105.5431482664),
              (1179553.6811741155, 626292.3013778647),
              (1194354.20865529,   626292.3013778647),
              (1194354.20865529,   647105.5431482664),
              (1179553.6811741155, 647105.5431482664)]
            ]) do multipolygon
        @test AG.toWKT(multipolygon) == wktmultipolygon
    end

    # Method 2
    AG.createmultipolygon() do multipolygon
        poly = AG.createpolygon(
            [(1204067.0548148106, 634617.5980860253),
             (1204067.0548148106, 620742.1035724243),
             (1215167.4504256917, 620742.1035724243),
             (1215167.4504256917, 634617.5980860253),
             (1204067.0548148106, 634617.5980860253)])
        AG.addgeom!(multipolygon,poly)

        poly = AG.createpolygon(
            [(1179553.6811741155, 647105.5431482664),
             (1179553.6811741155, 626292.3013778647),
             (1194354.20865529,   626292.3013778647),
             (1194354.20865529,   647105.5431482664),
             (1179553.6811741155, 647105.5431482664)])
        AG.addgeom!(multipolygon,poly)

        @test AG.toWKT(multipolygon) == wktmultipolygon
    end

    # Method 3
    AG.creategeom(GDAL.wkbMultiPolygon) do multipolygon
        poly = AG.creategeom(GDAL.wkbPolygon)
            ring = AG.creategeom(GDAL.wkbLinearRing)
                AG.addpoint!(ring, 1204067.0548148106, 634617.5980860253)
                AG.addpoint!(ring, 1204067.0548148106, 620742.1035724243)
                AG.addpoint!(ring, 1215167.4504256917, 620742.1035724243)
                AG.addpoint!(ring, 1215167.4504256917, 634617.5980860253)
                AG.addpoint!(ring, 1204067.0548148106, 634617.5980860253)
            AG.addgeom!(poly, ring)
        AG.addgeom!(multipolygon,poly)

        poly = AG.creategeom(GDAL.wkbPolygon)
            ring = AG.creategeom(GDAL.wkbLinearRing)
                AG.addpoint!(ring, 1179553.6811741155, 647105.5431482664)
                AG.addpoint!(ring, 1179553.6811741155, 626292.3013778647)
                AG.addpoint!(ring, 1194354.20865529,   626292.3013778647)
                AG.addpoint!(ring, 1194354.20865529,   647105.5431482664)
                AG.addpoint!(ring, 1179553.6811741155, 647105.5431482664)
            AG.addgeom!(poly, ring)
        AG.addgeom!(multipolygon,poly)

        @test AG.toWKT(multipolygon) == wktmultipolygon
    end

    # Method 4
    multipolygon = AG.creategeom(GDAL.wkbMultiPolygon)
        poly = AG.creategeom(GDAL.wkbPolygon)
            ring = AG.creategeom(GDAL.wkbLinearRing)
                AG.addpoint!(ring, 1204067.0548148106, 634617.5980860253)
                AG.addpoint!(ring, 1204067.0548148106, 620742.1035724243)
                AG.addpoint!(ring, 1215167.4504256917, 620742.1035724243)
                AG.addpoint!(ring, 1215167.4504256917, 634617.5980860253)
                AG.addpoint!(ring, 1204067.0548148106, 634617.5980860253)
            AG.addgeom!(poly, ring)
        AG.addgeom!(multipolygon,poly)

        poly = AG.creategeom(GDAL.wkbPolygon)
            ring = AG.creategeom(GDAL.wkbLinearRing)
                AG.addpoint!(ring, 1179553.6811741155, 647105.5431482664)
                AG.addpoint!(ring, 1179553.6811741155, 626292.3013778647)
                AG.addpoint!(ring, 1194354.20865529,   626292.3013778647)
                AG.addpoint!(ring, 1194354.20865529,   647105.5431482664)
                AG.addpoint!(ring, 1179553.6811741155, 647105.5431482664)
            AG.addgeom!(poly, ring)
        AG.addgeom!(multipolygon,poly)

    @test AG.toWKT(multipolygon) == wktmultipolygon

    # Method 5
    @test AG.toWKT(AG.createmultipolygon_noholes(Vector{Tuple{Float64,Float64}}[
             [(1204067.0548148106, 634617.5980860253),
              (1204067.0548148106, 620742.1035724243),
              (1215167.4504256917, 620742.1035724243),
              (1215167.4504256917, 634617.5980860253),
              (1204067.0548148106, 634617.5980860253)],
             [(1179553.6811741155, 647105.5431482664),
              (1179553.6811741155, 626292.3013778647),
              (1194354.20865529,   626292.3013778647),
              (1194354.20865529,   647105.5431482664),
              (1179553.6811741155, 647105.5431482664)]
            ])) == wktmultipolygon
end

@testset "Create a GeometryCollection" begin

    wktcollection = "GEOMETRYCOLLECTION " *
    "(POINT (-122.23 47.09),LINESTRING (-122.6 47.14,-122.48 47.23))"

    # Method not applicable here

    # Method 2
    AG.creategeomcollection() do geomcol
        for g in [AG.createpoint(-122.23, 47.09),
                  AG.createlinestring([(-122.60, 47.14), (-122.48, 47.23)])]
            AG.addgeom!(geomcol, g)
        end
        @test AG.toWKT(geomcol) == wktcollection
    end

    # Method 3
    AG.creategeom(GDAL.wkbGeometryCollection) do geomcol
        point = AG.creategeom(GDAL.wkbPoint)
            AG.addpoint!(point, -122.23, 47.09)
        AG.addgeom!(geomcol, point)

        line = AG.creategeom(GDAL.wkbLineString)
            AG.addpoint!(line, -122.60, 47.14)
            AG.addpoint!(line, -122.48, 47.23)
        AG.addgeom!(geomcol, line)

        @test AG.toWKT(geomcol) == wktcollection
    end

    # Method 4
    geomcol =  AG.creategeom(GDAL.wkbGeometryCollection)
        point = AG.creategeom(GDAL.wkbPoint)
            AG.addpoint!(point, -122.23, 47.09)
        AG.addgeom!(geomcol, point)

        line = AG.creategeom(GDAL.wkbLineString)
            AG.addpoint!(line, -122.60, 47.14)
            AG.addpoint!(line, -122.48, 47.23)
        AG.addgeom!(geomcol, line)

    @test AG.toWKT(geomcol) == wktcollection
end

@testset "Create Geometry from WKT" begin
    wkt = "POINT (1120351.5712494177 741921.4223245403)"
    x, y = 1120351.5712494177, 741921.4223245403

    # Method 1
    AG.fromWKT(wkt) do point
        @test AG.getx(point, 0) ≈ x
        @test AG.gety(point, 0) ≈ y
    end

    # Method 2
    point = AG.fromWKT(wkt)
    @test AG.getx(point, 0) ≈ x
    @test AG.gety(point, 0) ≈ y
end

@testset "Create Geometry from GeoJSON" begin
    geojson = """{"type":"Point","coordinates":[108420.33,753808.59]}"""
    x, y = 108420.33, 753808.59

    # Method 1
    AG.fromJSON(geojson) do point
        @test AG.getx(point, 0) ≈ x
        @test AG.gety(point, 0) ≈ y
    end

    # Method 2
    point = AG.fromJSON(geojson)
    @test AG.getx(point, 0) ≈ x
    @test AG.gety(point, 0) ≈ y
end

@testset "Create Geometry from GML" begin
    gml = """<gml:Point xmlns:gml="http://www.opengis.net/gml">
             <gml:coordinates>108420.33,753808.59</gml:coordinates>
             </gml:Point>"""
    x, y = 108420.33, 753808.59

    # Method 1
    AG.fromGML(gml) do point
        @test AG.getx(point, 0) ≈ x
        @test AG.gety(point, 0) ≈ y
    end

    # Method 2
    point = AG.fromGML(gml)
    @test AG.getx(point, 0) ≈ x
    @test AG.gety(point, 0) ≈ y
end

@testset "Create Geometry from WKB" begin
    wkb = [0x01,0x01,0x00,0x00,0x00,0x7b,0x14,0xae,0x47,0x45,0x78,0xfa,0x40,
           0xe1,0x7a,0x14,0x2e,0x21,0x01,0x27,0x41]
    x, y = 108420.33, 753808.59

    # Method 1
    AG.fromWKB(wkb) do point
        @test AG.getx(point, 0) ≈ x
        @test AG.gety(point, 0) ≈ y
    end

    # Method 2
    point = AG.fromWKB(wkb)
    @test AG.getx(point, 0) ≈ x
    @test AG.gety(point, 0) ≈ y
end

@testset "Count Points in a LineString" begin
    wkt = "LINESTRING (1181866.263593049 615654.4222507705, 1205917.1207499576 623979.7189589312, 1227192.8790041457 643405.4112779726, 1224880.2965852122 665143.6860159477)"

    # Method 1
    AG.fromWKT(wkt) do geom
        @test AG.ngeom(geom) == 4
    end

    # Method 2
    @test AG.ngeom(AG.fromWKT(wkt)) == 4
end

@testset "Count Points in a MultiPoint" begin
    wkt = "MULTIPOINT (1181866.263593049 615654.4222507705, 1205917.1207499576 623979.7189589312, 1227192.8790041457 643405.4112779726, 1224880.2965852122 665143.6860159477)"

    # Method 1
    AG.fromWKT(wkt) do geom
        @test AG.ngeom(geom) == 4
    end

    # Method 2
    @test AG.ngeom(AG.fromWKT(wkt)) == 4
end

@testset "Iterate over Geometries in a Geometry" begin
    wkt = "MULTIPOINT (1181866.263593049 615654.4222507705, 1205917.1207499576 623979.7189589312, 1227192.8790041457 643405.4112779726, 1224880.2965852122 665143.6860159477)"
    AG.fromWKT(wkt) do geom
        # TODO Should getgeom use Julian counting from 1?
        @test AG.toWKT(AG.getgeom(geom, 3)) == "POINT (1224880.29658521 665143.686015948)"
    end
end

@testset "Iterate over Points in a Geometry" begin
    wkt = "LINESTRING (1181866.263593049 615654.4222507705, 1205917.1207499576 623979.7189589312, 1227192.8790041457 643405.4112779726, 1224880.2965852122 665143.6860159477)"
    AG.fromWKT(wkt) do geom
        @test AG.getpoint(geom, 3) == (1.2248802965852122e6, 665143.6860159477, 0.0)
    end
end

@testset "Buffer a Geometry" begin
    wkt = "POINT (1198054.34 648493.09)"

    # Method 1
    AG.fromWKT(wkt) do pt
        bufferdist = 500
        AG.buffer(pt, bufferdist) do poly
            @test AG.getgeomtype(poly) == GDAL.wkbPolygon
        end
    end

    # Method 2
    @test AG.getgeomtype(AG.buffer(AG.fromWKT(wkt), 500)) == GDAL.wkbPolygon
end

@testset "Calculate Envelope of a Geometry" begin
    wkt = "LINESTRING (1181866.263593049 615654.4222507705, 1205917.1207499576 623979.7189589312, 1227192.8790041457 643405.4112779726, 1224880.2965852122 665143.6860159477)"
    AG.fromWKT(wkt) do line
        env = AG.envelope(line)
        @test (env.MaxX - env.MinX) * (env.MaxY - env.MinY) ≈ 2.2431808256625123e9
    end
end

@testset "Calculate Bounding Box of a Geometry" begin
    wkt = "POLYGON ((1162440.5712740074 672081.4332727483, 1162440.5712740074 647105.5431482664, 1195279.2416228633 647105.5431482664, 1195279.2416228633 672081.4332727483, 1162440.5712740074 672081.4332727483))"
    expected = AG.createpolygon([[672081.4332727483, 1162440.5712740074], [672081.4332727483, 1195279.2416228633], [647105.5431482664, 1195279.2416228633], [647105.5431482664, 1162440.5712740074], [672081.4332727483, 1162440.5712740074]])
    AG.fromWKT(wkt) do polygon
        calculated = AG.boundingbox(polygon)
        @test AG.equals(expected, calculated)
    end
end

@testset "Calculate the Area of a Geometry" begin
    wkt = "POLYGON ((1162440.5712740074 672081.4332727483, 1162440.5712740074 647105.5431482664, 1195279.2416228633 647105.5431482664, 1195279.2416228633 672081.4332727483, 1162440.5712740074 672081.4332727483))"
    AG.fromWKT(wkt) do poly
        @test AG.geomarea(poly) ≈ 8.201750224671059e8
    end
end

@testset "Calculate the Length of a Geometry" begin
    wkt = "LINESTRING (1181866.263593049 615654.4222507705, 1205917.1207499576 623979.7189589312, 1227192.8790041457 643405.4112779726, 1224880.2965852122 665143.6860159477)"
    AG.fromWKT(wkt) do line
        @test AG.geomlength(line) ≈ 76121.94397805972
    end
end

@testset "Get the geometry type (as a string) from a Geometry" begin
    types = ["POINT", "LINESTRING", "POLYGON"]
    wkts = [
        "POINT (1198054.34 648493.09)",
        "LINESTRING (1181866.263593049 615654.4222507705, 1205917.1207499576 623979.7189589312, 1227192.8790041457 643405.4112779726, 1224880.2965852122 665143.6860159477)",
        "POLYGON ((1162440.5712740074 672081.4332727483, 1162440.5712740074 647105.5431482664, 1195279.2416228633 647105.5431482664, 1195279.2416228633 672081.4332727483, 1162440.5712740074 672081.4332727483))"
    ]
    for (i, wkt) in enumerate(wkts)
        AG.fromWKT(wkt) do geom
            @test AG.geomname(geom) == types[i]
        end
    end
end

@testset "Calculate intersection between two Geometries" begin
    wkt1 = "POLYGON ((1208064.271243039 624154.6783778917, 1208064.271243039 601260.9785661874, 1231345.9998651114 601260.9785661874, 1231345.9998651114 624154.6783778917, 1208064.271243039 624154.6783778917))"
    wkt2 = "POLYGON ((1199915.6662253144 633079.3410163528, 1199915.6662253144 614453.958118695, 1219317.1067437078 614453.958118695, 1219317.1067437078 633079.3410163528, 1199915.6662253144 633079.3410163528)))"
    wkt3 = "POLYGON ((1208064.27124304 624154.678377892,1219317.10674371 624154.678377892,1219317.10674371 614453.958118695,1208064.27124304 614453.958118695,1208064.27124304 624154.678377892))"

    # Method 1
    AG.fromWKT(wkt1) do poly1
    AG.fromWKT(wkt2) do poly2
        AG.intersection(poly1, poly2) do poly3
            @test AG.toWKT(poly3) == wkt3
        end
    end
    end

    # Method 2
    @test AG.toWKT(AG.intersection(AG.fromWKT(wkt1), AG.fromWKT(wkt2))) == wkt3
end

@testset "Calculate union between two Geometries" begin
    wkt1 = "POLYGON ((1208064.271243039 624154.6783778917, 1208064.271243039 601260.9785661874, 1231345.9998651114 601260.9785661874, 1231345.9998651114 624154.6783778917, 1208064.271243039 624154.6783778917))"
    wkt2 = "POLYGON ((1199915.6662253144 633079.3410163528, 1199915.6662253144 614453.958118695, 1219317.1067437078 614453.958118695, 1219317.1067437078 633079.3410163528, 1199915.6662253144 633079.3410163528)))"
    wkt3 = "POLYGON ((1199915.66622531 614453.958118695,1199915.66622531 633079.341016353,1219317.10674371 633079.341016353,1219317.10674371 624154.678377892,1231345.99986511 624154.678377892,1231345.99986511 601260.978566187,1208064.27124304 601260.978566187,1208064.27124304 614453.958118695,1199915.66622531 614453.958118695))"

    # Method 1
    AG.fromWKT(wkt1) do poly1
    AG.fromWKT(wkt2) do poly2
        AG.union(poly1, poly2) do poly3
            @test AG.toWKT(poly3) == wkt3
        end
    end
    end

    # Method 2
    @test AG.toWKT(AG.union(AG.fromWKT(wkt1), AG.fromWKT(wkt2))) == wkt3
end

@testset "Write Geometry to GeoJSON|GML|WKT|WKB" begin
    AG.createpolygon([(1179091.1646903288, 712782.8838459781),
                      (1161053.0218226474, 667456.2684348812),
                      (1214704.933941905,  641092.8288590391),
                      (1228580.428455506,  682719.3123998424),
                      (1218405.0658121984, 721108.1805541387),
                      (1179091.1646903288, 712782.8838459781)]) do poly
        @test AG.toJSON(poly)[1:19] == "{ \"type\": \"Polygon\""
        @test AG.toGML(poly)[1:13] == "<gml:Polygon>"
        @test AG.toWKT(poly)[1:7] == "POLYGON"
        @test AG.toWKB(poly) == UInt8[0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x38, 0x25, 0x29, 0x2a, 0xd3, 0xfd, 0x31, 0x41, 0xc5, 0x75, 0x87, 0xc4, 0x9d, 0xc0, 0x25, 0x41, 0x45, 0x2b, 0x96, 0x05, 0x5d, 0xb7, 0x31, 0x41, 0xf8, 0x4b, 0x70, 0x89, 0x80, 0x5e, 0x24, 0x41, 0x12, 0xd1, 0x16, 0xef, 0xf0, 0x88, 0x32, 0x41, 0x44, 0x36, 0x60, 0xa8, 0x89, 0x90, 0x23, 0x41, 0x92, 0x42, 0xaf, 0x6d, 0x24, 0xbf, 0x32, 0x41, 0x45, 0xdf, 0xf2, 0x9f, 0xbe, 0xd5, 0x24, 0x41, 0x78, 0x11, 0xd9, 0x10, 0x65, 0x97, 0x32, 0x41, 0x92, 0x97, 0x71, 0x5c, 0xa8, 0x01, 0x26, 0x41, 0x38, 0x25, 0x29, 0x2a, 0xd3, 0xfd, 0x31, 0x41, 0xc5, 0x75, 0x87, 0xc4, 0x9d, 0xc0, 0x25, 0x41]
    end
end

@testset "Force polygon to multipolygon" begin
    wkt = "POLYGON ((1179091.164690328761935 712782.883845978067257,1161053.021822647424415 667456.268434881232679,1214704.933941904921085 641092.828859039116651,1228580.428455505985767 682719.312399842427112,1218405.065812198445201 721108.180554138729349,1179091.164690328761935 712782.883845978067257))"

    # Method 1
    AG.fromWKT(wkt) do poly
        @test AG.getgeomtype(poly) == GDAL.wkbPolygon
        AG.forceto(poly, GDAL.wkbMultiPolygon) do mpoly
            @test AG.getgeomtype(mpoly) == GDAL.wkbMultiPolygon
        end
    end

    # Method 2
    @test AG.getgeomtype(AG.forceto(AG.fromWKT(wkt), GDAL.wkbMultiPolygon)) == GDAL.wkbMultiPolygon
end


using ComplexMixtures
using PDBTools
using Plots
using LaTeXStrings
using EasyFit

# Plot defaults
plot_font = "Computer Modern"
default(
    fontfamily=plot_font,
    linewidth=2.5,
    framestyle=:box,
    label=nothing,
    grid=false,
    palette=:tab10
)
scalefontsizes(); scalefontsizes(1.3)

# Load system PDB file
system = readPDB("./simulation/equilibrated.pdb")

# Select the atoms corresponding to glycerol and water
dmf = select(system,"resname DMF")
acr = select(system,"resname FACR or resname ACR or resname LACR")

# Set ComplexMixtures selections
solute = Selection(acr,nmols=1)
solvent = Selection(dmf,natomspermol=12)

# Load previously computed data
mddf_dmf_acr = load("./results/mddf_dmf_acr.json")

# Plot the MDDF of DMF relative to PolyACR and its corresponding KB integral
plot(layout=(2,1))
x = mddf_dmf_acr.d
y = movavg(mddf_dmf_acr.mddf,n=10).x
plot!(x,y,ylabel="MDDF",subplot=1)
plot!(xlim=(0,20),subplot=1)

y = movavg(mddf_dmf_acr.kb,n=10).x
plot!(x,y,xlabel=L"\textrm{Distance / \AA}",ylabel=L"\textrm{KB~/~cm^3~mol^{-1}}",subplot=2)
plot!(xlim=(0,20),subplot=2)
savefig("./results/mddf_kb.png")

# Plot DMF group contributions to the MDDF. We use a vector of tuples here,
# the first element of the tuple is a list of atom names, and the second element
# is the label of the group, to be used in the plot.
groups = [
    (["C","O"],"CO"), # carbonyl
    (["HA"],"HA"),
    (["N"],"N"),
    (["CC","CT","HC1","HC2","HC3","HT1","HT2","HT3"],"Methyl groups")
]
plot(layout=(2,1))
x = mddf_dmf_acr.d
y = movavg(mddf_dmf_acr.mddf,n=10).x
plot!(x,y,label="Total",subplot=1)
for group in groups
    # Retrieve the contributions of the atoms of this group
    group_contrib = contrib(solvent,mddf_dmf_acr.solvent_atom,group[1])
    y = movavg(group_contrib,n=10).x
    plot!(x,y,label=group[2],subplot=1)
end
plot!(
    xlim=(1,5),
    ylabel="MDDF",
    subplot=1
)

# Plot ACR group contributions to the MDDF
groups = [
    (["CF","HF1","HF2","HF3",
      "CL","HL1","HL2","HL3"],L"\textrm{CH_3}"), # terminal methyles
    (["OE1","CD"],"CO"), # carbonyl
    (["NE2","HE22","HE21"],L"\textrm{NH_2}"), # amine
    (["C","H2","H1","CA","HA"],L"\textrm{CHCH_2}"), # backbone
]
x = mddf_dmf_acr.d
y = movavg(mddf_dmf_acr.mddf,n=10).x
plot!(x,y,label="Total",subplot=2)
for group in groups
    group_contrib = contrib(solute,mddf_dmf_acr.solute_atom,group[1])
    y = movavg(group_contrib,n=10).x
    plot!(x,y,label=group[2],subplot=2)
end
plot!(
    xlim=(1,5),
    xlabel=L"\textrm{Distance / \AA}",
    ylabel="MDDF", subplot=2
)
savefig("./results/mddf_groups.png")

# 2D plot of group contributions
groups = [
    (["CF","HF1","HF2","HF3"],L"\textrm{CH_3}"),
    (["OE1","CD"],"CO"),
    (["NE2","HE22","HE21"],L"\textrm{NH_2}"),
    (["C","H2","H1","CA","HA"],L"\textrm{CHCH_2}"),
    (["CL","HL1","HL2","HL3"],L"\textrm{CH_3}")
]

# Here we split the polymer in residues, to extract the contribution of
# each chemical group of each polymer mer independently
mers = collect(eachresidue(acr))
group_contribs = zeros(length(mddf_dmf_acr.d),0)
labels = String[]
for (imer, mer) in pairs(mers)
    # The mer.atoms array is just the original array of atoms,
    # and the atoms of this residue are retrieved by choosing the
    # corresponding range (the eachresidue function does not copy)
    mer_atoms = mer.atoms[mer.range]
    for igroup in 1:length(groups)
        group = groups[igroup]
        (imer != 1 && igroup == 1) && continue # only first residue has a first CH3
        (imer != 5 && igroup == 5) && continue # only last residue has a terminal CH3
        # And from the mer_atoms atoms, filter the ones corresponding to this group
        atoms = filter(at -> at.name in group[1], mer_atoms)
        # Retrive the contribution of these atoms to the MDDF
        contribs = movavg(contrib(solute,mddf_dmf_acr.solute_atom,atoms),n=10).x
        # Concatenate the results to build the 2D matrix
        group_contribs = hcat(group_contribs,contribs)
        # Push label to label list
        push!(labels,group[2])
    end
end

# Find the indices of the limits of the map we want
idmin = findfirst( d -> d > 1.5, mddf_dmf_acr.d)
idmax = findfirst( d -> d > 3.2, mddf_dmf_acr.d)

# Plot contour map
contourf(
    1:length(labels),
    mddf_dmf_acr.d[idmin:idmax],
    group_contribs[idmin:idmax,:],
    color=cgrad(:tempo),linewidth=1,linecolor=:black,
    colorbar=:none,levels=10,
    xlabel="Group",ylabel=L"r/\AA",xrotation=60,
    xticks=(1:length(labels),labels),
    margin=5Plots.Measures.mm # adjust margin
)
savefig("./results/map2D_acr.png")


export TabularApproximator, TabularVApproximator, TabularQApproximator

"""
    TabularApproximator(table<:AbstractArray, opt)

For `table` of 1-d, it will serve as a state value approximator.
For `table` of 2-d, it will serve as a state-action value approximator.

!!! warning
    For `table` of 2-d, the first dimension is action and the second dimension is state.
"""
struct TabularApproximator{N,T<:AbstractArray{Float64},O} <: AbstractApproximator
    table::T
    optimizer::O
    function TabularApproximator(table::T, opt::O) where {T<:AbstractArray,O}
        n = ndims(table)
        n <= 2 || throw(ArgumentError("the dimension of table must be <= 2"))
        new{n,T,O}(table, opt)
    end
end

const TabularVApproximator = TabularApproximator{1}
const TabularQApproximator = TabularApproximator{2}

TabularVApproximator(; n_state, init = 0.0, opt = InvDecay(1.0)) =
    TabularApproximator(fill(init, n_state), opt)
TabularQApproximator(; n_state, n_action, init = 0.0, opt = InvDecay(1.0)) =
    TabularApproximator(fill(init, n_action, n_state), opt)

(app::TabularVApproximator)(s::Int) = @views app.table[s]

(app::TabularQApproximator)(s::Int) = @views app.table[:, s]
(app::TabularQApproximator)(s::Int, a::Int) = app.table[a, s]

function RLBase.update!(app::TabularVApproximator, correction::Pair{Int,Float64})
    s, e = correction
    x = @view app.table[s]
    x̄ = @view [e][1]
    Flux.Optimise.update!(app.optimizer, x, x̄)
end

function RLBase.update!(app::TabularQApproximator, correction::Pair{Tuple{Int,Int},Float64})
    (s, a), e = correction
    x = @view app.table[a, s]
    x̄ = @view [e][1]
    Flux.Optimise.update!(app.optimizer, x, x̄)
end

function RLBase.update!(app::TabularQApproximator, correction::Pair{Int,Vector{Float64}})
    s, errors = correction
    x = @view app.table[:, s]
    Flux.Optimise.update!(app.optimizer, x, errors)
end