#=##############################################################################
# DESCRIPTION
    Vehicles based on FLOWVLM (VLM and blade-element) models.

# AUTHORSHIP
  * Author    : Eduardo J. Alvarez
  * Email     : Edo.AlvarezR@gmail.com
  * Created   : Apr 2020
  * License   : MIT
=###############################################################################

################################################################################
# ABSTRACT VLM VEHICLE TYPE
################################################################################
"""
    `AbstractVLMVehicle{N, M, R} <: AbstractVehicle{N, M, R}`

Implementations of `AbstractUVLMVehicle` are expected to have the following
fields:

# Required inputs
* `system::vlm.WingSystem`

# Optional inputs
* `tilting_systems::NTuple{N, vlm.WingSystem}`
* `rotor_systems::NTuple{M, Array{vlm.Rotor, 1}}`
* `vlm_system::vlm.WingSystem`
* `wake_system::vlm.WingSystem`
* `grids::Array{gt.GridTypes, 1}`

# Internal properties
* `V::Array{R, 1}`                          # Current vehicle velocity
* `W::Array{R, 1}`                          # Current vehicle angular velocity
* `prev_data::Array{Any, 1}`                # Information about previous step
* `grid_O::Array{Array{R, 1}, 1}`           # Origin of every grid

They also need to implement the following functions required by
`AbstractVehicle`:

* `shed_wake(...)`
* `generate_static_particle_fun(...)`
* `save_vtk(...)`

See the documentation and code of `AbstractVehicle` for more details.

In general, in order for implementation to work correctly, it is required that
the components of `wake_system` are also components of either `vlm_system` or
`rotor_systems`, and that none of the components of `rotor_systems` are also
components of `vlm_system`.
"""
abstract type AbstractVLMVehicle{N, M, R} <: AbstractVehicle{N, M, R} end

# Implementations
for header_name in ["unsteady", "quasisteady"]
  include("FLOWUnsteady_vehicle_vlm_"*header_name*".jl")
end

##### FUNCTIONS  ###############################################################
get_ntltsys(self::AbstractVLMVehicle) = typeof(self).parameters[1]

get_nrtrsys(self::AbstractVLMVehicle) = typeof(self).parameters[2]

function add_dV(self::AbstractVLMVehicle, dV)
    self.V .+= dV
    return nothing
end

function add_dW(self::AbstractVLMVehicle, dW)
    self.W .+= dW
    return nothing
end

function set_V(self::AbstractVLMVehicle, V)
    self.V .= V
    return nothing
end

function set_W(self::AbstractVLMVehicle, W)
    self.W .= W
    return nothing
end

function tilt_systems(self::AbstractVLMVehicle{N,M,R}, angles::NTuple{N, Array{R2, 1}}
                                                    ) where{N, M, R, R2<:Real}
    # Iterate over tilting system
    for i in 1:get_ntltsys(self)
        sys = self.tilting_systems[i]
        Oaxis = gt.rotation_matrix2([-a for a in angles[i]]...)
        vlm.setcoordsystem(sys, sys.O, Oaxis)
    end
    return nothing
end
function tilt_systems(self::AbstractVLMVehicle{0,M,R}, angles::Tuple{})  where{M, R}
    return nothing
end

function nextstep_kinematic(self::AbstractVLMVehicle, dt::Real)
    dX = dt*self.V                  # Translation
    dA = 180/pi * dt*self.W         # Angular rotation (degrees)

    O = self.system.O + dX          # New origin of the system (translation)
    M = gt.rotation_matrix2([-a for a in dA]...) # Rotation matrix
    Oaxis = M*self.system.Oaxis     # New axes of the system (rotation)

    # Save state of current vehicle
    _update_prev_vlm_system(self, deepcopy(self.vlm_system))
    _update_prev_wake_system(self, deepcopy(self.wake_system))
    _update_prev_rotor_systems(self, deepcopy(self.rotor_systems))

    # Translation and rotation
    vlm.setcoordsystem(self.system, O, Oaxis)

    # Translation and rotation of every grid
    for i in 1:length(self.grids)
        self.grid_O[i] .+= dX

        # Translation
        gt.lintransform!(self.grids[i], Array(1.0I, 3, 3) , dX)

        # Brings the grid back to the global origin
        gt.lintransform!(self.grids[i], Array(1.0I, 3, 3) , -self.grid_O[i])

        # Rotation and brings the grid back to its position
        gt.lintransform!(self.grids[i], M, self.grid_O[i])
    end

    return nothing
end

"""
Precalculations before calling the solver.

Calculates kinematic velocity on VLM an adds them as a solution field
"""
function precalculations(self::AbstractVLMVehicle, Vinf::Function,
                                pfield::vpm.ParticleField, t::Real, dt::Real,
                                nt::Int)

    if nt!=0
        # ---------- 1) Recalculate wake horseshoes with kinematic velocity -
        # Calculate kinematic velocity at every control point
        Vkin = _Vkinematic_wake(self, dt; t=t, targetX="CP")
        vlm._addsolution(self.wake_system, "Vkin", Vkin; t=t)

        Vkin = _Vkinematic_vlm(self, dt; t=t, targetX="CP")
        vlm._addsolution(self.vlm_system, "Vkin", Vkin; t=t)

        # Recalculate horseshoes
        vlm.getHorseshoes(self.vlm_system; t=t, extraVinf=_extraVinf1)
        vlm.getHorseshoes(self.wake_system; t=t, extraVinf=_extraVinf1)

        # ---------- 2) Paste previous Gamma solution ----------------------
        for i in 1:length(self.wake_system.wings)
            wing = vlm.get_wing(self.wake_system, i)
            prev_wing = vlm.get_wing(_get_prev_wake_system(self), i)

            if typeof(prev_wing) != vlm.Rotor
                sol = deepcopy(prev_wing.sol["Gamma"])
            else
                sol = deepcopy(prev_wing._wingsystem.sol["Gamma"])
            end

            vlm._addsolution(wing, "Gamma", sol; t=t)
        end
        for i in 1:length(self.vlm_system.wings)
            wing = vlm.get_wing(self.vlm_system, i)
            prev_wing = vlm.get_wing(_get_prev_vlm_system(self), i)

            if typeof(prev_wing) != vlm.Rotor
                sol = deepcopy(prev_wing.sol["Gamma"])
            else
                sol = deepcopy(prev_wing._wingsystem.sol["Gamma"])
            end

            vlm._addsolution(wing, "Gamma", sol; t=t)
        end
    end

    return nothing
end

function save_vtk_base(self::AbstractVLMVehicle, filename; path=nothing,
                                   num=nothing, save_wopwopin=false, optargs...)
    strn = vlm.save(self.system, filename; path=path, num=num, optargs...)

    for (i, grid) in enumerate(self.grids)
        strn *= gt.save(grid, filename*"_Grid$i"; path=path, num=num)
    end

    # Generate inputs for PSU-WOPWOP
    if save_wopwopin
        for (si, rotors) in enumerate(self.rotor_systems)
            # Compact patch for loading
            generate_vtkliftinglines(rotors, filename*"_Sys$(si)", path; num=num,
                                                                suf="_compact")
            # Loft for thickness
            # NOTE: this is not needed since FLOWNoise can read the lofted VTK
            # straight up
            # for (ri, rotors) enumerate(rotors)
            #     vlm.save(rotor, filename*"_Sys$(si)_Rotor$(ri)"; path=path, addtiproot=true,
            #                             wopwop=true, wopbin=true, wopv=1.0, num=num)
            # end

            # Loading file
            save_gammas = [[vlm.get_blade(rotor, i).sol["Gamma"]
                                      for i in 1:rotor.B] for rotor in rotors]
            # NOTE TO SELF: Ftot is a force per unit length
            save_Ftot = [[vlm.get_blade(rotor, i).sol["Ftot"]
                                      for i in 1:rotor.B] for rotor in rotors]
            loadfname = "loading_Sys$(si)"*(num!=nothing ? ".$num" : "")*".jld"
            JLD.save(joinpath(path, loadfname), "rotorgammas", save_gammas,
                                                        "Ftot", save_Ftot)
        end
    end

    return strn
end

##### INTERNAL FUNCTIONS  ######################################################

_get_prev_vlm_system(self::AbstractVLMVehicle) = self.prev_data[1]
_get_prev_wake_system(self::AbstractVLMVehicle) = self.prev_data[2]
_get_prev_rotor_systems(self::AbstractVLMVehicle) = self.prev_data[3]
function _update_prev_vlm_system(self::AbstractVLMVehicle, system)
    self.prev_data[1] = system
end
function _update_prev_wake_system(self::AbstractVLMVehicle, system)
    self.prev_data[2] = system
end
function _update_prev_rotor_systems(self::AbstractVLMVehicle, rotor_systems)
    self.prev_data[3] = rotor_systems
end

"""
Return the maximum number of static particles.
"""
function _get_m_static(self::AbstractVLMVehicle)
    m = vlm.get_m(self.vlm_system)

    for rotors in self.rotor_systems
        for rotor in rotors
            m += vlm.get_m(rotor)
        end
    end

    return m
end

"""
Returns the local translational velocity of every control point in `vlm_system`.
"""
function _Vkinematic_vlm(self::AbstractVLMVehicle, args...; optargs...)
    return _Vkinematic(self.vlm_system, _get_prev_vlm_system(self), args...;
                                                                    optargs...)
end

"""
Returns the local translational velocity of every control point in `wake_system`.
"""
function _Vkinematic_wake(self::AbstractVLMVehicle, args...; optargs...)
    return _Vkinematic(self.wake_system, _get_prev_wake_system(self), args...;
                                                                    optargs...)
end

function _Vkinematic(system, prev_system, dt::Real; t=0.0, targetX="CP")

    cur_Xs = _get_Xs(system, targetX; t=t)
    prev_Xs = _get_Xs(prev_system, targetX; t=t)

    return [-(cur_Xs[i]-prev_Xs[i])/dt for i in 1:size(cur_Xs, 1)]
end

"""
Returns the local translational velocity of every midpoint in the ri-th rotor in
the si-th system formatted as the output of `_get_midXs()`.
"""
function _Vkinematic_rotor(rotor_systems::NTuple{M, Array{vlm.Rotor, 1}},
                           prev_rotor_systems::NTuple{M, Array{vlm.Rotor, 1}},
                           si, ri, dt::Real
                          ) where{M}

    cur_Xs = _get_midXs(rotor_systems[si][ri])
    prev_Xs = _get_midXs(prev_rotor_systems[si][ri])

    Vkin = [-(cur_Xs[i]-prev_Xs[i])/dt for i in 1:size(cur_Xs, 1)]

    return Vkin

end

function _get_Xs(system::Union{vlm.Wing, vlm.WingSystem, vlm.Rotor},
                                                        targetX::String; t=0.0)
    if targetX=="CP"
        Xs = [vlm.getControlPoint(system, i
                                        ) for i in 1:vlm.get_m(system)]
    else
        Xs = [vlm.getHorseshoe(system, i; t=t)[vlm.VLMSolver.HS_hash[targetX]
                                        ] for i in 1:vlm.get_m(system)]
    end
    return Xs
end

function _get_Xs(system::Union{vlm.Wing, vlm.WingSystem, vlm.Rotor},
                                        targetXs::Array{T,1}; t=0.0) where{T}
    return vcat([_get_Xs(system, targetX; t=t) for targetX in targetXs]...)
end

"Returns the i-th bound-vortex midpoint of the j-th blade in `rotor`"
function _get_midX(rotor::vlm.Rotor, j::Int, i::Int)
    return (vlm.getHorseshoe(vlm.get_blade(rotor, j), i)[2] +
            vlm.getHorseshoe(vlm.get_blade(rotor, j), i)[3]) / 2
end

"Returns all bound-vortex midpoints of the j-th blade in `rotor`"
function _get_midXs(rotor::vlm.Rotor, j::Int)
    return [_get_midX(rotor, j, i)
                            for i in 1:vlm.get_m(vlm.get_blade(rotor, j)) ]
end
"Returns all bound-vortex midpoints in `rotor`"
_get_midXs(rotor::vlm.Rotor) = vcat([_get_midXs(rotor, j) for j in 1:rotor.B]...)

"Returns all bound-vortex midpoints in an array of rotors"
_get_midXs(rotors::Array{vlm.Rotor, 1}) = vcat([_get_midXs(rotor) for rotor in rotors]...)

"Returns all bound-vortex midpoints in multiple rotor systems"
function _get_midXs(rotor_systems::NTuple{M, Array{vlm.Rotor, 1}}) where{M}
    return vcat([_get_midXs(rotors) for rotors in rotor_systems]...)
end

"Receives an array `midXs` formatted as the output of `_get_midXs(rotor_systems)`
and returns the section of the array that corresponds to the ri-th rotor in the
si-th system (formatted linearly).
"
function _parse_midXs(rotor_systems::NTuple{M, Array{vlm.Rotor, 1}},
                        midXs::Array{Array{R, 1}}, si, ri) where{R<:Real, M}

    if si>length(rotor_systems) || si<=0
        error("Got invalid system index $si; max is $(length(rotor_systems))")
    end

    # Find lower bound of midXs belonging to rotor ri
    i_low = 1
    for (sj, rotors) in enumerate(rotor_systems[1:si])       # Iterate over rotor systems
        for (rj, rotor) in enumerate(rotors)                 # Iterative over rotors
            if sj==si && rj==ri
                break
            end
            i_low += vlm.get_m(rotor)
        end
    end

    # Find upper bound of midXs belonging to rotor ri
    rotor = rotor_systems[si][ri]
    i_up = i_low-1 + vlm.get_m(rotor)

    # # Format it as blades
    out = midXs[i_low:i_up]
    # out = [out[ ((i-1)*rotor.m+1):(i*rotor.m) ] for i in 1:rotor.B]
    return out
end

"""
    Receives the output of `parse_midXs(rotor_systems, midXs, si, ri)` and
outputs the array formatted as blades.
"""
function _format_blades(arr::Array{Array{R, 1}, 1},
                        rotor_systems::NTuple{M, Array{vlm.Rotor, 1}},
                        si, ri) where{R, M}

    rotor = rotor_systems[si][ri]
    arr = [arr[ ((i-1)*rotor.m+1):(i*rotor.m) ] for i in 1:rotor.B]

    return arr
end

"""
Function for FLOWVLM to fetch and add extra velocities to what every
control point imposes as the boundary condition
"""
function _extraVinf1(i, t; wing=nothing)
    if wing==nothing; error("Logic error!"); end;
    return wing.sol["Vkin"][i]
end
function _extraVinf2(i, t; wing=nothing)
    if wing==nothing; error("Logic error!"); end;
    return wing.sol["Vvpm"][i] + wing.sol["Vkin"][i]
end
function _extraVinf3(i, t; wing=nothing)
    if wing==nothing; error("Logic error!"); end;
    return wing.sol["Vind"][i] + wing.sol["Vkin"][i]
end

"""
Receives the FLOWVLM object `system` (Wing/WingSystem/Rotor), and adds vortex
particles to the particle field `pfield` at each trailing edge position where
an infinite vortex starts. `dt` indicates the length of the interval of time
that the vortex shedding represents.

Give it a previous system to detect differences in circulation and add
unsteady particles.
"""
function VLM2VPM(system::Union{vlm.Wing, vlm.WingSystem, vlm.Rotor}, pfield, dt,
                    Vinf;
                    t=0.0, prev_system=nothing, unsteady_shedcrit=-1.0,
                    p_per_step=1, sigmafactor=1.0, overwrite_sigma=nothing,
                    check=true, debug=false, tol=1e-6, omit_shedding=[])

  m = vlm.get_m(system)   # Number of lattices

  # Velocity at horseshoe points Ap and Bp
  if prev_system==nothing
      Vinfs_Ap = vlm.getVinfs(system; t=t, target="Ap")
      Vinfs_Bp = vlm.getVinfs(system; t=t, target="Bp")
  else
      Vinfs_Ap = []
      Vinfs_Bp = []
      for i in 1:m
          Ap = vlm.getHorseshoe(system, i)[1]
          prev_Ap = vlm.getHorseshoe(prev_system, i)[1]
          push!(Vinfs_Ap, -(Ap-prev_Ap)/dt + Vinf(Ap, t))

          Bp = vlm.getHorseshoe(system, i)[4]
          prev_Bp = vlm.getHorseshoe(prev_system, i)[4]
          push!(Vinfs_Bp, -(Bp-prev_Bp)/dt + Vinf(Bp, t))
      end
  end

  # Adds a particle at each infinite vortex
  prev_HS = [nothing for i in 1:8]
  for i in 1:m  # Iterates over lattices
    HS = vlm.getHorseshoe(system, i)

    Ap, A, B, Bp, CP, infDA, infDB, Gamma = HS
    (prev_Ap, prev_A, prev_B, prev_Bp, prev_CP,
      prev_infDA, prev_infDB, prev_Gamma) = prev_HS
    cntgs = true          # Contiguous horseshoes flag

    if nothing in HS; error("Logic error! $HS"); end;
    if true in [isnan.(elem) for elem in HS]; error("Logic error! $HS"); end;

    # ----------- Case of left wing tip ---------------------------
    if i==1
      # Adds particle at Ap
      X = Ap                                    # Particle position
      gamma = Gamma                             # Infinite vortex circulation
      V = norm(Vinfs_Ap[i])                     # Freestream at X
      infD = -infDA                             # Direction of vorticity
      sigma = sigmafactor*V*dt                  # Vortex blob radius
      vol = pi*(norm(Bp-Ap)/2)^2*V*dt           # Volume of particle
      l = -infD*V*dt                             # Distance the TE travels

      # Check if the system loops around
      if norm(Ap - vlm.getHorseshoe(system, m)[4]) / norm(Bp - Ap) <= tol
          gamma -= vlm.getHorseshoe(system, m)[8]
      end

      if unsteady_shedcrit<=0
          if !(i in omit_shedding)
              add_particle(pfield, X, gamma, dt, V, infD, sigma, vol,
                                l, p_per_step; overwrite_sigma=overwrite_sigma)
          end
      end

    # ----------- Case of wing tip on discontinuous wing --------
    elseif norm(Ap - prev_Bp) / norm(Bp - Ap) > tol
      if debug; println("Wing tip found at i=$i. $Ap!=$prev_Bp"); end;

      cntgs = false

      # Adds particle at previous Bp
      X = prev_Bp                               # Particle position
      gamma = -prev_Gamma                       # Infinite vortex circulation
      V = norm(Vinfs_Bp[i-1])                   # Freestream at X
      infD = -prev_infDB                        # Direction of vorticity
      sigma = sigmafactor*V*dt                  # Vortex blob radius
      vol = pi*(norm(prev_Bp-prev_Ap)/2)^2*V*dt # Volume of particle
      l = -infD*V*dt                             # Distance the TE travels

      if unsteady_shedcrit<=0
          if !(i-1 in omit_shedding)
              add_particle(pfield, X, gamma, dt, V, infD, sigma, vol,
                              l, p_per_step; overwrite_sigma=overwrite_sigma)
          end
      end

      # Adds particle at Ap
      X = Ap                                    # Particle position
      gamma = Gamma                             # Infinite vortex circulation
      V = norm(Vinfs_Ap[i])                     # Freestream at X
      infD = -infDA                             # Direction of vorticity
      sigma = sigmafactor*V*dt                  # Vortex blob radius
      vol = pi*(norm(Bp-Ap)/2)^2*V*dt           # Volume of particle
      l = -infD*V*dt                             # Distance the TE travels

      if unsteady_shedcrit<=0
          if !(i in omit_shedding)
              add_particle(pfield, X, gamma, dt, V, infD, sigma, vol,
                                l, p_per_step; overwrite_sigma=overwrite_sigma)
          end
      end

    # ----------- Case of contiguous horseshoes -----------------
    else

      # Verify logic
      if check
        crit1 = norm(Vinfs_Ap[i]-Vinfs_Bp[i-1]) / norm((Vinfs_Ap[i]+Vinfs_Bp[i-1])/2)
        crit2 = norm(infDA-prev_infDB) / norm((infDA+prev_infDB)/2)
        if crit1 > tol
          error("Logic error! Vinfs_Ap[i]!=Vinfs_Bp[i-1] "*
                  "( $(Vinfs_Ap[i])!=$(Vinfs_Bp[i-1]) )")
        elseif crit2 > tol
          error("Logic error! infDA!=prev_infDB "*
                  "( $(infDA)!=$(prev_infDB) )")
        elseif norm(infDA) - 1 > tol
          error("Logic error! norm(infDA)!= 1 "*
                  "( $(norm(infDA)) )")
        end
      end

      # Adds particle at Ap
      X = Ap                                    # Particle position
      gamma = Gamma - prev_Gamma                # Infinite vortex circulation
      V = norm(Vinfs_Ap[i])                     # Freestream at X
      infD = -infDA                             # Direction of vorticity
      sigma = sigmafactor*V*dt                  # Vortex blob radius
      vol = pi*(norm(Bp-Ap)/2)^2*V*dt           # Volume of particle
      l = -infD*V*dt                             # Distance the TE travels

      if unsteady_shedcrit<=0
          if !(i in omit_shedding)
              add_particle(pfield, X, gamma, dt, V, infD, sigma, vol,
                              l, p_per_step; overwrite_sigma=overwrite_sigma)
          end
      end

      # ----------- Case of discontinuous right wing tip -----------------------
      if i==m && norm(Bp - vlm.getHorseshoe(system, 1)[1]) / norm(Bp - Ap) > tol
        # Adds particle at Bp
        X = Bp                                    # Particle position
        gamma = -Gamma                            # Infinite vortex circulation
        V = norm(Vinfs_Bp[i])                     # Freestream at X
        infD = -infDB                             # Direction of vorticity
        sigma = sigmafactor*V*dt                  # Vortex blob radius
        vol = pi*(norm(Bp-Ap)/2)^2*V*dt           # Volume of particle
        l = -infD*V*dt                             # Distance the TE travels

        if unsteady_shedcrit<=0
            if !(i in omit_shedding)
                add_particle(pfield, X, gamma, dt, V, infD, sigma, vol,
                            l, p_per_step; overwrite_sigma=overwrite_sigma)
            end
        end
      end
    end

    # Adds unsteady particle
    if prev_system!=nothing && unsteady_shedcrit>0

      (p_Ap, p_A, p_B, p_Bp, p_CP,
          p_infDA, p_infDB, p_Gamma) = vlm.getHorseshoe(prev_system, i)
      X = (Ap+Bp)/2                               # LE midpoint position
      p_X = (p_Ap+p_Bp)/2                         # Previous LE midpoint position
      gamma = Gamma - p_Gamma                     # Bound circulation increase
      infD = (!cntgs || i==1 ? Ap : (prev_Ap+prev_Bp)/2) - X # Direction of circulation
      sigma = sigmafactor*norm(B-A)               # Vortex blob radius
      vol = 4/3*pi*(sigma/2)^3                    # Volume of particle
      l = -(X-p_X) + Vinf(X, t)*dt                # Distance the TE travels

      # Adds particle only if difference is greater than 1%
      if abs(gamma/p_Gamma) > unsteady_shedcrit && !(i in omit_shedding)
        add_particle(pfield, X, gamma, 1.0, 1.0, infD, sigma, vol,
                                        l, 1; overwrite_sigma=overwrite_sigma)
      end
    end

    prev_HS = HS
  end
end
##### END OF ABSTRACT VLM VEHICLE ##############################################


### Processing GEOJSON file: INPUT is path_to_json_data and OUTPUT is distance_matrix_km, number_zones

function processgeodata(path_to_json_data, path_to_data, csv_dataset_list, path_to_results)

    open(path_to_json_data, "r") do json_file
        global julia_file
        julia_file = JSON.parse(json_file);
    end

    a = julia_file["features"]
    first_zone_id = parse(Int,a[1]["properties"]["MOVEMENT_ID"]);
    if (first_zone_id == 0) # catches the case that the first zone ID is a zero
        number_zones = parse(Int,a[length(a)]["properties"]["MOVEMENT_ID"]) + 1;
    else
        number_zones =  parse(Int,a[length(a)]["properties"]["MOVEMENT_ID"]);
    end
    longitude_matrix = zeros(number_zones, 100000);
    latitude_matrix = zeros(number_zones, 100000);

    for i=1:length(a)
        entry = a[i];
        id = parse(Int,entry["properties"]["MOVEMENT_ID"]);
        if (id == 0) # catches the case that the first zone ID is a zero
            id = number_zones;
        end
        coordinates = entry["geometry"]["coordinates"];
        counter = 0;

        if(typeof(coordinates) == Float64)
            counter = counter + 1;
            longitude_matrix[id, counter] = coordinates[1];
            latitude_matrix[id, counter] = coordinates[2];
        else
            for j=1:length(coordinates)
                nested_coordinates = coordinates[j]
                if(typeof(nested_coordinates) == Float64)
                    counter = counter + 1;
                    longitude_matrix[id, counter] = coordinates[1];
                    latitude_matrix[id, counter] = coordinates[2];
                else
                    for k=1:length(nested_coordinates)
                        nested_coordinates = coordinates[j][k];
                        if(typeof(nested_coordinates) == Float64)
                            counter = counter + 1;
                            longitude_matrix[id, counter] = coordinates[j][1];
                            latitude_matrix[id, counter] = coordinates[j][2];
                        else
                            for l=1:length(nested_coordinates)
                                nested_coordinates = coordinates[j][k][l];
                                if(typeof(nested_coordinates) == Float64)
                                    counter = counter + 1;
                                    longitude_matrix[id, counter] = coordinates[j][k][1];
                                    latitude_matrix[id, counter] = coordinates[j][k][2];
                                else
                                    for m=1:length(nested_coordinates)
                                        nested_coordinates = coordinates[j][k][l][m];
                                        if(typeof(nested_coordinates) == Float64)
                                            counter = counter + 1;
                                            longitude_matrix[id, counter] = coordinates[j][k][l][1];
                                            latitude_matrix[id, counter] = coordinates[j][k][l][2];
                                        else
                                            for n=1:length(nested_coordinates)
                                                nested_coordinates = coordinates[j][k][l][m][n];
                                                if(typeof(nested_coordinates) == Float64)
                                                    counter = counter + 1;
                                                    longitude_matrix[id, counter] = coordinates[j][k][l][m][1];
                                                    latitude_matrix[id, counter] = coordinates[j][k][l][m][2];
                                                else
                                                    for o=1:length(nested_coordinates)
                                                        nested_coordinates = coordinates[j][k][l][m][n][o];
                                                        if(typeof(coordinates) == Float64)
                                                            counter = counter + 1;
                                                            longitude_matrix[id, counter] = coordinates[j][k][l][m][n][1];
                                                            latitude_matrix[id, counter] = coordinates[j][k][l][m][n][2];
                                                        else
                                                            println("Double-check how often this json file is nested at:")
                                                            println("i = ", i)
                                                            println("j = ", j)
                                                            println("k = ", k)
                                                            println("l = ", l)
                                                            println("m = ", m)
                                                            println("n = ", n)
                                                            println("o = ", o)
                                                        end
                                                    end
                                                end
                                            end
                                        end
                                    end
                                end
                            end
                        end
                    end
                end
            end
        end
    end

    centroid_lat = zeros(number_zones,1);
    centroid_long = zeros(number_zones,1);
    area_polygon = zeros(number_zones,1);

    ### Calculating the area of each polygon
    for i=1:number_zones
        if(longitude_matrix[i,1] != 0)
            summation_term = 0;
            for j=1:10000
                if(longitude_matrix[i,j] == 0)
                    ### Correction of summation term
                    summation_term = summation_term - (latitude_matrix[i,j-1] * longitude_matrix[i,j] - latitude_matrix[i,j] * longitude_matrix[i,j-1])

                    ### Addition of first vertex as last one
                    longitude_matrix[i,j] = longitude_matrix[i,1];
                    latitude_matrix[i,j] = latitude_matrix[i,1];

                    ### Recalculation of last summation term
                    summation_term = summation_term + (latitude_matrix[i,j-1] * longitude_matrix[i,j] - latitude_matrix[i,j] * longitude_matrix[i,j-1])
                    break
                else
                    summation_term = summation_term + (latitude_matrix[i,j] * longitude_matrix[i,j+1] - latitude_matrix[i,j+1] * longitude_matrix[i,j])
                end
            end
            area_polygon[i] = summation_term/2;
        end
    end

    ### Calculating the polygon centroid coordinates
    for i=1:number_zones
        if(longitude_matrix[i,1] != 0)
            summation_long = 0;
            summation_lat = 0;
            for j=1:10000
                if(longitude_matrix[i,j] == 0)
                    ### Correction of summation term
                    summation_long = summation_long - (longitude_matrix[i,j-1] + longitude_matrix[i,j]) * (longitude_matrix[i,j-1] * latitude_matrix[i,j] - longitude_matrix[i,j] * latitude_matrix[i,j-1]);
                    summation_lat = summation_lat - (latitude_matrix[i,j-1] + latitude_matrix[i,j]) * (longitude_matrix[i,j-1] * latitude_matrix[i,j] - longitude_matrix[i,j] * latitude_matrix[i,j-1]);
                else
                    summation_long = summation_long + (longitude_matrix[i,j] + longitude_matrix[i,j+1]) * (longitude_matrix[i,j] * latitude_matrix[i,j+1] - longitude_matrix[i,j+1] * latitude_matrix[i,j]);
                    summation_lat = summation_lat + (latitude_matrix[i,j] + latitude_matrix[i,j+1]) * (longitude_matrix[i,j] * latitude_matrix[i,j+1] - longitude_matrix[i,j+1] * latitude_matrix[i,j]);
                end
            end
            centroid_long[i] = -summation_long/(6 * area_polygon[i]);
            centroid_lat[i] = -summation_lat/(6 * area_polygon[i]);
        end
    end

    ## calculating geographic distances between zones
    c_lat_long = 111.3; # this is a natural constant
    conv_deg_rad = 0.01745; # this is a constant for the conversion from degrees into radians. It serves for input into trig. functions.
    distance_matrix_km = zeros(number_zones, number_zones);
    for i=1:(number_zones-1)
        long1 = centroid_long[i];
        lat1 = centroid_lat[i];
        for j=i:number_zones
            long2 = centroid_long[j];
            lat2 = centroid_lat[j];
            distance_km = c_lat_long * sqrt((cos((lat1+lat2)/2 * conv_deg_rad))^2 * (long1-long2)^2 + (lat1-lat2)^2);
            distance_matrix_km[i,j] = distance_km;
            distance_matrix_km[j,i] = distance_km;
        end
    end

    ## Filling the diagonal zeros that stand for a travel distance within the same zone with a distance of 1 km.
    for i=1:number_zones
        distance_matrix_km[i,i] = 1;
    end

    ## save the results
    results = zeros(number_zones, 2)
    for i = 1:number_zones
        results[i,1] = centroid_lat[i];
        results[i,2] = centroid_long[i];
    end
    results = convert(DataFrame, results);
    header_vector = ["latitude", "longitude"]; # creates an empty string vector in the desired dimension
    results_string = string(path_to_results,"/zoneID_coordinates.csv");
    CSV.write(results_string, results, header=header_vector);

    return distance_matrix_km, number_zones

end


#=
  Original Author: Anastasia Yendiki

  Copyright © 2022 The General Hospital Corporation (Boston, MA) "MGH"

  Terms and conditions for use, reproduction, distribution and contribution
  are found in the 'FreeSurfer Software License Agreement' contained
  in the file 'LICENSE' found in the FreeSurfer distribution, and here:

  https://surfer.nmr.mgh.harvard.edu/fswiki/FreeSurferSoftwareLicense

  Reporting: freesurfer@nmr.mgh.harvard.edu
=#

using LinearAlgebra

export Tract, trk_read, trk_write

"Container for header and streamline data stored in .trk format"
mutable struct Tract
  # Header fields (.trk format version 2)
  id_string::Vector{UInt8}
  dim::Vector{Int16}
  voxel_size::Vector{Float32}
  origin::Vector{Float32}
  n_scalars::Int16
  scalar_name::Matrix{UInt8}
  n_properties::Int16
  property_name::Matrix{UInt8}
  vox_to_ras::Matrix{Float32}
  reserved::Vector{UInt8}
  voxel_order::Vector{UInt8}
  voxel_order_original::Vector{UInt8}
  image_orientation_patient::Vector{Float32}
  pad1::Vector{UInt8}
  invert_x::UInt8
  invert_y::UInt8
  invert_z::UInt8
  swap_xy::UInt8
  swap_yz::UInt8
  swap_zx::UInt8
  n_count::Int32
  version::Int32
  hdr_size::Int32

  # Streamline data fields
  npts::Vector{Int32}
  properties::Matrix{Float32}
  xyz::Vector{Any}
  scalars::Vector{Any}
end

"""
    Tract()

Return an empty `Tract` structure
"""
Tract() = Tract(
  Vector{UInt8}(undef, 0),
  Vector{Int16}(undef, 0),
  Vector{Float32}(undef, 0),
  Vector{Float32}(undef, 0),
  Int16(0),
  Matrix{UInt8}(undef, 0, 0),
  Int16(0),
  Matrix{UInt8}(undef, 0, 0),
  Matrix{Float32}(undef, 0, 0),
  Vector{UInt8}(undef, 0),
  Vector{UInt8}(undef, 0),
  Vector{UInt8}(undef, 0),
  Vector{Float32}(undef, 0),
  Vector{UInt8}(undef, 0),
  UInt8(0),
  UInt8(0),
  UInt8(0),
  UInt8(0),
  UInt8(0),
  UInt8(0),
  Int32(0),
  Int32(0),
  Int32(0),

  Vector{Int32}(undef, 0),
  Matrix{Float32}(undef, 0, 0),
  [],
  []
)

"""
    Tract(ref::MRI)

Return a `Tract` structure whose header fields are populated based on the
reference `MRI` structure `ref`
"""
function Tract(ref::MRI)

  tr = Tract()

  # In the following I must take into account that mri_read()
  # may have permuted the first 2 dimensions of volsize and volres,
  # but it has not changed the vox2ras matrix

  # Find orientation of image coordinate system
  orient = vox2ras_to_orient(ref.vox2ras)

  # Find patient-to-scanner coordinate transform:
  # Take x and y vectors from vox2RAS matrix, convert to LPS,
  # divide by voxel size
  if ref.ispermuted
    p2s = [-1 0 0; 0 -1 0; 0 0 1] * ref.vox2ras[1:3, 1:2] *
                                    Diagonal([1, 1]./ref.volres[[2,1]])
  else
    p2s = [-1 0 0; 0 -1 0; 0 0 1] * ref.vox2ras[1:3, 1:2] *
                                    Diagonal([1, 1]./ref.volres[1:2])
  end

  tr.id_string     = UInt8.(collect("TRACK\0"))

  if ref.ispermuted
    tr.dim         = Int16.(ref.volsize[[2,1,3]])
    tr.voxel_size  = Float32.(ref.volres[[2,1,3]])
  else
    tr.dim         = Int16.(ref.volsize)
    tr.voxel_size  = Float32.(ref.volres)
  end
  tr.origin        = Float32.(zeros(3))

  tr.n_scalars     = Int16(0)
  tr.scalar_name   = UInt8.(zeros(10, 20))
  tr.n_properties  = Int16(0)
  tr.property_name = UInt8.(zeros(10, 20))

  tr.vox_to_ras    = ref.vox2ras
  tr.reserved      = UInt8.(zeros(444))
  tr.voxel_order               = vcat(UInt8.(collect(orient)), UInt8(0))
  tr.voxel_order_original      = tr.voxel_order
  tr.image_orientation_patient = Float32.(p2s[:])
  tr.pad1          = UInt8.(zeros(2))

  tr.invert_x      = UInt8(0)
  tr.invert_y      = UInt8(0)
  tr.invert_z      = UInt8(0)
  tr.swap_xy       = UInt8(0)
  tr.swap_yz       = UInt8(0)
  tr.swap_zx       = UInt8(0)
  tr.n_count       = Int32(0)
  tr.version       = Int32(2)
  tr.hdr_size      = Int32(1000)

  return tr
end

"""
    trk_read(infile::String)

Read tractography streamlines from `infile` and return a `Tract` structure

Input file must be in .trk format, see:
http://trackvis.org/docs/?subsect=fileformat
"""
function trk_read(infile::String)

  tr = Tract()

  io = open(infile, "r")

  # Read .trk header
  tr.id_string     = read!(io, Vector{UInt8}(undef, 6))

  tr.dim           = read!(io, Vector{Int16}(undef, 3))
  tr.voxel_size    = read!(io, Vector{Float32}(undef, 3))
  tr.origin        = read!(io, Vector{Float32}(undef, 3))

  tr.n_scalars     = read(io, Int16)
  tr.scalar_name   = read!(io, Matrix{UInt8}(undef, 20, 10))
  tr.scalar_name   = permutedims(tr.scalar_name, [2,1])

  tr.n_properties  = read(io, Int16)
  tr.property_name = read!(io, Matrix{UInt8}(undef, 20, 10))
  tr.property_name = permutedims(tr.property_name, [2,1])

  tr.vox_to_ras    = read!(io, Matrix{Float32}(undef, 4, 4))
  tr.vox_to_ras    = permutedims(tr.vox_to_ras, [2,1])
  tr.reserved      = read!(io, Vector{UInt8}(undef, 444))
  tr.voxel_order               = read!(io, Vector{UInt8}(undef, 4))
  tr.voxel_order_original      = read!(io, Vector{UInt8}(undef, 4))
  tr.image_orientation_patient = read!(io, Vector{Float32}(undef, 6))
  tr.pad1          = read!(io, Vector{UInt8}(undef, 2))

  tr.invert_x      = read(io, UInt8)
  tr.invert_y      = read(io, UInt8)
  tr.invert_z      = read(io, UInt8)
  tr.swap_xy       = read(io, UInt8)
  tr.swap_yz       = read(io, UInt8)
  tr.swap_zx       = read(io, UInt8)
  tr.n_count       = read(io, Int32)
  tr.version       = read(io, Int32)
  tr.hdr_size      = read(io, Int32)

  # Read streamline data
  tr.npts       = Vector{Int32}(undef, tr.n_count)
  tr.properties = Matrix{Float32}(undef, tr.n_properties, tr.n_count)

  for istr in 1:tr.n_count		# Loop over streamlines
    tr.npts[istr] = read(io, Int32)

    push!(tr.xyz, Matrix{Float32}(undef, 3, tr.npts[istr]))
    push!(tr.scalars, Matrix{Float32}(undef, tr.n_scalars, tr.npts[istr]))

    for ipt in 1:tr.npts[istr]		# Loop over points on a streamline
      # Divide by voxel size and make voxel coordinates 0-based
      tr.xyz[istr][:, ipt] = read!(io, Vector{Float32}(undef, 3)) ./
                             tr.voxel_size .- .5

      tr.scalars[istr][:, ipt] = read!(io, Vector{Float32}(undef, tr.n_scalars))
    end

    tr.properties[:, istr] = read!(io, Vector{Float32}(undef, tr.n_properties))
  end

  close(io)

  return tr
end

"""
     trk_write(tr::Tract, outfile::String)

Write a `Tract` structure to a file in the .trk format

Return true if an error occurred (i.e., the number of bytes written was not the
expected based on the size of the `Tract` structure)
"""
function trk_write(tr::Tract, outfile::String)

  io = open(outfile, "w")

  nb = 0

  # Write .trk header
  nb += write(io, UInt8.(tr.id_string))

  nb += write(io, Int16.(tr.dim))
  nb += write(io, Float32.(tr.voxel_size))
  nb += write(io, Float32.(tr.origin))

  nb += write(io, Int16(tr.n_scalars))
  nb += write(io, UInt8.(permutedims(tr.scalar_name, [2,1])))
  nb += write(io, Int16(tr.n_properties))
  nb += write(io, UInt8.(permutedims(tr.property_name, [2,1])))

  nb += write(io, Float32.(permutedims(tr.vox_to_ras, [2,1])))
  nb += write(io, UInt8.(tr.reserved))
  nb += write(io, UInt8.(tr.voxel_order))
  nb += write(io, UInt8.(tr.voxel_order_original))
  nb += write(io, Float32.(tr.image_orientation_patient))
  nb += write(io, UInt8.(tr.pad1))

  nb += write(io, UInt8(tr.invert_x))
  nb += write(io, UInt8(tr.invert_y))
  nb += write(io, UInt8(tr.invert_z))
  nb += write(io, UInt8(tr.swap_xy))
  nb += write(io, UInt8(tr.swap_yz))
  nb += write(io, UInt8(tr.swap_zx))
  nb += write(io, Int32(tr.n_count))
  nb += write(io, Int32(tr.version))
  nb += write(io, Int32(tr.hdr_size))

  # Write streamline data
  for istr in 1:tr.n_count		# Loop over streamlines
    nb += write(io, Int32(tr.npts[istr]))

    for ipt in 1:tr.npts[istr]		# Loop over points on a streamline
      # Make voxel coordinates .5-based and multiply by voxel size
      nb += write(io, Float32.((tr.xyz[istr][:, ipt] .+ .5) .* tr.voxel_size))

      nb += write(io, Float32.(tr.scalars[istr][:, ipt]))
    end

    nb += write(io, Float32.(tr.properties[:, istr]))
  end

  close(io)

  err = (nb != sizeof(UInt8) * 866 +
               sizeof(Int32) * (3 + length(tr.npts)) +
               sizeof(Int16) * 5 +
               sizeof(Float32) * (28 + sum(length.(tr.xyz)) +
                                       sum(length.(tr.scalars)) +
                                       length(tr.properties)))

  return err
end


using Comrade, ComradeNested, Distributions
using Test

load_ehtim()
include(joinpath(@__DIR__, "../../../test/test_util.jl"))

@testset "ComradeNested.jl" begin
    m, vis, amp, lcamp, cphase = load_data()
    prior = test_prior()
    lklhd = RadioLikelihood(lcamp, cphase)
    post = Posterior(lklhd, prior, test_model)
    a1 = Nested(dimension(post), 1000)

    chain, stats = sample(post, a1; dlogz=0.01, progress=false)

    #cpost = ascube(post)
    #xopt = chain[end]
    #@test isapprox(xopt.f1/xopt.f2, 2.0, atol=1e-2)
    #@test isapprox(xopt.σ1*2*sqrt(2*log(2)), μas2rad(40.0), rtol=1e-3)
    #@test isapprox(xopt.σ1*xopt.τ1*2*sqrt(2*log(2)), μas2rad(20.0), rtol=1e-3)
    #@test isapprox(-xopt.ξ1, π/3, atol=1e-2)
    #@test isapprox(xopt.σ2*2*sqrt(2*log(2)), μas2rad(20.0), atol=1e-2)
    #@test isapprox(xopt.σ2*xopt.τ2*2*sqrt(2*log(2)), μas2rad(10.0), rtol=1e-2)
    #@test isapprox(-xopt.ξ2, π/6, atol=1e-2)
    #@test isapprox(xopt.x, μas2rad(30.0), rtol=1e-2)
    #@test isapprox(xopt.y, μas2rad(30.0), rtol=1e-2)

end


module Colors

using FixedPointNumbers
using Reexport

@reexport using ColorTypes
Base.@deprecate_binding RGB1 XRGB
Base.@deprecate_binding RGB4 RGBX

import Base: ==, +, -, *, /
import Base: convert, eltype, isless, range, show, showable, typemin, typemax

# Additional exports, not exported by ColorTypes
export weighted_color_mean,
       hex, @colorant_str,
       protanopic, deuteranopic, tritanopic,
       distinguishable_colors, whitebalance,
       colordiff, DE_2000, DE_94, DE_JPC79, DE_CMC, DE_BFD, DE_AB, DE_DIN99, DE_DIN99d, DE_DIN99o,
       MSC, sequential_palette, diverging_palette, colormap,
       normalize_hue, mean_hue,
       colormatch, CIE1931_CMF, CIE1964_CMF, CIE1931J_CMF, CIE1931JV_CMF, CIE2006_2_CMF, CIE2006_10_CMF

# Early utilities
include("utilities.jl")

# Include other module components
include("conversions.jl")
include("algorithms.jl")
include("parse.jl")
include("differences.jl")
include("colormaps.jl")
include("display.jl")
include("colormatch.jl")

"""
Colors used in the Julia logo as a `NamedTuple`.

The keys are approximate descriptions of the hue and do not include black.

Not exported, use as `JULIA_LOGO_COLORS.red` etc.
"""
const JULIA_LOGO_COLORS = (red = RGB{N0f8}(0.796, 0.235, 0.2),  # colorant"#cb3c33" blocks precompilation
                           green = RGB{N0f8}(0.22, 0.596, 0.149),
                           blue = RGB{N0f8}(0.251, 0.388, 0.847),
                           purple = RGB{N0f8}(0.584, 0.345, 0.698))

if VERSION >= v"1.1"   # work around https://github.com/JuliaLang/julia/issues/34121
    include("precompile.jl")
    _precompile_()
end

end # module


using MPIReco

@testset "multi-patch in-memory reconstruction" begin
  bSF = MultiMPIFile([joinpath(datadir, "calibrations", "12.mdf")])
  dirs = ["1.mdf", "2.mdf", "3.mdf", "4.mdf"]
  b = MultiMPIFile(joinpath.(datadir, "measurements", "20211226_203916_MultiPatch", dirs))
  names = (:color, :x, :y, :z, :time)
  values1 = (1:1,
	    -27.5u"mm":1.25u"mm":27.5u"mm",
	    -27.5u"mm":1.25u"mm":27.5u"mm",
	    0.0u"mm":1.0u"mm":0.0u"mm",
	    0.0u"ms":0.6528u"ms":0.0u"ms")
  values2 = (values1[1], -19.375u"mm":1.25u"mm":19.375u"mm",
	     -19.375u"mm":1.25u"mm":19.375u"mm", values1[4:5]...)

  # basic multi-patch reconstruction
  c1 = reconstruction(bSF, b;
			    SNRThresh=5, frames=1, minFreq=80e3,
			    recChannels=1:2,iterations=1,
			    spectralLeakageCorrection=false)
  @test axisnames(c1) == names
  @test axisvalues(c1) == values1
  exportImage(joinpath(imgdir, "MultiPatch1.png"), arraydata(c1[1,:,:,1,1]))

  # TODO test description
  c2 = reconstruction(bSF, b;
			    SNRThresh=5, frames=1, minFreq=80e3,
			    recChannels=1:2,iterations=1, roundPatches=true,
			    spectralLeakageCorrection=false)
  @test axisnames(c2) == names
  @test axisvalues(c2) == values1
  exportImage(joinpath(imgdir, "MultiPatch2.png"), arraydata(c2[1,:,:,1,1]))

  # multi-patch reconstruction using multiple system matrices
  dirs = ["8.mdf", "9.mdf", "10.mdf", "11.mdf"]
  bSFs = MultiMPIFile(joinpath.(datadir, "calibrations", dirs))
  c3 = reconstruction(bSFs, b;
			    SNRThresh=5, frames=1, minFreq=80e3,
			    recChannels=1:2,iterations=1,
			    spectralLeakageCorrection=false)
  @test axisnames(c3) == names
  @test axisvalues(c3) == values2
  exportImage(joinpath(imgdir, "MultiPatch3.png"), arraydata(c3[1,:,:,1,1]))

  # flexible multi-patch reconstruction
  dirs = ["8.mdf", "9.mdf", "10.mdf", "11.mdf"]
  bSFs = MultiMPIFile(joinpath.(datadir, "calibrations", dirs))
  mapping = [1,2,3,4]
  freq = filterFrequencies(bSFs, SNRThresh=5, minFreq=80e3)
  S = [getSF(SF,freq,nothing,"kaczmarz", bgcorrection=false)[1] for SF in bSFs]
  SFGridCenter = zeros(3,4)
  FFPos = zeros(3,4)
  FFPos[:,1] = [-0.008, 0.008, 0.0]
  FFPos[:,2] = [-0.008, -0.008, 0.0]
  FFPos[:,3] = [0.008, 0.008, 0.0]
  FFPos[:,4] = [0.008, -0.008, 0.0]
  c4 = reconstruction(bSFs, b;
			    SNRThresh=5, frames=1, minFreq=80e3,
			    recChannels=1:2,iterations=1,
			    spectralLeakageCorrection=false, mapping=mapping,
			    systemMatrices = S, SFGridCenter=SFGridCenter,
			    FFPos=FFPos, FFPosSF=FFPos)
  @test axisnames(c4) == names
  @test axisvalues(c4) == values2
  exportImage(joinpath(imgdir, "MultiPatch4.png"), arraydata(c4[1,:,:,1,1]))
  # TODO the last test shows odd results
end


# *********************************************************************************
# REopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# Redistributions of source code must retain the above copyright notice, this list
# of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice, this
# list of conditions and the following disclaimer in the documentation and/or other
# materials provided with the distribution.
#
# Neither the name of the copyright holder nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
# *********************************************************************************
"""
    MPCElectricLoad

    Base.@kwdef struct MPCElectricLoad
        loads_kw::Array{Real,1}
        critical_loads_kw::Union{Missing, Array{Real,1}} = missing
    end
"""
Base.@kwdef struct MPCElectricLoad
    loads_kw::Array{Real,1}
    critical_loads_kw::Union{Missing, Array{Real,1}} = missing
end

"""
    MPCFinancial

    Base.@kwdef struct MPCFinancial
        value_of_lost_load_per_kwh::Union{Array{R,1}, R} where R<:Real = 1.00
    end
"""
Base.@kwdef struct MPCFinancial
    value_of_lost_load_per_kwh::Union{Array{R,1}, R} where R<:Real = 1.00
end

"""
    MPCPV
```julia
Base.@kwdef struct MPCPV
    name::String="PV"
    size_kw::Real = 0
    prod_factor_series::Union{Missing, Array{Real,1}} = missing
end
```
"""
Base.@kwdef struct MPCPV
    name::String="PV"
    size_kw::Real = 0
    prod_factor_series::Union{Missing, Array{Real,1}} = missing
end

struct MPCElectricTariff
    energy_rates::AbstractVector{Float64}
    n_energy_tiers::Int

    monthly_demand_rates::AbstractVector{Float64}
    time_steps_monthly::Array{Array{Int64,1},1}  # length = 0 or 12
    monthly_previous_peak_demands::AbstractVector{Float64}
    n_monthly_demand_tiers::Int

    tou_demand_rates::AbstractVector{Float64}
    tou_demand_ratchet_time_steps::Array{Array{Int64,1},1}  # length = n_tou_demand_ratchets
    tou_previous_peak_demands::AbstractVector{Float64}
    n_tou_demand_tiers::Int

    fixed_monthly_charge::Float64
    annual_min_charge::Float64
    min_monthly_charge::Float64

    export_rates::DenseAxisArray{Array{Float64,1}}
    export_bins::Array{Symbol,1}

    # coincident_peak not used in MPC but must implement it for model building
    coincident_peak_load_active_time_steps::AbstractVector{AbstractVector{Int64}}
    coincident_peak_load_charge_per_kw::AbstractVector{Float64}
    coincpeak_periods::AbstractVector{Int64}
end

"""
    MPCElectricTariff(d::Dict)

function for parsing user inputs into
```julia
    struct MPCElectricTariff
        monthly_previous_peak_demands::Array{Float64,1}
        energy_rates::Array{Float64,1}

        monthly_demand_rates::Array{Float64,1}
        time_steps_monthly::Array{Array{Int64,1},1}  # length = 0 or 12

        tou_demand_rates::Array{Float64,1}
        tou_demand_ratchet_time_steps::Array{Array{Int64,1},1}  # length = n_tou_demand_ratchets
        tou_previous_peak_demands::Array{Float64,1}

        fixed_monthly_charge::Float64
        annual_min_charge::Float64
        min_monthly_charge::Float64

        export_rates::DenseAxisArray{Array{Float64,1}}
        export_bins::Array{Symbol,1}
    end
```

Keys for `d` include:

  - `energy_rates`

    - REQUIRED
    - must have length equal to `ElectricLoad.loads_kw`

  - `monthly_demand_rates`

    - default = [0]

  - `time_steps_monthly`

    - array of arrays for integer time steps that the `monthly_demand_rates` apply to
    - default = [collect(1:length(energy_rates))]

  - `monthly_previous_peak_demands`

    - default = [0]

  - `tou_demand_rates`

    - an array of time-of-use demand rates
    - must have length equal to `tou_demand_time_steps`
    - default = []

  - `tou_demand_time_steps`

    - an array of arrays for the integer time steps that apply to the `tou_demand_rates`
    - default = []

  - `tou_previous_peak_demands`

    - an array of the previous peak demands set in each time-of-use demand period
    - must have length equal to `tou_demand_time_steps`
    - default = []

  - `net_metering`

    - boolean, if `true` then customer DER export is compensated at the `energy_rates`

  - `export_rates`

    - can be a <:Real or Array{<:Real, 1}, or not provided
    - if provided, customer DER export is compensated at the `export_rates`

NOTE: if both `net_metering=true` and `export_rates` are provided then the model can choose from either option.
"""
function MPCElectricTariff(d::Dict)

    energy_rates = d["energy_rates"]

    monthly_demand_rates = get(d, "monthly_demand_rates", [0.0])
    time_steps_monthly = get(d, "time_steps_monthly", [collect(1:length(energy_rates))])
    monthly_previous_peak_demands = get(d, "monthly_previous_peak_demands", [0.0])

    tou_demand_rates = get(d, "tou_demand_rates", Float64[])
    tou_demand_time_steps = get(d, "tou_demand_time_steps", [])
    tou_previous_peak_demands = get(d, "tou_previous_peak_demands", Float64[])
    @assert length(tou_demand_rates) == length(tou_demand_time_steps) == length(tou_previous_peak_demands)

    # TODO can remove these inputs?
    fixed_monthly_charge = 0.0
    annual_min_charge = 0.0
    min_monthly_charge = 0.0

    # TODO handle tiered rates
    export_bins = [:NEM, :WHL]
    nem_rate = []
    NEM = get(d, "net_metering", false)
    if NEM
        nem_rate = [-0.999 * x for x in energy_rates]
    end
    # export_rates can be a <:Real or Array{<:Real, 1}, or not provided
    export_rates = get(d, "export_rates", nothing)
    if !isnothing(export_rates)
        export_rates = convert(Vector{Real}, export_rates)
    end
    whl_rate = create_export_rate(export_rates, length(energy_rates), 1)

    if !NEM & (sum(whl_rate) >= 0)
        export_rates = DenseAxisArray{Array{Float64,1}}(undef, [])
        export_bins = Symbol[]
    elseif !NEM
        export_bins = [:WHL]
        export_rates = DenseAxisArray([whl_rate], export_bins)
    elseif (sum(whl_rate) >= 0)
        export_bins = [:NEM]
        export_rates = DenseAxisArray([nem_rate], export_bins)
    else
        export_bins = [:NEM, :WHL]  # NOTE: not modeling EXC bin b/c MPC does not track annaul energy exported
        export_rates = DenseAxisArray([nem_rate, whl_rate], export_bins)
    end

    MPCElectricTariff(
        energy_rates,
        1,
        monthly_demand_rates,
        time_steps_monthly,
        monthly_previous_peak_demands,
        1,
        tou_demand_rates,
        tou_demand_time_steps,
        tou_previous_peak_demands,
        1,
        fixed_monthly_charge,
        annual_min_charge,
        min_monthly_charge,
        export_rates,
        export_bins,

        # empty values for coincident_peak
        [Int64[]],
        Float64[],
        Int64[],
    )
end

"""
    MPCElectricStorage

```julia
Base.@kwdef struct MPCElectricStorage < AbstractElectricStorage
    size_kw::Float64
    size_kwh::Float64
    charge_efficiency::Float64 =  0.96 * 0.975^2
    discharge_efficiency::Float64 =  0.96 * 0.975^2
    soc_min_pct::Float64 = 0.2
    soc_init_pct::Float64 = 0.5
    can_grid_charge::Bool = true
    grid_charge_efficiency::Float64 = 0.96 * 0.975^2
end
```
"""
Base.@kwdef struct MPCElectricStorage <: AbstractElectricStorage
    size_kw::Float64
    size_kwh::Float64
    charge_efficiency::Float64 = 0.96 * 0.975^2
    discharge_efficiency::Float64 = 0.96 * 0.975^2
    soc_min_pct::Float64 = 0.2
    soc_init_pct::Float64 = 0.5
    can_grid_charge::Bool = true
    grid_charge_efficiency::Float64 = 0.96 * 0.975^2
    max_kw::Float64 = size_kw
    max_kwh::Float64 = size_kwh
    minimum_avg_soc_fraction::Float64 = 0.0
end

"""
    MPCGenerator

struct with inner constructor:
```julia
function MPCGenerator(;
    size_kw::Real,
    fuel_cost_per_gallon::Float64 = 3.0,
    fuel_slope_gal_per_kwh::Float64 = 0.076,
    fuel_intercept_gal_per_hr::Float64 = 0.0,
    fuel_avail_gal::Float64 = 660.0,
    min_turn_down_pct::Float64 = 0.0,  # TODO change this to non-zero value
    only_runs_during_grid_outage::Bool = true,
    sells_energy_back_to_grid::Bool = false,
    om_cost_per_kwh::Float64=0.0,
    )
```
"""
struct MPCGenerator <: AbstractGenerator
    size_kw
    max_kw
    fuel_cost_per_gallon
    fuel_slope_gal_per_kwh
    fuel_intercept_gal_per_hr
    fuel_avail_gal
    min_turn_down_pct
    only_runs_during_grid_outage
    sells_energy_back_to_grid
    om_cost_per_kwh

    function MPCGenerator(;
        size_kw::Real,
        fuel_cost_per_gallon::Float64 = 3.0,
        fuel_slope_gal_per_kwh::Float64 = 0.076,
        fuel_intercept_gal_per_hr::Float64 = 0.0,
        fuel_avail_gal::Float64 = 660.0,
        min_turn_down_pct::Float64 = 0.0,  # TODO change this to non-zero value
        only_runs_during_grid_outage::Bool = true,
        sells_energy_back_to_grid::Bool = false,
        om_cost_per_kwh::Float64=0.0,
        )

        max_kw = size_kw

        new(
            size_kw,
            max_kw,
            fuel_cost_per_gallon,
            fuel_slope_gal_per_kwh,
            fuel_intercept_gal_per_hr,
            fuel_avail_gal,
            min_turn_down_pct,
            only_runs_during_grid_outage,
            sells_energy_back_to_grid,
            om_cost_per_kwh,
        )
    end
end

"""
    MPCCoolingLoad

    Base.@kwdef struct MPCCoolingLoad
        loads_kw_thermal::Array{Real,1}
    end
"""
Base.@kwdef struct MPCCoolingLoad
    loads_kw_thermal::Array{Real,1}
    cop::Union{Real, Nothing}
end

"""
    MPCLimits

struct for MPC specific input parameters:
- `grid_draw_limit_kw_by_time_step::Vector{<:Real}` limits for grid power consumption in each time step; length must be same as `length(loads_kw)`.
- `export_limit_kw_by_time_step::Vector{<:Real}` limits for grid power export in each time step; length must be same as `length(loads_kw)`.

!!! warn
    `grid_draw_limit_kw_by_time_step` and `export_limit_kw_by_time_step` values can lead to
    infeasible problems. For example, there is a constraint that the electric load must be met in
    each time step and by limiting the amount of power from the grid the load balance constraint
    could be infeasible.
"""
Base.@kwdef struct MPCLimits
    grid_draw_limit_kw_by_time_step::Vector{<:Real} = Real[]
    export_limit_kw_by_time_step::Vector{<:Real} =  Real[]
end


#     The forecast_ functions create the forecast dictionary, which contains time-
#     varying parameters.

function forecast_base!(fileNm::String, p::Dict; f::Dict=Dict(),
    startDate::Date=Date(), endDate::Date=Date(), startT::Int=1,
    T::Int=-1, history::Dict=Dict())
    return forecast_base!(CSV.read(fileNm), p; f=f, startDate=startDate,
        endDate=endDate, startT=startT, T=T, history=history)
end

function forecast_base!(data::DataFrame, p::Dict; f::Dict=Dict(),
    startDate::Date=Date(), endDate::Date=Date(), startT::Int=1,
    T::Int=-1, history::Dict=Dict())
    # in the optimization, we are in local time
    data[:Date] += Dates.Hour(p[:utc_offset])

    if startDate != Date()
        startT = findDate(startDate, Date.(data[:Date]))
    end

    if endDate != Date()
        endT = findDate(endDate, Date.(data[:Date]))
        T = endT - startT + 2;
    end

    # Time
    if T == -1
        T = 1 + size(data)[1] - startT+1;
    end
    endT = startT + T - 2;
    f[:startT] = startT; # remember where I started reading from.
    f[:endT] = endT; # remember where I stopped reading from.
    f[:T] = T;
    f[:timestamp] = data[:Date][startT:endT]

    # number of months
    f[:M] = getMonthNum(T-1, f);
    f[:loadKWH] = data[:Campus_KWH][startT:endT]; # kWh

    f[:hourCA] = mod.(Dates.hour.(f[:timestamp])-7, 24)
    return f;
end

function forecast_water!(f::Dict, fileNm::String, p::Dict; history::Dict=Dict())
    return forecast_water!(f, readtable(fileNm), p; history)
end

function forecast_water!(f::Dict, data::DataFrame, p::Dict;
    history::Dict=Dict())

    startT = f[:startT]
    endT = f[:endT]

    if haskey(data, :chillerPE)
        f[:chillerPE] = data[:chillerPE][startT:endT] # kW/ton
    else
        f[:chillerPE] = ones(endT-startT+1) * p[:chillerPE]
    end
    f[:loadHW] = data[:HW_loads][startT:endT] # tons
    f[:loadCW] = data[:CW_loads][startT:endT] # mmbtu
    if haskey(history, :hwTank)
        f[:hwTankInit] = history[:hwTank][end]
    else
        f[:hwTankInit] = p[:hwTankInit]
    end
    if haskey(history, :cwTank)
        f[:cwTankInit] = history[:cwTank][end]
    else
        f[:cwTankInit] = p[:cwTankInit]
    end
    f[:cwTankEnd] = f[:cwTankInit]/2
    f[:hwTankEnd] = f[:hwTankInit]/2
    return f;
end

function forecast_storage!(f::Dict, fileNm::String, p::Dict;
    history::Dict=Dict())
    return forecast_storage!(f, readtable(fileNm), p; history=history)
end

function forecast_storage!(f::Dict, data::DataFrame, p::Dict;
    history::Dict=Dict())

    startT = f[:startT]
    endT = f[:endT]

    if haskey(history, :elecStorageInit)
        f[:elecStorageInit] = history[:elecStorageInit][end]
    else
        f[:elecStorageInit] = p[:elecStorageInit]
    end
    return f;
end

function forecast_prices!(f::Dict, fileNm::String, p::Dict;
    history::Dict=Dict())
    return forecast_prices!(f, readtable(fileNm), p; history=history)
end

function forecast_prices!(f::Dict, data::DataFrame, p::Dict;
    history::Dict=Dict())

    startT = f[:startT]
    endT = f[:endT]
    # add a fixed cost here?
    f[:elecPrice] = data[:Electricity_prices][startT:endT] # $/kW
    f[:gasPrice] = p[:gasPrice]
    #f[:demandCharge] = p[:demandCharge] * ones(f[:M])
    # use demandCharge data to populate f[:demandCharge]
    simMonths = unique(DateTime.(Dates.Year.(f[:timestamp]),
                                 Dates.Month.(f[:timestamp])))
    f[:demandCharge] = [p[:demandCharge][m] for m in simMonths]
    return f
end

function forecast_carbon!(f::Dict, fileNm::String, p::Dict;
    history::Dict=Dict())
    return forecast_carbon!(f, readtable(fileNm), p, history)
end

function forecast_carbon!(f::Dict, data::DataFrame, p::Dict;
    history::Dict=Dict())

    startT = f[:startT]
    endT = f[:endT]

    f[:carbonIntensityGrid] = data[:carbon][startT:endT]*p[:carbon_units]
    f[:carbonIntensityGas] = p[:carbonIntensityGas]
    f[:carbonPrice] = p[:carbonPrice]
    return f
end

function forecast_CBP!(f::Dict, fileNm::String, p::Dict; history::Dict=Dict())
    if !haskey(f, :eGrid)
        error("There should be :eGrid in f")
    end
    f[:event_ts] # contains a list of timestamps at which we are doing demand response

    f[:ts_to_index] #maps ts->integer index
    return f;
end


export
    MultiStageCBS

module MultiStageCBS

using ..CRCBS
using Parameters, LightGraphs, DataStructures
using GraphUtils: get_graph, get_vtx

# NOTE MultiStageCBS Could also serve as the definition for "iterative MAPF" as
# defined in "Priority Inheritance with Backtracking for Iterative Multi-agent
# Path Finding", Okumura et al, IJCAI 2019

################################################################################
############################### ENVIRONMENT DEF ################################
################################################################################
# state
@with_kw struct State <: AbstractGraphState
    vtx::Int    = -1 # vertex of graph
    stage::Int  = -1 # which stage of the sequence
    t::Int      = -1
end
Base.convert(::Type{State},s::GraphState) = State(vtx=get_vtx(s),t=get_t(s))
# CRCBS.get_vtx(s::State) = s.vtx
CRCBS.get_t(s::State) = s.t
get_stage(s::State) = s.stage
Base.string(s::State) = "(v=$(get_vtx(s)),stage=$(get_stage(s)),t=$(get_t(s)))"
CRCBS.is_valid(state::State) = get_vtx(state) > 0
# action
@with_kw struct Action <: AbstractGraphAction
    e::Edge{Int}    = Edge(-1,-1)
    dt::Int         = 1
end
Base.convert(::Type{Action},a::GraphAction) = Action(e=get_e(a),dt=get_dt(a))
CRCBS.get_e(a::Action) = a.e
CRCBS.get_dt(a::Action) = a.dt
Base.string(a::Action) = "(e=$(get_e(a).src) → $(get_e(a).dst))"
@with_kw struct LowLevelEnv{C<:AbstractCostModel,H<:LowLevelSearchHeuristic,G<:AbstractGraph,T} <: GraphEnv{State,Action,C}
    graph::G                        = Graph()
    goal_sequence::Vector{State}    = Vector{State}()
    agent_idx::Int                  = -1
    constraints::T                  = discrete_constraint_table(nv(graph),nv(graph)^2,agent_idx)
    cost_model::C                   = SumOfTravelTime()
    heuristic::H                    = NullHeuristic() # MultiStagePerfectHeuristic(graph,Vector{Vector{Int}}())
end
# CRCBS.get_graph(env::LowLevelEnv)            = env.graph
# GraphUtils.get_graph(env::LowLevelEnv)            = env.graph
# CRCBS.get_cost_model(env::LowLevelEnv)       = env.cost_model
# CRCBS.get_agent_id(env::LowLevelEnv)         = env.agent_idx
# CRCBS.get_constraints(env::LowLevelEnv)      = env.constraints
# CRCBS.get_goal(env::LowLevelEnv)             = env.goal_sequence
# CRCBS.get_heuristic_model(env::LowLevelEnv)  = env.heuristic
CRCBS.base_env_type(env::LowLevelEnv)        = LowLevelEnv

function CRCBS.get_next_state(s::State,a::Action)
    @assert(is_valid(s))
    @assert(get_vtx(s) == get_e(a).src)
    State(get_e(a).dst, get_stage(s), get_t(s)+get_dt(a))
end
function CRCBS.get_next_state(env::E,s::State,a::Action) where {E<:LowLevelEnv}
    @assert(is_valid(s))
    @assert(get_stage(s) <= length(env.goal_sequence))
    stage = get_stage(s)
    if states_match(s, env.goal_sequence[get_stage(s)])
        stage = min(stage+1, length(env.goal_sequence))
    end
    return State(get_e(a).dst, stage, get_t(s)+get_dt(a))
end
CRCBS.get_possible_actions(env::LowLevelEnv,s)  = map(v->Action(e=Edge(get_vtx(s),v)),outneighbors(get_graph(env),get_vtx(s)))
CRCBS.wait(s::State) = Action(e=Edge(get_vtx(s),get_vtx(s)))
CRCBS.wait(env::E,s::State) where {E<:LowLevelEnv} = Action(e=Edge(get_vtx(s),get_vtx(s)))

function CRCBS.build_env(mapf::MAPF{E,S,G}, node::N, idx::Int)  where {S,G,E <: LowLevelEnv,N<:ConstraintTreeNode}
    goals = deepcopy(mapf.goals[idx])
    n = PathNode{State,Action}(sp=goals[end])
    s_constraints, _ = search_constraints(mapf.env,get_constraints(node,idx),n)
    t_goal = get_t(get_sp(n))
    for c in s_constraints
        t_goal = max(t_goal,get_time_of(c)+1)
    end
    goals[end] = State(get_sp(n),t=t_goal)
    E(
        graph = get_graph(mapf.env),
        constraints = get_constraints(node,idx),
        goal_sequence = goals,
        agent_idx = idx,
        cost_model = get_cost_model(mapf.env),
        heuristic = get_heuristic_model(mapf.env)
        )
end
function CRCBS.is_consistent(path::Path,start::State,goals::Vector{State})
    valid = true
    stage = 1
    if states_match(start,goals[stage])
        stage += 1
    end
    for k in 1:length(path)
        node = get_path_node(path,k)
        if states_match(get_sp(node),goals[stage])
            stage += 1
        end
        if stage > length(goals)
            return true
        end
    end
    return false
end
function CRCBS.is_goal(env::E,s::State) where {E<:LowLevelEnv}
    if get_stage(s) == length(env.goal_sequence)
        if states_match(s, env.goal_sequence[get_stage(s)])
            if get_t(s) >= get_t(env.goal_sequence[get_stage(s)])
                return true
            end
        end
    end
    return false
end
function CRCBS.get_heuristic_cost(h::MultiStagePerfectHeuristic,env::E,s::State) where {E<:LowLevelEnv}
# function CRCBS.get_heuristic_cost(env::E,h::MultiStagePerfectHeuristic,s::State) where {E<:LowLevelEnv}
    get_heuristic_cost(h, env.agent_idx, s.stage, s.vtx)
end

end # end module MultiStageCBS


VERSION >= v"0.4" && __precompile__()

module OAuth

using Compat
using URIParser, Requests, Nettle

export
oauth_timestamp,
oauth_nonce,
oauth_sign_hmac_sha1,
oauth_signing_key,
oauth_signature_base_string,
oauth_percent_encode_keys!,
oauth_serialize_url_parameters,
encodeURI!,
oauth_body_hash_file,
oauth_body_hash_data,
oauth_body_hash_encode,
oauth_header,
oauth_request_resource

#############################################################
#
# OAuth Client Functions
#
#############################################################

#Get current timestamp
function oauth_timestamp()
    "$(@compat round(Int, time()))"
end

#Generate random string
function oauth_nonce(length::Int64)
    randstring(length)
end

#HMAC-SHA1 sign message
function oauth_sign_hmac_sha1(message::AbstractString,signingkey::AbstractString)
    base64encode(digest("sha1", signingkey, message))
end

#Create signing key
function oauth_signing_key(oauth_consumer_secret::AbstractString, oauth_token_secret::AbstractString)
    "$(oauth_consumer_secret)&$(oauth_token_secret)"
end

#Create signature_base_string
function oauth_signature_base_string(httpmethod::AbstractString, url::AbstractString, parameterstring::AbstractString)
    "$(httpmethod)&$(encodeURI(url))&$(encodeURI(parameterstring))"
end

#URL-escape keys
function oauth_percent_encode_keys!(options::Dict)
    #options encoded
    originalkeys = collect(keys(options))

    for key in originalkeys
        key_str = string(key)
        encoded_key = encodeURI(key_str)

        options[encoded_key] = encodeURI(options[key_str])
        if encodeURI(key_str) != key
            delete!(options, key_str)
        end
    end

    options
end

@deprecate(
    oauth_percent_encode_keys(options::Dict),
    oauth_percent_encode_keys!(options::Dict)
)

#Create query string from dictionary keys
oauth_serialize_url_parameters(options::Dict) = join(
    ["$key=$(options[key])" for key in sort!(collect(keys(options)))],
    "&"
)

# See: https://github.com/randyzwitch/OAuth.jl/issues/3
encodeURI(s) = URIParser.escape(s)

function encodeURI!(dict_of_parameters::Dict)
    for (k, v) in dict_of_parameters
        if typeof(v) <: AbstractString
            dict_of_parameters[k] = encodeURI(v)
        end
    end
    return dict_of_parameters
end

@deprecate(
    encodeURI(dict_of_parameters::Dict),
    encodeURI!(dict_of_parameters::Dict)
)

function oauth_body_hash_file(filename::AbstractString)
    oauth_body_hash_data(readall(open(filename)))
end

function oauth_body_hash_data(data::AbstractString)
    "oauth_body_hash=$(oauth_body_hash_encode(data))"
end

function oauth_body_hash_encode(data::AbstractString)
        base64encode(digest("SHA1", data))
end

#Use this function to build the header for every OAuth call
# This function assumes that options Dict has already been run through encodeURI!
function oauth_header(httpmethod, baseurl, options, oauth_consumer_key, oauth_consumer_secret, oauth_token, oauth_token_secret;
                     oauth_signature_method = "HMAC-SHA1",
                     oauth_version = "1.0")

    #keys for parameter string
    options["oauth_consumer_key"] = oauth_consumer_key
    options["oauth_nonce"] = oauth_nonce(32)
    options["oauth_signature_method"] = oauth_signature_method
    options["oauth_timestamp"] = oauth_timestamp()
    options["oauth_token"] = oauth_token
    options["oauth_version"] = oauth_version

    #options encoded
    oauth_percent_encode_keys!(options)

    #Create ordered query string
    parameterstring = oauth_serialize_url_parameters(options)

    #Calculate signature_base_string
    signature_base_string = oauth_signature_base_string(uppercase(httpmethod), baseurl, parameterstring)

    #Calculate signing_key
    signing_key = oauth_signing_key(oauth_consumer_secret, oauth_token_secret)

    #Calculate oauth_signature
    oauth_sig = encodeURI(oauth_sign_hmac_sha1(signature_base_string, signing_key))

    return "OAuth oauth_consumer_key=\"$(options["oauth_consumer_key"])\", oauth_nonce=\"$(options["oauth_nonce"])\", oauth_signature=\"$(oauth_sig)\", oauth_signature_method=\"$(options["oauth_signature_method"])\", oauth_timestamp=\"$(options["oauth_timestamp"])\", oauth_token=\"$(options["oauth_token"])\", oauth_version=\"$(options["oauth_version"])\""

end

function oauth_request_resource(endpoint::AbstractString, httpmethod::AbstractString, options::Dict, oauth_consumer_key::AbstractString, oauth_consumer_secret::AbstractString, oauth_token::AbstractString, oauth_token_secret::AbstractString)
    #Build query string
    query_str = Requests.format_query_str(options)

    #Build oauth_header
    oauth_header_val = oauth_header(httpmethod, endpoint, options, oauth_consumer_key, oauth_consumer_secret, oauth_token, oauth_token_secret)

    #Make request
    headers = @compat(
        Dict{AbstractString,AbstractString}(
            "Content-Type" => "application/x-www-form-urlencoded",
            "Authorization" => oauth_header_val,
            "Accept" => "*/*"
        )
    )

    if uppercase(httpmethod) == "POST"
        return Requests.post(URI(endpoint), query_str; headers = headers)
    elseif uppercase(httpmethod) == "GET"
        return Requests.get(URI("$(endpoint)?$query_str"); headers = headers)
    end
end

end


#!/usr/bin/env julia
using FASTX
using BioAlignments

# Read sequence dictionary for HLA
seqFile = "data/S2_callseqs.fa"
name2seq = Dict()
open(FASTA.Reader,seqFile) do reader
	for record in reader
		name2seq[FASTA.identifier(record)]=FASTA.sequence(record)
	end
end

#target = "HLA:HLA00602" # >HLA:HLA00602 DQA1*01:02:01:01 6484 bp
#chrStart=32628178 # DQA1
#chrEnd=32637531
target = "HLA:HLA00010" # >HLA:HLA00010 A*02:05:01:01 3517 bp
chrStart = 29941259
chrEnd = 29945884

hlaseq = name2seq[target]

# Read chromosome 6
chr6File= "data/chr6_hg38.fa"
chr6record = FASTA.Record()
reader = open(FASTA.Reader,chr6File)
read!(reader,chr6record)
close(reader)

# Pull out region of chromosome6
chr6seq = FASTA.sequence(chr6record)
hlaref = chr6seq[chrStart:chrEnd]

# Align that region to big HLA sequence

for j in 0:40
	scoremodel = AffineGapScoreModel(EDNAFULL, gap_open=-5, gap_extend=-1);
	res = pairalign(LocalAlignment(),hlaref,hlaseq,scoremodel)
end

#print(res)

# HLA-A
# ./cutandalign.jl  2.69s user 0.54s system 104% cpu 3.080 total

# 10X loop:
# cutandalign2.jl  3.48s

# 20X loop:
# cutandalign2.jl  4.54s

# 40X ./cutandalign2.jl  6.65s
# So 2sec = 20X extra so 10X/second steady state.

# HLA-DQA1
# ./cutandalign.jl  2.58s user 0.86s system 101% cpu 3.398 total


using Test, Onda, Random, Dates

@testset "$(repr(name)) serializer" for (name, options) in [(:lpcm, nothing),
                                         (Symbol("lpcm.zst"), Dict(:level => 2))]
    signal = Signal([:a, :b, :c], Nanosecond(0), Nanosecond(0), :unit, 0.25, -0.5, Int16,
                    50.5, name, options)
    samples = encode(Samples(signal, false, rand(MersenneTwister(1), 3, Int(50.5 * 10)))).data
    s = serializer(signal)
    bytes = serialize_lpcm(samples, s)
    name == :lpcm && @test bytes == reinterpret(UInt8, vec(samples))
    @test deserialize_lpcm(bytes, s) == samples
    @test deserialize_lpcm(IOBuffer(bytes), s) == samples
    io = IOBuffer()
    serialize_lpcm(io, samples, s)
    seekstart(io)
    @test take!(io) == bytes
end

@test_throws ArgumentError Onda.serializer_constructor_for_file_extension(Val(:extension))
@test_throws ErrorException Onda.register_file_extension_for_serializer(:extension, LPCM)


using Base: steprange_last, steprange_last_empty
using Dates: guess, len, Millisecond

function Base.:(:)(start::LaxZonedDateTime, stop::LaxZonedDateTime)
    return StepRange(start, Millisecond(1), stop)
end

"""
    guess(start::LaxZonedDateTime, finish::LaxZonedDateTime, step) -> Integer

Given a start and end date, indicates how many steps/periods are between them. Defining this
function allows `StepRange`s to be defined for `LaxZonedDateTime`s. (For non-empty
`StepRange`s, `guess` will ideally return one less than the number of elements.)
"""
function Dates.guess(start::LaxZonedDateTime, finish::LaxZonedDateTime, step)
    if isvalid(start) && isvalid(finish)
        return guess(DateTime(start, UTC), DateTime(finish, UTC), step)
    else
        return 0    # Can't easily guess. It will be calculated with len instead.
    end
end

function Dates.len(start::LaxZonedDateTime, finish::LaxZonedDateTime, step)
    !(isrepresentable(start) && isrepresentable(finish)) && return 0

    # Because of the way unrepresentable LZDTs can propagate, we need to start counting from
    # start, instead of starting from min(start, finish) and incrementing by abs(step).
    compare = (step > zero(step)) ? (<=) : (>=)

    i = guess(start, finish, step) - 1
    while isrepresentable(start + step * i) && compare(start + step * i, finish)
        i += 1
    end
    return i - 1
end

function Base.steprange_last(start::LaxZonedDateTime, step, stop::LaxZonedDateTime)
    z = zero(step)
    step == z && throw(ArgumentError("step cannot be zero"))

    if (stop == start) || !stop.representable
        last = stop
    else
        if (step > z) != (stop > start)
            last = steprange_last_empty(start, step, stop)
        else
            last = start + step * len(start, stop, step)
        end
    end
    return last
end

function Base.isempty(r::StepRange{LaxZonedDateTime})
    return !(isrepresentable(r.start) && isrepresentable(r.stop)) || (
        (r.start != r.stop) & ((r.step > zero(r.step)) != (r.stop > r.start))
    )
end

##### Support for StepRange{AnchoredInterval{LaxZonedDateTime}} #####
# Ideally this would go in Intervals.jl, but it goes here because it doesn't make sense to
# reference a private package (this one) in a public package (Intervals.jl)

function Dates.guess(start::A, finish::A, step) where A <: AnchoredInterval{P, LaxZonedDateTime} where P
    return guess(anchor(start), anchor(finish), step)
end

function Dates.len(start::A, finish::A, step) where A <: AnchoredInterval{P, LaxZonedDateTime} where P
    return len(anchor(start), anchor(finish), step)
end

function Base.steprange_last(start::A, step, stop::A) where A <: AnchoredInterval{P, LaxZonedDateTime} where P
    return AnchoredInterval{P}(steprange_last(anchor(start), step, anchor(stop)))
end

function Base.isempty(r::StepRange{A}) where A <: AnchoredInterval{P, LaxZonedDateTime} where P
    a_start, a_stop, step = anchor(r.start), anchor(r.stop), r.step
    return !(isrepresentable(a_start) && isrepresentable(a_stop)) || (
        (a_start != a_stop) & ((step > zero(step)) != (a_stop > a_start))
    )
end


function removefreevariables!(sdp::SparseSDP)

end


##########################################################################
# Copyright 2017 Samuel Ridler.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##########################################################################

# for converting OpenStreetMap files with road networks to format usable for JEMSS

using JEMSS
import OpenStreetMapX # v0.1.1
const OSM = OpenStreetMapX

include("graph_tools.jl")

# Read OSM file with road network, return nodes and arcs
# kwargs:
# - levels: highway types that will be kept, see ROAD_CLASSES in OSM for more details.
# - boundsLLA: crop map within given bounds.
# Output may need further processing, such as only keeping the largest strongly connected component, and removing duplicate arcs.
# Note that distance calculations done here are different (more accurate) than those in JEMSS.
function readOsmNetworkFile(osmFilename::String;
	levels::Union{Set{Int},Nothing} = nothing, boundsLLA::Union{OSM.Bounds{OSM.LLA},Nothing} = nothing)

	if levels == nothing levels = OSM.ROAD_CLASSES |> values |> Set end

	# read osm file
	@assert(isfile(osmFilename))
	# get osm data; this fails if the osm file does not contain bounds, e.g. <bounds minlat="1.0" minlon="2.0" maxlat="3.0" maxlon="4.0"/>
	osmData = OSM.get_map_data(osmFilename; road_levels = levels, use_cache = false) # 'use_cache = true' ignores other kwargs (e.g. road_levels)
	(nodesENU, roadways) = (osmData.nodes, osmData.roadways)
	@assert(length(nodesENU) > 0)
	@assert(length(roadways) > 0)
	@assert(typeof(first(nodesENU)[2]) == OSM.ENU)

	# crop map within bounds
	if boundsLLA == nothing
		boundsLLA = osmData.bounds
		@assert(boundsLLA != OSM.Bounds{OSM.LLA}(0.0, 0.0, 0.0, 0.0), "missing bounds")
	end
	nodesLLA = OSM.LLA(nodesENU, boundsLLA)
	OSM.crop!(nodesLLA, boundsLLA, roadways)

	# get network data from nodes and ways
	edges, class = OSM.get_edges(nodesENU, roadways) # edges = vector of tuple of vertex keys; class[i] is class (in levels) of edges[i]
	@assert(issubset(Set(class), levels))
	vertices = OSM.get_vertices(edges) # Dict{Int,Int}; vertices[k] gives index (from 1:n) of vertex key k
	weights = OSM.distance(nodesENU, edges) # weights[i] is weight of edges[i]; appears to be equivalent to Geodesy.distance, based on output

	# create nodes from vertices
	numNodes = length(vertices)
	nodes = Vector{Node}(undef, numNodes)
	for (key, i) in vertices
		nodes[i] = Node()
		nodes[i].index = i
		nodes[i].location.x = nodesLLA[key].lon
		nodes[i].location.y = nodesLLA[key].lat
		nodes[i].fields["osm_key"] = key
	end

	# create arcs from edges
	numArcs = length(edges)
	arcs = Vector{Arc}(undef, numArcs)
	for i = 1:numArcs
		v1, v2 = edges[i] # vertex keys (to, from) for edge
		arcs[i] = Arc()
		arcs[i].index = i
		arcs[i].fromNodeIndex = vertices[v1]
		arcs[i].toNodeIndex = vertices[v2]
		arcs[i].fields["osm_class"] = class[i]
		arcs[i].fields["osm_weight"] = weights[i] # edge weight = length
		arcs[i].distance = weights[i] / 1000 # convert metres to km
	end

	return nodes, arcs
end

# merge arc2 into arc1, keeping minimum class (should have higher speed) in arc1
# arc2 will need to be removed in separate step
function mergeDuplicateOsmArcs!(arc1::Arc, arc2::Arc; classFieldName::String = "osm_class")
	@assert(arc1.fromNodeIndex == arc2.fromNodeIndex)
	@assert(arc1.toNodeIndex == arc2.toNodeIndex)
	arc1.fields[classFieldName] = min(arc1.fields[classFieldName], arc2.fields[classFieldName])
	# arcs should already have same weight (length)
end
mergeDuplicateOsmArcs!(arcs::Vector{Arc}, i::Int, j::Int) = mergeDuplicateOsmArcs!(arcs[i], arcs[j])

# Process nodes and arcs from OSM, processing performed:
# - only keep specified highway types (levels)
# - keep only the largest strongly connected component
# - merge duplicate arcs (same start and end node) according to kwarg mergeArcsFunction
# - set offRoadAccess field of nodes, according to kwarg classOffRoadAccess
# - calculate travel time for each arc
# kwargs:
# - levels: highway types that will be kept, see ROAD_CLASSES in OSM for more details.
# - classSpeeds: classSpeeds[i] has dict of the speed (km/hr) of each class of arc, for travel mode i
# - classOffRoadAccess: dict indicating which nodes of different arc classes can be used to get on and off road; node will not have off-road access if all connecting arcs have class returning false in dict
# - mergeArcsFunction: function to merge any duplicate arcs found, given the arcs and two arc indices
function convertOsmNetwork!(nodes::Vector{Node}, arcs::Vector{Arc};
	levels::Union{Set{Int},Nothing} = nothing, classSpeeds::Vector{Dict{Int,Float}} = Dict{Int,Float}[], classOffRoadAccess::Dict{Int,Bool} = Dict{Int,Bool}(), mergeArcsFunction::Function = mergeDuplicateOsmArcs!)

	numTravelModes = length(classSpeeds)

	if levels == nothing levels = OSM.ROAD_CLASSES |> values |> Set end

	# if classOffRoadAccess not given, assume it is true for all levels
	if classOffRoadAccess == Dict()
		classOffRoadAccess = Dict([level => true for level in levels])
	end

	# check that input dicts contain all levels
	for level in levels
		@assert(all(c -> haskey(c, level), classSpeeds))
		@assert(haskey(classOffRoadAccess, level))
	end

	# convert classSpeeds (km/hr) to classInvSpeeds (days/metre)
	# this is needed as the arc lengths (arc field "osm_weight") are in metres
	classInvSpeeds = deepcopy(classSpeeds) # will be days / metre
	for i = 1:numTravelModes
		for (class, speed) in classSpeeds[i]
			classInvSpeeds[i][class] = 1 / (speed * 1000 * 24) # days / metre
		end
	end

	# edit nodes and arcs as needed
	graphRemoveElts!(nodes, arcs, arcFilter = arc -> in(arc.fields["osm_class"], levels)) # keep only specified levels
	graphRemoveDisconnectedArcs!(nodes, arcs) # probably not needed, but use just in case
	graphKeepLargestComponent!(nodes, arcs) # keep only the largest strongly connected component
	graphMergeDuplicateArcs!(nodes, arcs; mergeArcsFunction = mergeArcsFunction)

	# set node offRoadAccess values
	for node in nodes
		node.offRoadAccess = false
	end
	for arc in arcs
		if classOffRoadAccess[arc.fields["osm_class"]]
			nodes[arc.fromNodeIndex].offRoadAccess = true
			nodes[arc.toNodeIndex].offRoadAccess = true
		end
	end

	# set arc travel times
	for i = 1:numTravelModes
		modeFieldName = travelModeString(i)
		for arc in arcs
			class = arc.fields["osm_class"]
			arc.fields[modeFieldName] = arc.fields["osm_weight"] * classInvSpeeds[i][class]
		end
	end
end

function convertOsmNetworkExample!(nodes::Vector{Node}, arcs::Vector{Arc})
	# the 'levels' of highways that will be kept, see ROAD_CLASSES in OSM for more details
	# e.g., levels = Set(1:5) are motorway to tertiary, including links
	# levels = Set(1:6) # roads 1:6 are motorway to residential and unclassified, including links
	levels = Set(1:6)

	# arc speed (km/hr) for each travel mode and each arc class
	v = [
		[110, 110, 60, 60, 60, 60, 60, 60], # travel mode 1, classes 1:8
		[ 90, 90, 45, 45, 45, 45, 45, 45] # travel mode 2, classes 1:8
	]
	classSpeeds = [Dict([j => Float(v[i][j]) for j = 1:length(v[i])]) for i = 1:length(v)]

	# whether nodes along road can be used to get on-road and off-road
	# where set to true, will set both ends of arc to be accessible
	classOffRoadAccess = Dict(
		1 => false, # motorway + link
		2 => false, # trunk + link
		3 => true, # primary + link
		4 => true, # secondary + link
		5 => true, # tertiary + link
		6 => true, # residential + link
	)

	convertOsmNetwork!(nodes, arcs; levels=levels, classSpeeds=classSpeeds, classOffRoadAccess=classOffRoadAccess)
end

function convertOsmNetworkFile(osmFilename::String;
	levels::Union{Set{Int},Nothing} = nothing, boundsLLA::Union{OSM.Bounds{OSM.LLA},Nothing} = nothing, classSpeeds::Vector{Dict{Int,Float}} = Dict{Int,Float}[], classOffRoadAccess::Dict{Int,Bool} = Dict{Int,Bool}(), mergeArcsFunction::Function = mergeDuplicateOsmArcs!)

	(nodes, arcs) = readOsmNetworkFile(osmFilename; levels = levels, boundsLLA = boundsLLA)
	convertOsmNetwork!(nodes, arcs; levels = levels, classSpeeds = classSpeeds, classOffRoadAccess = classOffRoadAccess, mergeArcsFunction = mergeArcsFunction)
	return nodes, arcs
end

# example use of convertOsmNetwork function
function convertOsmNetworkFileExample(osmFilename::String)
	levels = Set(1:6) # roads 1:6 are motorway to residential and unclassified, including links
	(nodes, arcs) = readOsmNetworkFile(osmFilename; levels = levels)
	convertOsmNetworkExample!(nodes, arcs)
	return nodes, arcs
end


synth_paletted_imgs = [
    ("RGB_paletted", IndirectArray(rand(1:100, 127, 257), rand(RGB{N0f8}, 100))),
    ("BRG_paletted", IndirectArray(rand(1:100, 127, 257), rand(BGR{N0f8}, 100))),
    ("ARGB_paletted", IndirectArray(rand(1:100, 127, 257), rand(ARGB{N0f8}, 100))),
    ("ABGR_paletted", IndirectArray(rand(1:100, 127, 257), rand(ABGR{N0f8}, 100))),
    ("RGBA_paletted", IndirectArray(rand(1:100, 127, 257), rand(RGBA{N0f8}, 100))),
    ("RGB_paletted_float", IndirectArray(rand(1:100, 127, 257), rand(RGB{Float64}, 100))),
    ("BRG_paletted_float", IndirectArray(rand(1:100, 127, 257), rand(BGR{Float64}, 100))),
    ("ARGB_paletted_float", IndirectArray(rand(1:100, 127, 257), rand(ARGB{Float64}, 100))),
    ("ABGR_paletted_float", IndirectArray(rand(1:100, 127, 257), rand(ABGR{Float64}, 100))),
    ("RGBA_paletted_float", IndirectArray(rand(1:100, 127, 257), rand(RGBA{Float64}, 100))),
]

expected_img(x::Matrix{<:TransparentColor}) = convert(Array{RGBA{N0f8}}, x)
expected_img(x::Matrix{<:AbstractRGB}) = convert(Array{RGB{N0f8}}, x)

@testset "synthetic paletted images" begin
    for (case, image) in synth_paletted_imgs
        @testset "$(case)" begin
            expected = expected_img(collect(image))
            fpath = joinpath(PNG_TEST_PATH, "test_img_$(case).png")
            @testset "write" begin
                open(io->PNGFiles.save(io, image), fpath, "w")
                @test PNGFiles.save(fpath, image) == 0
            end
            @testset "read" begin
                global read_in_pngf = PNGFiles.load(fpath)
                @test typeof(read_in_pngf) <: AbstractMatrix
            end
            @testset "compare" begin
                @test all(expected .≈ read_in_pngf)
            end
            global read_in_immag = _standardize_grayness(ImageMagick.load(fpath))
            @testset "$(case): ImageMagick read type equality" begin
                # The lena image is Grayscale saved as RGB...
                @test eltype(_standardize_grayness(read_in_pngf)) == eltype(read_in_immag)
            end
            @testset "$(case): ImageMagick read values equality" begin
                imdiff_val = imdiff(read_in_pngf, read_in_immag)
                onfail(@test imdiff_val < 0.01) do
                    PNGFiles._inspect_png_read(fpath)
                    _add_debugging_entry(fpath, case, imdiff_val)
                end
            end
            path, ext = splitext(fpath)
            newpath = path * "_new" * ext
            PNGFiles.save(newpath, read_in_pngf)
            @testset "$(case): IO is idempotent" begin
                @test all(read_in_pngf .≈ PNGFiles.load(newpath))
                @test all(read_in_pngf .≈ open(io->PNGFiles.load(io), newpath))
            end
        end
    end
end


struct SynchronousExecution{T <: AbstractFloat,
                            A <: AbstractVector,
                            S <: LQSolver} <: AbstractExecution
    subworkers::Vector{SubWorker{T,A,S}}

    function SynchronousExecution(::Type{T}, ::Type{A}, ::Type{S}) where {T <: AbstractFloat, A <: AbstractVector, S <: LQSolver}
        return new{T,A,S}(Vector{SubWorker{T,A,S}}(undef, nworkers()))
    end
end

function init_subproblems!(ph::AbstractProgressiveHedgingSolver, subsolver::QPSolver, execution::SynchronousExecution)
    return init_subproblems!(ph, subsolver, execution.subworkers)
end

function iterate!(ph::AbstractProgressiveHedgingSolver, ::SynchronousExecution)
    return iterate_nominal!(ph)
end

function init_workers!(::AbstractProgressiveHedgingSolver, ::SynchronousExecution)
    return nothing
end

function close_workers!(::AbstractProgressiveHedgingSolver, ::SynchronousExecution)
    return nothing
end

function resolve_subproblems!(ph::AbstractProgressiveHedgingSolver, execution::SynchronousExecution{T}) where T <: AbstractFloat
    partial_objectives = Vector{T}(undef, nworkers())
    @sync begin
        for (i,w) in enumerate(workers())
            @async partial_objectives[i] = remotecall_fetch(resolve_subproblems!, w, execution.subworkers[w-1], ph.ξ, penalty(ph))
        end
    end
    return sum(partial_objectives)
end

function update_iterate!(ph::AbstractProgressiveHedgingSolver, execution::SynchronousExecution{T,A}) where {T <: AbstractFloat, A <: AbstractVector}
    partial_primals = Vector{A}(undef, nworkers())
    @sync begin
        for (i,w) in enumerate(workers())
            @async partial_primals[i] = remotecall_fetch(collect_primals, w, execution.subworkers[w-1], length(ph.ξ))
        end
    end
    ξ_prev = copy(ph.ξ)
    ph.ξ[:] = sum(partial_primals)
    # Update δ₁
    ph.data.δ₁ = norm(ph.ξ-ξ_prev, 2)^2
end

function update_subproblems!(ph::AbstractProgressiveHedgingSolver, execution::SynchronousExecution)
    # Update dual prices
    @sync begin
        for w in workers()
            @async remotecall_fetch((sw,ξ,r)->begin
                subproblems = fetch(sw)
                if length(subproblems) > 0
                    update_subproblems!(subproblems, ξ, r)
                end
                end,
                w,
                execution.subworkers[w-1],
                ph.ξ,
                penalty(ph))
        end
    end
    return nothing
end

function update_dual_gap!(ph::AbstractProgressiveHedgingSolver, execution::SynchronousExecution)
    return update_dual_gap!(ph, execution.subworkers)
end

function calculate_objective_value(ph::AbstractProgressiveHedgingSolver, execution::SynchronousExecution)
    return calculate_objective_value(ph, execution.subworkers)
end

function fill_submodels!(ph::AbstractProgressiveHedgingSolver, scenarioproblems, execution::SynchronousExecution)
    return fill_submodels!(ph, scenarioproblems, execution.subworkers)
end
# API
# ------------------------------------------------------------
struct Synchronous <: Execution end

function (execution::Synchronous)(::Type{T}, ::Type{A}, ::Type{S}) where {T <: AbstractFloat, A <: AbstractVector, S <: LQSolver}
    return SynchronousExecution(T,A,S)
end

function str(::Synchronous)
    return "synchronous execution"
end