# ======================================
# Solve function for continuous systems
# ======================================

"""
    solve(ivp::IVP{<:AbstractContinuousSystem}, tspan, alg; kwargs...)

Solves the initial-value problem defined by `ivp` over the time span `tspan`,
using the algorihm `alg`. If no algorithm is given, a default algorithm is chosen.

### Input

- `ivp`   -- initial-value problem
- `tspan` -- time span for this initial-value problem
- `alg`   -- reachability algorithm

Additional options are passed as arguments or keyword arguments; see the notes
below for details. See the online documentation for examples.

### Output

The solution of a reachability problem, as an instance of a `ReachSolution`.

### Notes

- Use the `alg`, `algorithm` or `opC` keyword arguments to specify the algorithm
  to solve the initial-value problem. Algorithm-specific options should be passed
  to the algorithm constructor as well.

- Use the `tspan` keyword argument to specify the time span; it can be:
    - a tuple,
    - an interval, or
    - a vector with two components.

- Use the `T` keyword argument to specify the time horizon; the initial time is
  then assumed to be zero.

- Use the `static` keyword argument to force conversion to static arrays in the
  algorithm (should be supported by the algorithm).

- Use the `NSTEPS` keyword argument to specify the number of discrete steps
  solved in the set-based recurrence.

- Use the `threading` option to use multi-threading parallelism. This option applies
  for initial-value problems whose initial condition is a vector of sets.
"""
function solve(ivp::IVP{<:AbstractContinuousSystem}, args...; kwargs...)

    # preliminary checks
    _check_dim(ivp)

    # get time span (or the emptyset if NSTEPS was specified)
    tspan = _get_tspan(args...; kwargs...)

    # get the continuous post or find a default one
    cpost = _get_cpost(ivp, args...; kwargs...)
    if cpost == nothing
        cpost = _default_cpost(ivp, tspan; kwargs...)
    end

    # run the continuous-post operator
    F = post(cpost, ivp, tspan; kwargs...)

    got_ensemble = get(kwargs, :ensemble, false)
    if got_ensemble
        @requires DifferentialEquations
        # compute trajectories using ensemble simulation
        ensemble_sol = _solve_ensemble(ivp, args...; kwargs...)
        dict = Dict{Symbol,Any}(:ensemble=>ensemble_sol)
        sol = ReachSolution(F, cpost, dict)
    else
        # wrap the flowpipe and algorithm in a solution structure
        sol = ReachSolution(F, cpost)
    end

    return sol
end

# solve for distributed initial conditions; uses multi-threaded implementation by default
function solve(ivp::IVP{AT, VT}, args...; kwargs...) where {AT<:AbstractContinuousSystem,
                                                            ST<:Union{LazySet, IntervalBox}, VT<:AbstractVector{ST}}
    _check_dim(ivp)
    tspan = _get_tspan(args...; kwargs...)
    cpost = _get_cpost(ivp, args...; kwargs...)
    if cpost == nothing
        cpost = _default_cpost(ivp, tspan; kwargs...)
    end

    X0 = initial_state(ivp)
    S = system(ivp)

    threading = haskey(kwargs, :threading) ? kwargs[:threading] : (Threads.nthreads() > 1)

    F = _solve_distributed(cpost, S, X0, tspan, Val(threading); kwargs...)
    return ReachSolution(MixedFlowpipe(F), cpost)
end

function _solve_distributed(cpost, S, X0, tspan, threading::Val{false}; kwargs...)
    return [post(cpost, IVP(S, X0i), tspan; kwargs...) for X0i in X0]
end

function _solve_distributed(cpost, S, X0, tspan, threading::Val{true}; kwargs...)
    nsets = length(X0)
    FT = Flowpipe{numtype(cpost), rsetrep(cpost)} # TODO add third parameter
    sol_tot = Vector{FT}(undef, nsets)

    Threads.@threads for i in 1:length(X0)
        sol_i = ReachabilityAnalysis.post(cpost, IVP(S, X0[i]), tspan; kwargs...)
        sol_tot[i] = sol_i
    end
    return sol_tot
end

# ====================================
# Continuous post operator interface
# ====================================

"""
    AbstractPost

Abstract supertype of all post operator types.
"""
abstract type AbstractPost end

"""
    AbstractContinuousPost

Abstract supertype of all continuous post operators.
"""
abstract type AbstractContinuousPost <: AbstractPost end

setrep(::InitialValueProblem{HS, ST}) where {HS, ST} = ST

# ==================
# Argument handling
# ==================

# extend dimension for common IntervalArithmetic types
LazySets.dim(::IA.Interval) = 1
LazySets.dim(::IA.IntervalBox{D, N}) where {D, N} = D

# lazy sets (or sets that behave like such -- TODO update once UnionSet <: LazySet)
_dim(X::Union{<:LazySet, <:IA.Interval, <:IA.IntervalBox, <:UnionSet, <:UnionSetArray}) = dim(X)

# singleton elements
_dim(X::Number) = 1
_dim(X::AbstractVector{N}) where {N<:Number} = length(X)

# vector of sets
function  _dim(X::AbstractVector{UT}) where {UT<:Union{<:LazySet,
                <:IA.Interval, <:IA.IntervalBox, <:UnionSet, <:UnionSetArray}}
    n = _dim(first(X))
    all(X -> _dim(X) == n, X)  || throw(ArgumentError("dimension mismatch between " *
            "the initial sets in this array; expected only sets of dimension $n"))
    return n
end

_dim(X) = throw(ArgumentError("the type of the initial condition, $(typeof(X)), cannot be handled"))

function _check_dim(S, X0; throw_error::Bool=true)
    n = statedim(S)
    d = _dim(X0)
    n == d && return true

    if throw_error
        throw(ArgumentError("the state-space dimension should match " *
                            "the dimension of the initial state, but they are " *
                            "$n and $d respectively"))
    end
    return false
end

_dim(X0::Tuple{VT, VT}) where {N, VT<:AbstractVector{N}} = length(X0[1]) + length(X0[2])
_dim(X0::Tuple{<:LazySet{N}, <:LazySet{N}}) where {N} = dim(X0[1]) + dim(X0[2])

function _check_dim(S::Union{SecondOrderLinearContinuousSystem,
                             SecondOrderAffineContinuousSystem,
                             SecondOrderConstrainedLinearControlContinuousSystem,
                             SecondOrderConstrainedAffineControlContinuousSystem},
                    X0; throw_error::Bool=true)
    n = statedim(S)
    d = _dim(X0)
    d == 2*n && return true

    if throw_error
        throw(ArgumentError("the dimension of the initial state should be " *
                            "twice the state-space dimension, but they are " *
                            "$d and $n respectively"))
    end
    return false
end

function _check_dim(ivp::InitialValueProblem{<:AbstractContinuousSystem}; throw_error::Bool=true)
    S = system(ivp)
    X0 = initial_state(ivp)
    _check_dim(S, X0, throw_error=throw_error)
end

# promotion from tuple-like arguments
@inline _promote_tspan((t1, t2)::Tuple{T, T}) where {T} = TimeInterval(t1, t2)
@inline _promote_tspan((t1, t2)::Tuple{T, S}) where {T, S} = TimeInterval(promote(t1, t2))

# no-op, corresponds to (inf(tspan), sup(tspan))
@inline _promote_tspan(tspan::IA.Interval) = tspan

# no-op, takes interval wrapped data; corresponds to (min(tspan), max(tspan))
@inline _promote_tspan(tspan::Interval) = tspan.dat

# number T defaults to a time interval of the form [0, T]
@inline _promote_tspan(tspan::Number) = TimeInterval(zero(tspan), tspan)

# catch-all array like tspan
@inline function _promote_tspan(tspan::AbstractArray)
    @assert length(tspan) == 2 throw(ArgumentError("the length of `tspan` must be two, but " *
                                                   "it is of length $(length(tspan))"))
    return TimeInterval(tspan[1], tspan[2])
end

function _get_tspan(args...; kwargs...)
    got_tspan = haskey(kwargs, :tspan)
    got_T = haskey(kwargs, :T)
    got_NSTEPS = haskey(kwargs, :NSTEPS)

    # throw error for repeated arguments
    if got_tspan && got_T
        throw(ArgumentError("cannot parse the time horizon `T` and the " *
            "time span `tspan` simultaneously; use one or the other"))
    elseif got_tspan && got_NSTEPS
        throw(ArgumentError("cannot parse the time span `tspan` and the " *
            "number of steps `NSTEPS` simultaneously; use one or the other"))
    elseif got_T && got_NSTEPS
        throw(ArgumentError("cannot parse the time horizon `T` and the " *
            "number of steps `NSTEPS` simultaneously; use one or the other"))
    end

    # parse time span
    if got_tspan
        tspan =_promote_tspan(kwargs[:tspan])

    elseif got_T
        tspan = _promote_tspan(kwargs[:T])

    elseif got_NSTEPS
        tspan = emptyinterval() # defined a posteriori

    elseif !isempty(args) && applicable(_promote_tspan, args[1])
        # applies to tuples, vectors, LazySets.Interval, IA.Interval, and numbers
        tspan = _promote_tspan(args[1])
#=
        (args[1] isa Tuple{Float64, Float64} || # time span given as tuple
                              args[1] isa Vector{Float64}         || # time span given as vector
                              args[1] isa Interval ||  # time span given as LazySets.Interval
                              args[1] isa IntervalArithmetic.Interval || # time span given as IA.Interval
                              args[1] isa Real) # got time horizon as first argument
=#

    elseif  haskey(kwargs, :max_jumps)
        # got maximum number of jumps, applicable to hybrid systems
        tspan = emptyinterval()

    else

        # couldn't find time span => error
        throw(ArgumentError("the time span has not been specified, but is required " *
                            "for `solve`; you should specify either the time horizon " *
                            "`T=...`, the time span `tspan=...`, or the number of steps, `NSTEPS=...`"))
    end
    return tspan
end

# return the time horizon given a time span
# the check_positive flag is used for algorithms that do not support negative
# times
function _get_T(tspan::TimeInterval; check_zero::Bool=true, check_positive::Bool=true)
    t0 = inf(tspan)
    if check_zero
        @assert iszero(t0) "this algorithm can only handle zero initial time"
    end
    T = sup(tspan)
    if check_positive
        @assert T > 0 "the time horizon should be positive"
    end
    return T
end

tstart(Δt::TimeInterval) = inf(Δt)
tend(Δt::TimeInterval) = sup(Δt)
tspan(Δt::TimeInterval) = Δt

function _get_cpost(ivp, args...; kwargs...)
    got_alg = haskey(kwargs, :alg)
    got_algorithm = haskey(kwargs, :algorithm)
    got_opC = haskey(kwargs, :opC)
    no_args = isempty(args) || args[1] === nothing

    # continous post was specified
    if got_alg
        cpost = kwargs[:alg]
    elseif got_algorithm
        cpost = kwargs[:algorithm]
    elseif got_opC
        cpost = kwargs[:opC]

    # check if either args[1] or args[2] are the post
    elseif !no_args && typeof(args[1]) <: AbstractContinuousPost
        cpost = args[1]
    elseif !no_args && length(args) == 2 && typeof(args[2]) <: AbstractContinuousPost
        cpost = args[2]
    elseif !no_args && length(args) > 2
        throw(ArgumentError("the number of arguments, $(length(args)), is not valid"))

    # no algorithm found => compute default
    else
        cpost = nothing
    end
    return cpost
end

# ===================
# Algorithm defaults
# ===================

# number of discrete steps used if only the time horizonis given
const DEFAULT_NSTEPS = 100

function _default_cpost(ivp::AbstractContinuousSystem, tspan; kwargs...)
    if isaffine(ivp)
        if haskey(kwargs, :δ)
            δ = kwargs[:δ]
        elseif haskey(kwargs, :N)
            N = kwargs[:N]
            δ = diam(tspan) / N
        elseif haskey(kwargs, :NSTEPS)
            NSTEPS = kwargs[:NSTEPS]
            δ = diam(tspan) / NSTEPS
        elseif haskey(kwargs, :num_steps)
            num_steps = kwargs[:num_steps]
            δ = diam(tspan) / num_steps
        else
            δ = diam(tspan) / DEFAULT_NSTEPS
        end
        if statedim(ivp) == 1 && !is_second_order(ivp)
            opC = INT(δ=δ)
        else
            static = haskey(kwargs, :static) ? kwargs[:static] : false
            opC = GLGM06(δ=δ, static=static)
        end
    else
        # check additional kwargs options if they exist, allowing some aliases
        orderQ = get(kwargs, :orderQ, DEFAULT_ORDER_Q_TMJETS)
        orderT = get(kwargs, :orderT, DEFAULT_ORDER_T_TMJETS)

        if haskey(kwargs, :max_steps)
            maxsteps = kwargs[:max_steps]
        elseif haskey(kwargs, :maxsteps)
            maxsteps = kwargs[:maxsteps]
        else
            maxsteps = DEFAULT_MAX_STEPS_TMJETS
        end

        if haskey(kwargs, :abs_tol)
            abstol = kwargs[:abs_tol]
        elseif haskey(kwargs, :abstol)
            abstol = kwargs[:abstol]
        else
            abstol = DEFAULT_ABS_TOL_TMJETS
        end

        opC = TMJets(orderQ=orderQ, orderT=orderT, maxsteps=maxsteps, abstol=abstol)
    end
    return opC
end

"""
    _default_cpost(ivp::IVP{<:AbstractContinuousSystem}, tspan; kwargs...)

### Input

- `ivp`   -- initial-value problem
- `tspan` -- time-span

### Output

A continuous post-operator that can handle the given initial-value problem.

### Notes

If the system is affine, then:

- If it is one-dimensional, algorithm `INT` is used, otherwise,
- Algorithm `GLGM06` is used.

If the system is not affine, then the algorithm `TMJets` is used.
"""
function _default_cpost(ivp::IVP{<:AbstractContinuousSystem}, tspan; kwargs...)
    _default_cpost(system(ivp), tspan; kwargs...)
end

# return a default algorithm, assuming that the first mode is representative
function _default_cpost(ivp::IVP{<:AbstractHybridSystem}, tspan; kwargs...)
    first_mode = mode(system(ivp), 1)
    _default_cpost(first_mode, tspan; kwargs...)
end


### Unary broadcasting

function broadcasted(::DefaultArrayStyle{N}, op, r::AbstractFill{T,N}) where {T,N}
    return Fill(op(getindex_value(r)), size(r))
end

### Binary broadcasting

function broadcasted(::DefaultArrayStyle, op, a::AbstractFill, b::AbstractFill)
    val = op(getindex_value(a), getindex_value(b))
    return Fill(val, broadcast_shape(size(a), size(b)))
end

function _broadcasted_zeros(a, b)
    return Zeros{promote_type(eltype(a), eltype(b))}(broadcast_shape(size(a), size(b)))
end
function _broadcasted_ones(a, b)
    return Ones{promote_type(eltype(a), eltype(b))}(broadcast_shape(size(a), size(b)))
end

broadcasted(::DefaultArrayStyle, ::typeof(+), a::Zeros, b::Zeros) = _broadcasted_zeros(a, b)
broadcasted(::DefaultArrayStyle, ::typeof(+), a::Ones, b::Zeros) = _broadcasted_ones(a, b)
broadcasted(::DefaultArrayStyle, ::typeof(+), a::Zeros, b::Ones) = _broadcasted_ones(a, b)

broadcasted(::DefaultArrayStyle, ::typeof(*), a::Zeros, b::Zeros) = _broadcasted_zeros(a, b)

for op in (:*, :/)
    @eval begin
        broadcasted(::DefaultArrayStyle, ::typeof($op), a::Zeros, b::Ones) = _broadcasted_zeros(a, b)
        broadcasted(::DefaultArrayStyle, ::typeof($op), a::Zeros, b::Fill) = _broadcasted_zeros(a, b)
        broadcasted(::DefaultArrayStyle, ::typeof($op), a::Zeros, b::Number) = _broadcasted_zeros(a, b)
        broadcasted(::DefaultArrayStyle, ::typeof($op), a::Zeros, b::AbstractRange) = _broadcasted_zeros(a, b)
        broadcasted(::DefaultArrayStyle, ::typeof($op), a::Zeros, b::AbstractArray) = _broadcasted_zeros(a, b)
        broadcasted(::DefaultArrayStyle{1}, ::typeof($op), a::Zeros, b::AbstractRange) = _broadcasted_zeros(a, b)
    end
end

for op in (:*, :\)
    @eval begin
        broadcasted(::DefaultArrayStyle, ::typeof($op), a::Ones, b::Zeros) = _broadcasted_zeros(a, b)
        broadcasted(::DefaultArrayStyle, ::typeof($op), a::Fill, b::Zeros) = _broadcasted_zeros(a, b)
        broadcasted(::DefaultArrayStyle, ::typeof($op), a::Number, b::Zeros) = _broadcasted_zeros(a, b)
        broadcasted(::DefaultArrayStyle, ::typeof($op), a::AbstractRange, b::Zeros) = _broadcasted_zeros(a, b)
        broadcasted(::DefaultArrayStyle, ::typeof($op), a::AbstractArray, b::Zeros) = _broadcasted_zeros(a, b)
        broadcasted(::DefaultArrayStyle{1}, ::typeof($op), a::AbstractRange, b::Zeros) = _broadcasted_zeros(a, b)
    end
end

broadcasted(::DefaultArrayStyle, ::typeof(*), a::Ones, b::Ones) = _broadcasted_ones(a, b)
broadcasted(::DefaultArrayStyle, ::typeof(/), a::Ones, b::Ones) = _broadcasted_ones(a, b)
broadcasted(::DefaultArrayStyle, ::typeof(\), a::Ones, b::Ones) = _broadcasted_ones(a, b)

# special case due to missing converts for ranges
_range_convert(::Type{AbstractVector{T}}, a::AbstractRange{T}) where T = a
_range_convert(::Type{AbstractVector{T}}, a::AbstractUnitRange) where T = convert(T,first(a)):convert(T,last(a))
_range_convert(::Type{AbstractVector{T}}, a::AbstractRange) where T = convert(T,first(a)):step(a):convert(T,last(a))

function broadcasted(::DefaultArrayStyle{1}, ::typeof(*), a::Ones{T}, b::AbstractRange{V}) where {T,V}
    broadcast_shape(size(a), size(b)) # Check sizes are compatible.
    return _range_convert(AbstractVector{promote_type(T,V)}, b)
end

function broadcasted(::DefaultArrayStyle{1}, ::typeof(*), a::AbstractRange{V}, b::Ones{T}) where {T,V}
    broadcast_shape(size(a), size(b)) # Check sizes are compatible.
    return _range_convert(AbstractVector{promote_type(T,V)}, a)
end

function broadcasted(::DefaultArrayStyle{1}, ::typeof(*), a::AbstractFill, b::AbstractRange)
    broadcast_shape(size(a), size(b)) # Check sizes are compatible.
    return broadcasted(*, getindex_value(a), b)
end

function broadcasted(::DefaultArrayStyle{1}, ::typeof(*), a::AbstractRange, b::AbstractFill)
    broadcast_shape(size(a), size(b)) # Check sizes are compatible.
    return broadcasted(*, a, getindex_value(b))
end

broadcasted(::DefaultArrayStyle{N}, op, r::AbstractFill{T,N}, x::Number) where {T,N} = Fill(op(getindex_value(r),x), size(r))
broadcasted(::DefaultArrayStyle{N}, op, x::Number, r::AbstractFill{T,N}) where {T,N} = Fill(op(x, getindex_value(r)), size(r))
broadcasted(::DefaultArrayStyle{N}, op, r::AbstractFill{T,N}, x::Ref) where {T,N} = Fill(op(getindex_value(r),x[]), size(r))
broadcasted(::DefaultArrayStyle{N}, op, x::Ref, r::AbstractFill{T,N}) where {T,N} = Fill(op(x[], getindex_value(r)), size(r))


module Parser
  export to_push

  # Produces a readily executable Push program from a given definition.
  to_push(s::String) = parse(lex(s))

  # Lex source code into a sequence of tokens.
  lex(s::String) = String[ss for ss in matchall(r"\(|\)|[^\(\)\s]+", s)]

  # Parse tokens into a Push program.
  function parse(tokens::Vector{String})
    prog = Any[]
    prog_stack = Vector{Any}[]

    while !isempty(tokens)
      t = shift!(tokens)
      if t == "("
        p2 = Any[]
        push!(prog, p2)
        push!(prog_stack, prog)
        prog = p2
      elseif t == ")"
        prog = pop!(prog_stack)
      elseif ismatch(r"^\-?\d+\.\d+$", t)
        push!(prog, convert(Float32, float(t)))
      elseif ismatch(r"^\-?\d+$", t)
        push!(prog, convert(Int32, int(t)))
      elseif t == "FALSE"
        push!(prog, false)
      elseif t == "TRUE"
        push!(prog, true)
      elseif ismatch(r"^\w\S*$", t)
        push!(prog, convert(Symbol, uppercase(t)))
      else
        error("PARSING ERROR - Unrecognised token: $(t).")
      end
    end

    if !isempty(prog_stack)
      error("PARSING ERROR - Unclosed parenthesis.")
    end
    if length(prog) != 1
      error("PARSING ERROR - Missing top-level parenthesis.")
    end

    return prog[1]
  end

end


include(joinpath("src", "axelrodnet.jl"))

for sc in 0:3:30
    Axelrodnet.run_experiment(
        experiment_name="grid_sc" * string(sc),
        agentcount=100,
        n_iter=5_000_000,
        nettopology="grid",
        networkprops=Dict("grid_height" => 10),
        stubborncount=sc,
        rndseed=1,
        n_replicates=10,
        export_every_n=1_000_000,
        exportdata=true,
        exportmode="default",
        keep_rawnet=false
    )
end

for sc in 0:3:30, beta in 0.05:0.05:0.2
    Axelrodnet.run_experiment(
        experiment_name="wattsstrogatz_sc" * string(sc) * "_beta" * lpad(trunc(Int, beta * 100), 2, "0"),
        agentcount=100,
        n_iter=5_000_000,
        nettopology="watts_strogatz",
        networkprops=Dict("k" => 10, "beta" => beta),
        stubborncount=sc,
        rndseed=1,
        n_replicates=10,
        export_every_n=1_000_000,
        exportdata=true,
        exportmode="default",
        keep_rawnet=false
    )
end

for sc in 0:3:30
    Axelrodnet.run_experiment(
        experiment_name="barabasialbert_sc" * string(sc),
        agentcount=100,
        n_iter=5_000_000,
        nettopology="barabasi_albert",
        networkprops=Dict("m0" => 10),
        stubborncount=sc,
        rndseed=1,
        n_replicates=10,
        export_every_n=1_000_000,
        exportdata=true,
        exportmode="default",
        keep_rawnet=false
    )
end

Axelrodnet.aggregate_all_experiments("experiments", write_data=true)


#=
read_json_url_julia:
- Julia version: 1.1.0 (2019-01-21)
- Author: arturogonzalez
- Date: 2019-04-22
=#
#=
using Pkg
Pkg.add("HTTP")
Pkg.add("JSON")
=#
using HTTP, JSON

function main()
    """
    :return: List of places and magnitudes, where magnitude is greater than 1.0.
    """
    resp = HTTP.get("https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.geojson")
    str = String(resp.body)
    data = JSON.Parser.parse(str)
    features_data = data["features"]
    # JSON.print(features_data, 2)
    for i in features_data
        place = i["properties"]["place"]
        magnitude = i["properties"]["mag"]
        if magnitude > 1.0
            println(place, " | ", magnitude)
            end
    end
end;
@time main()


module Tmp
using Random, BenchmarkTools, Test, InteractiveUtils

include("src/params.jl")
include("src/functions.jl")
import .Params
import .Func

function flipcoin(randval::Float64)
    if randval<0.5
        return "H"
    else
        return "T"
    end
end
@show code_typed(flipcoin(rand()))

end # module


# lgr_read.jl
#
#   Load Los Gatos Research (LGR) Methane data
#
# Jeremy Rüffer
# Thünen Institut
# Institut für Agrarklimaschutz
# Junior Research Group NITROSPHERE
# Julia 1.6.0
# 07.11.2014
# Last Edit: 28.04.2021

# - Programmatically zipped data files have a PGP signature at the end after the last line of data
# - Data files are TXT files withing a ZIP file
# - Sometimes multiple text files and a folder can be contained within a ZIP file
# - Line width is assumed to be fixed

"# lgr_read(source::String;verbose::Bool=false)

`time,data,columns = lgr_read(source)` Load a single LGR TXT files\n
* **source**::String = File name and location\n\n

---\n

#### Keywords:\n
* verbose::Bool = Display information as the function runs, TRUE is default\n\n"
function lgr_read(source::String;verbose::Bool=false)
	# Check for file
	!isfile(source) ? error(source * " must be a file") : nothing

	dfmt = Dates.DateFormat("dd/mm/yyyy HH:MM:SS.sss") # Date format

	#############################################
	##  Prepare Settings for Loading the Data  ##
	#############################################
	# Header Info
	fsize = stat(source).size
	fid = open(source,"r")
	readline(fid)
	cols = readline(fid,keep=true)[1:end-1]
	cols = permutedims(readdlm(IOBuffer("\"" * replace(replace(cols," " => ""),"," => "\",\"") * "\""),','),[2,1])
	startpos = position(fid)

	# Determine Line Count
	l = readline(fid)
	l_count = 1
	while !isempty(l) && !eof(fid)
		l = readline(fid)
		l_count += 1
	end
	footerlines = 1
	while !eof(fid)
		readline(fid)
		footerlines += 1
	end
	close(fid)

	# Column Types
	col_types = fill!(Array{DataType}(undef,length(cols)),Float64)
	col_types[length(col_types)] = String
	col_types[1] = DateTime

	# Column Names
	col_names = Array{String}(undef,length(cols))
	for i=1:length(cols)
		col_names[i] = replace(cols[i],"[" => "")
		col_names[i] = replace(col_names[i],"]" => "")
	end

	#####################
	##  Load the Data  ##
	#####################
	D = CSV.read(source,
		DataFrame,
		datarow=3,
		dateformat=dfmt,
		delim=',',
		header=col_names,
		#limit=l_count-1, # Read up until the PGP signature but not beyond it
		footerskip=footerlines, # PGP signed
		types=col_types)

	close(fid)
	if Sys.iswindows()
		# Garbage collection ensures the temporary file is closed so that it can be deleted.
		# Windows does not seem to close it in time whereas Linux does
		GC.gc()
	end
	return D
end


module MultiAgentPathFinding

# stdlib
using Random
using DataStructures

# others
using Parameters
using Graphs

# Abstract types
""" MAPFState

Abstract base type for tracking the state of an agent in a MAPF problem
"""
abstract type MAPFState end

""" MAPFAction

Abstract base type for the action that an agent can take in a MAPF problem.
"""
abstract type MAPFAction end

""" MAPFConflict

Abstract base type for a path-path conflict in a MAPF problem.
"""
abstract type MAPFConflict end

""" MAPFConstraints

Abstract base type for the constraints on the low-level path search induced by a conflict in a MAPF problem.
"""
abstract type MAPFConstraints end

""" MAPFEnvironment

Abstract base type for the environment in a MAPF problem.
"""
abstract type MAPFEnvironment end

# Types
export
    MAPFState,
    MAPFAction,
    MAPFConflict,
    MAPFConstraints,
    MAPFEnvironment

# Utils
export
    PlanResult,
    get_mapf_state_from_idx,
    get_mapf_action,
    get_plan_result_from_astar

# High Level Cost elements
export
    HighLevelCost,
    compute_cost,
    accumulate_cost,
    deaccumulate_cost,
    SumOfCosts,
    Makespan

# CBS elements
export
    get_empty_constraint,
    set_low_level_context!,
    get_first_conflict,
    create_constraints_from_conflict,
    overlap_between_constraints,
    add_constraint,
    low_level_search!,
    CBSSolver,
    search!,
    focal_heuristic,
    ECBSSolver

# Grid 2D Types
export
    Grid2DState,
    Grid2DAction,
    Grid2DConflict,
    Grid2DConstraints,
    Grid2DEnvironment,
    Grid2DLocation,
    AStarGrid2DEnvironment

include("utils.jl")
include("high_level_cost.jl")
include("cbs.jl")
include("ecbs.jl")
include("domains/grid2d/types.jl")
include("domains/grid2d/cbs_grid2d.jl")
include("domains/grid2d/ecbs_grid2d.jl")
include("domains/grid2d/a_star_epsilon_grid2d.jl")

end # module


export check_tri_index, car2lin_unchecked, car2lin, lin2car

# Convert between 1D and 2D indexing for triangular arrays

"""
    check_tri_index(Bool, layout::TriLayout, i, j)::Bool
    check_tri_index(Bool, layout::TriLayout, idx::CartesianIndex{2})::Bool

Check whether a row/column index is within the stored region of a `TriMatrix`
with the given layout. Assumes the index is otherwise valid for the
corresponding matrix size.
"""
function check_tri_index end

@inline check_tri_index(::Type{Bool}, ::TriLower{true}, i, j) = j <= i
@inline check_tri_index(::Type{Bool}, ::TriLower{false}, i, j) = j < i
@inline check_tri_index(::Type{Bool}, ::TriUpper{true}, i, j) = i <= j
@inline check_tri_index(::Type{Bool}, ::TriUpper{false}, i, j) = i < j
@inline check_tri_index(::Type{Bool}, ::TriSymmetric{true}, i, j) = true
@inline check_tri_index(::Type{Bool}, ::TriSymmetric{false}, i, j) = i != j
check_tri_index(::Type{Bool}, layout::TriLayout, idx::CartesianIndex{2}) = check_tri_index(Bool, layout, idx[1], idx[2])

"""
    check_tri_index(layout::TriLayout, i::Integer, j::Integer)
    check_tri_index(layout::TriLayout, idx::CartesianIndex{2})

Check that a row/column index is within the stored data region of a `TriMatrix`
with the given `layout` or throw a `DomainError`. Assumes the index is otherwise
valid for the corresponding matrix size.
"""
function check_tri_index(layout::TriLayout, i::Integer, j::Integer)
    check_tri_index(Bool, layout, i, j) || throw(DomainError("Invalid index for TriLayout $(layout): ($i, $j)"))
end

check_tri_index(layout::TriLayout, idx::CartesianIndex{2}) = check_tri_index(layout, idx[1], idx[2])

# Convert cartesian index of given layout to cartesian index of TriLower{true}
# which has the same index in the data array.
# Assumes index is within the stored data region.
@inline cartesian_to_tril(::TriLower{true}, i, j) = (i, j)
@inline cartesian_to_tril(::TriLower{false}, i, j) = (i - 1, j)
@inline cartesian_to_tril(::TriUpper{true}, i, j) = (j, i)
@inline cartesian_to_tril(::TriUpper{false}, i, j) = (j - 1, i)
@inline function cartesian_to_tril(::TriSymmetric{D}, i, j) where D
	if i >= j
		return cartesian_to_tril(TriLower{D}(), i, j)
	else
		return cartesian_to_tril(TriUpper{D}(), i, j)
	end
end

# Inverse of cartesian_to_tril()
@inline cartesian_from_tril(::TriLower{true}, i, j) = (i, j)
@inline cartesian_from_tril(::TriLower{false}, i, j) = (i + 1, j)
@inline cartesian_from_tril(::TriUpper{true}, i, j) = (j, i)
@inline cartesian_from_tril(::TriUpper{false}, i, j) = (j, i + 1)
@inline cartesian_from_tril(::TriSymmetric{D}, i, j) where D = cartesian_from_tril(TriLower{D}(), i, j)

"""
	$(FUNCTIONNAME)(layout::TriLayout, i::Integer, j::Integer)
	$(FUNCTIONNAME)(layout::TriLayout, idx::CartesianIndex{2})

Like [`car2lin`](@ref), but somewhat faster because it does not check that the
index is within the data region of the matrix.
"""
@inline car2lin_unchecked(::TriLower{true}, i::Integer, j::Integer) = trinum(i - 1) + j
@inline car2lin_unchecked(layout::TriLayout, i::Integer, j::Integer) = car2lin_unchecked(TriLower(), cartesian_to_tril(layout, i, j)...)
# @inline car2lin_unchecked(::TriLower{true}, i, j) = car2lin_unchecked(i, j)
# @inline car2lin_unchecked(::TriLower{false}, i, j) = car2lin_unchecked(i - 1, j)
# @inline car2lin_unchecked(::TriUpper{D}, i, j) where D = car2lin_unchecked(TriLower{D}, j, i)
# function @inline car2lin_unchecked(::TriSymmetric{D}, i, j) where D
#     if i >= j
#         return car2lin_unchecked(TriLower{D}(), i, j)
#     else
#         return car2lin_unchecked(TriUpper{D}(), j, i)
#     end
# end
@inline car2lin_unchecked(layout::TriLayout, idx::CartesianIndex{2}) = car2lin_unchecked(layout, idx[1], idx[2])

"""
	$(FUNCTIONNAME)(layout::TriLayout, i::Integer, j::Integer)
	$(FUNCTIONNAME)(layout::TriLayout, idx::CartesianIndex{2})

Convert Cartesian row/column indices of a `TriMatrix` with the given layout to
a linear index of its data array.

This function is the inverse of [`lin2car`](@ref).
"""
function car2lin(layout::TriLayout, i::Integer, j::Integer)
	check_tri_index(layout, i, j)
	return car2lin_unchecked(layout, i, j)
end

car2lin(layout::TriLayout, idx::CartesianIndex{2}) = car2lin(layout, idx[1], idx[2])

"""
    $(FUNCTIONNAME)(layout::TriLayout, i::Integer)

Convert linear index of the data array of a `TriMatrix` with the given layout to
the corresponding Cartesian index.

This function is the inverse of [`car2lin`](@ref).
"""
function lin2car(layout::TriLayout, i::Integer)
	r, c = lin2car(i)
	return cartesian_from_tril(layout, r, c)
end

function lin2car(i::Integer)
	n, i = triinv_rem(i - 1)
	return (n + 1, i + 1)
end


function LongitudeExtremumDetector(arg0::OneAxisEllipsoid)
    return LongitudeExtremumDetector((OneAxisEllipsoid,), arg0)
end

function LongitudeExtremumDetector(arg0::jdouble, arg1::jdouble, arg2::OneAxisEllipsoid)
    return LongitudeExtremumDetector((jdouble, jdouble, OneAxisEllipsoid), arg0, arg1, arg2)
end

function g(obj::LongitudeExtremumDetector, arg0::SpacecraftState)
    return jcall(obj, "g", jdouble, (SpacecraftState,), arg0)
end

function get_body(obj::LongitudeExtremumDetector)
    return jcall(obj, "getBody", BodyShape, ())
end