"""
    MeshExportModule

Module for export of meshes and data defined on meshes.
"""
module MeshExportModule

module VTK
################################################################################
# VTK export
################################################################################
using ...FTypesModule: FInt, FFlt, FCplxFlt, FFltVec, FIntVec, FFltMat, FIntMat, FMat, FVec, FDataDict
import ...FESetModule: AbstractFESet, FESetP1, FESetL2, FESetT3, FESetQ4, FESetT4, FESetH8, FESetQ8, FESetL3, FESetT6, FESetT10, FESetH20, connasarray
import ...FENodeSetModule: FENodeSet
import Base.close

using Printf
import LinearAlgebra: norm, cross

const P1=1
const L2=3
const T3=5
const Q4=9
const T4=10
const H8=12
const L3=21
const T6=22
const Q8=23
const T10=24
const H20=25

VTKtypemap = Dict{DataType, Int}(FESetP1=>P1, FESetL2=>L2, FESetT3=>T3,
    FESetQ4=>Q4, FESetT4=>T4, FESetH8=>H8, FESetQ8=>Q8, FESetL3=>L3, FESetT6=>T6,
    FESetT10=>T10, FESetH20=>H20)

numnodesmap = Dict{Int, Int}(P1=>1, L2=>2, T3=>3,
    Q4=>4, T4=>4, H8=>8, Q8=>8, L3=>3, T6=>6,
    T10=>10, H20=>20)

"""
    vtkexportmesh(theFile::String, fens::FENodeSet, fes::T; opts...) where {T<:AbstractFESet}

Export mesh to a VTK 1.0 file as an unstructured grid.

`opts` = keyword argument list, where
`scalars` = array of tuples, (name, data)
`vectors` = array of tuples, (name, data)

For the `scalars`: If `data` is a vector, that data is exported as a single field.
On the other hand, if it is an 2d array, each column is exported  as a separate field.

"""
function vtkexportmesh(theFile::String, fens::FENodeSet, fes::T;
    opts...) where {T<:AbstractFESet}
    Cell_type = get(()->error("Unknown VTK type!"), VTKtypemap, typeof(fes));
    return vtkexportmesh(theFile, connasarray(fes), fens.xyz, Cell_type; opts...)
end

"""
    vtkexportmesh(theFile::String, Connectivity, Points, Cell_type;
        vectors=nothing, scalars=nothing)

Export mesh to a VTK 1.0 file as an unstructured grid.

`opts` = keyword argument list, where
`scalars` = array of tuples, (name, data)
`vectors` = array of tuples, (name, data)

For the `scalars`: If `data` is a vector, that data is exported as a single field.
On the other hand, if it is an 2d array, each column is exported  as a separate field.
"""
function vtkexportmesh(theFile::String, Connectivity, Points, Cell_type;
    vectors=nothing, scalars=nothing)
    X = Points
    if size(Points, 2) < 3
        X = zeros(size(Points,1),3)
        for j  = 1:size(Points,1)
            X[j,1:size(Points,2)] =  Points[j,:]
        end
    end
    numnodes = get(()->error("Wrong number of connected nodes!"), numnodesmap, Cell_type);
    if typeof(Connectivity[1]) <: Tuple # Vector of connectivity tuples: convert to an array
        c = fill(0, length(Connectivity), length(Connectivity[1]))
        for i = 1:length(Connectivity)
            c[i, :] = [Connectivity[i]...]
        end
        Connectivity = c
    end
    @assert numnodes == size(Connectivity,2)

    fid=open(theFile,"w");
    if (fid == -1)
        error(["Could not open " * theFile])
        return nothing
    end
    print(fid,"# vtk DataFile Version 1.0\n");
    print(fid,"FinEtools Export\n");
    print(fid,"ASCII\n");
    print(fid,"\n");
    print(fid,"DATASET UNSTRUCTURED_GRID\n");
    print(fid,"POINTS ", size(X,1), " double\n");
    for i= 1:size(X, 1)
        for j= 1:size(X,2)-1
            print(fid,X[i,j]," ");
        end
        print(fid,X[i,end],"\n");
    end
    print(fid,"\n");
    print(fid,"\n");

    print(fid,"CELLS ",size(Connectivity,1)," ",(size(Connectivity,1)*(size(Connectivity,2)+1)),"\n");
    for i= 1:size(Connectivity, 1)
        print(fid,size(Connectivity,2)," ");
        for j= 1:size(Connectivity,2)-1
            print(fid,Connectivity[i,j]-1," ");
        end
        print(fid,Connectivity[i,end]-1,"\n");
    end
    print(fid,"\n");
    print(fid,"\n");
    print(fid,"CELL_TYPES ",size(Connectivity,1),"\n");
    for i= 1:size(Connectivity,1)
        print(fid,Cell_type,"\n");
    end
    print(fid,"\n");
    print(fid,"\n");

    did_point_data = false

    # First try to write point data
    if (scalars != nothing)
        did_point_data = false
        for  sx = 1:length(scalars)
            name, data = scalars[sx]
            if (size(data, 1) == size(Points, 1)) # point data
                if (!did_point_data)
                    print(fid,"POINT_DATA ",size(data, 1),"\n");
                    did_point_data = true
                end
                if size(data, 2) > 1 # there are multiple scalar fields here
                    for i = 1:size(data, 2)
                        print(fid,"SCALARS ", name * "$(i)"," double\n");
                        print(fid,"LOOKUP_TABLE default\n");
                        for j= 1:size(data,  1)
                            print(fid,data[j, i],"\n");
                        end
                    end
                else  # there's just one scalar field here
                    print(fid,"SCALARS ", name," double\n");
                    print(fid,"LOOKUP_TABLE default\n");
                    for j= 1:size(data,  1)
                        print(fid,data[j],"\n");
                    end
                end
                print(fid,"\n");
            elseif (size(data, 1) == size(Connectivity, 1)) # cell data
                # Handled below, in the section for cell data
            end
        end
    end

    print(fid,"\n");

    if vectors != nothing
        for  vx = 1:length(vectors)
            name, data = vectors[vx]
            if (!did_point_data)
                print(fid,"POINT_DATA ",size(data, 1),"\n");
                did_point_data = true
            end
            print(fid,"VECTORS ", name," double\n");
            #print(fid,"LOOKUP_TABLE default\n");
            if size(data, 2) < 3
                X = zeros(size(data,1),3)
                for j  = 1:size(data,1)
                    X[j, 1:size(data,2)] =  data[j,:]
                end
            else
                X = data
            end
            for j= 1:size(X,1)
                k=1;
                print(fid,X[j,k]);
                for k = 2:size(X,2)
                    print(fid," ", X[j,k]);
                end
                print(fid,"\n");
            end
            print(fid,"\n");
        end
    end
    print(fid,"\n");

    # Are there any cell data?  If so, write out.
    if (scalars != nothing)
        did_cell_data = false
        for  sx = 1:length(scalars)
            name, data = scalars[sx]
            if (size(data, 1) == size(Points, 1)) # point data
                # Handled above for the case of point data
            elseif (size(data, 1) == size(Connectivity, 1)) # cell data
                if (!did_cell_data)
                    print(fid,"CELL_DATA ",size(data, 1),"\n");
                    did_cell_data = true
                end
                if size(data, 2) > 1 # there are multiple scalar fields here
                    for i = 1:size(data, 2)
                        print(fid,"SCALARS ", name * "$(i)"," double\n");
                        print(fid,"LOOKUP_TABLE default\n");
                        for j= 1:size(data,  1)
                            print(fid,data[j, i],"\n");
                        end
                    end
                else  # there's just one scalar field here
                    print(fid,"SCALARS ", name," double\n");
                    print(fid,"LOOKUP_TABLE default\n");
                    for j= 1:size(data,  1)
                        print(fid,data[j],"\n");
                    end
                end
                print(fid,"\n");
            end
        end
    end

    fid=close(fid);
    return true
end

"""
    vtkexportvectors(theFile::String, Points, vectors)

Export vector data to a VTK 1.0 file.

- `Points` = array of collection of coordinates (tuples or vectors),
- `vectors` = array of tuples, `(name, data)`, where `name` is a string, and
  `data` is array of collection of coordinates (tuples or vectors).

# Example
```
Points = [(1.0, 3.0), (0.0, -1.0)]
vectors = [("v", [(-1.0, -2.0), (1.0, 1.0)])]
vtkexportvectors("theFile.VTK", Points, vectors)
```
will produce file with
```
# vtk DataFile Version 1.0
FinEtools Export
ASCII

DATASET UNSTRUCTURED_GRID
POINTS 2 double
1.0 3.0 0.0
0.0 -1.0 0.0

POINT_DATA 2
VECTORS v double
-1.0 -2.0 0.0
1.0 1.0 0.0
```

!!! note

The filter "Glyph" must be used Paraview. Also in the drop-down
menu "Glyph mode" select "all points".
"""
function vtkexportvectors(theFile::String, Points, vectors)
    fid=open(theFile,"w");
    if (fid == -1)
        error(["Could not open " * theFile])
        return nothing
    end
    print(fid,"# vtk DataFile Version 1.0\n");
    print(fid,"FinEtools Export\n");
    print(fid,"ASCII\n");
    print(fid,"\n");
    print(fid,"DATASET UNSTRUCTURED_GRID\n");
    print(fid,"POINTS ", length(Points), " double\n");
    pt = fill(0.0, 3)
    for i = 1:length(Points)
        for k = 1:length(Points[i])
            pt[k] = Points[i][k]
        end
        for j= 1:length(pt)-1
            print(fid, pt[j]," ");
        end
        print(fid,pt[end],"\n");
    end
    print(fid,"\n");
    print(fid,"\n");

    did_point_data = false

    if vectors != nothing
        for  vx = 1:length(vectors)
            name, data = vectors[vx]
            if (!did_point_data)
                print(fid,"POINT_DATA ",length(data),"\n");
                did_point_data = true
            end
            print(fid,"VECTORS ", name," double\n");
            #print(fid,"LOOKUP_TABLE default\n");
            for i= 1:length(data)
                for k = 1:length(data[i])
                    pt[k] = data[i][k]
                end
                for j= 1:length(pt)-1
                   print(fid, pt[j]," ");
                end
                print(fid,pt[end],"\n");
            end
            print(fid,"\n");
        end
    end
    print(fid,"\n");

    fid=close(fid);
    return true
end

end # VTK

module Abaqus
################################################################################
# Abaqus export
################################################################################
using ...FTypesModule: FInt, FFlt, FCplxFlt, FFltVec, FIntVec, FFltMat, FIntMat, FMat, FVec, FDataDict
import ...FESetModule: AbstractFESet, FESetP1, FESetL2, FESetT3, FESetQ4, FESetT4, FESetH8, FESetQ8, FESetL3, FESetT6, FESetT10, FESetH20, connasarray
import ...FENodeSetModule: FENodeSet
import Base.close
using LinearAlgebra

"""
    AbaqusExporter

Export mesh to Abaqus.
"""
mutable struct AbaqusExporter
  filename::AbstractString
  ios::IO
  element_range::Tuple{Int64, Int64}
  function AbaqusExporter(filename::AbstractString)
    if match(r".*\.inp$", filename) == nothing
      filename = filename * ".inp"
    end
    ios = open(filename,"w+")
    return new(deepcopy(filename), ios, (typemax(Int64), 0))
  end
end

"""
    COMMENT(self::AbaqusExporter, Text::AbstractString)

Write out the `**` comment option.
"""
function COMMENT(self::AbaqusExporter, Text::AbstractString)
  println(self.ios, "**" * Text)
end

"""
    HEADING(self::AbaqusExporter, Text::AbstractString)

Write out the `*HEADING` option.
"""
function HEADING(self::AbaqusExporter, Text::AbstractString)
  println(self.ios, "*HEADING");
  println(self.ios, Text)
end

"""
    PART(self::AbaqusExporter, NAME::AbstractString)

Write out the `*PART` option.
"""
function PART(self::AbaqusExporter, NAME::AbstractString)
  println(self.ios, "*PART, NAME=" * NAME)
end

"""
    END_PART(self::AbaqusExporter)

Write out the `*END PART` option.
"""
function END_PART(self::AbaqusExporter)
  println(self.ios, "*END PART")
end

"""
    ASSEMBLY(self::AbaqusExporter, NAME::AbstractString)

Write out the `*ASSEMBLY` option.
"""
function ASSEMBLY(self::AbaqusExporter, NAME::AbstractString)
  println(self.ios, "*ASSEMBLY, NAME=" * NAME)
end

"""
    END_ASSEMBLY(self::AbaqusExporter)

Write out the `*END ASSEMBLY` option.
"""
function END_ASSEMBLY(self::AbaqusExporter)
  println(self.ios, "*END ASSEMBLY")
end

"""
    INSTANCE(self::AbaqusExporter, NAME::AbstractString, PART::AbstractString)

Write out the `*INSTANCE` option.
"""
function INSTANCE(self::AbaqusExporter, NAME::AbstractString, PART::AbstractString)
  println(self.ios, "*INSTANCE, NAME=" * NAME * ", PART=" * PART)
end

"""
    END_INSTANCE(self::AbaqusExporter)

Write out the `*END INSTANCE` option.
"""
function END_INSTANCE(self::AbaqusExporter)
  println(self.ios, "*END INSTANCE")
end

"""
    NODE(self::AbaqusExporter, xyz::AbstractArray{T, 2}) where {T}

Write out the `*NODE` option.

`xyz`=array of node coordinates
"""
function NODE(self::AbaqusExporter, xyz::AbstractArray{T, 2}) where {T}
  println(self.ios, "*NODE")
  for j = 1:size(xyz,1)
    print(self.ios, "$j,$(xyz[j, 1])")
    for ixxxx = 2:1:size(xyz,2)
      print(self.ios, ",$(xyz[j, ixxxx])")
    end
    print(self.ios, "\n")
  end
end

"""
    ELEMENT(self::AbaqusExporter, TYPE::AbstractString, ELSET::AbstractString,
      start::Integer, conn::AbstractArray{T, 2}) where {T<:Integer}

Write out the `*ELEMENT` option.

`TYPE`= element type code,
`ELSET`= element set to which the elements belong,
`start`= start the element numbering at this integer,
`conn`= connectivity array for the elements, one row per element
"""
function ELEMENT(self::AbaqusExporter, TYPE::AbstractString, ELSET::AbstractString,
    start::Integer, conn::AbstractArray{T, 2}) where {T<:Integer}
    # Check that start is valid
    @assert start > 0 "The  starting element number must be > 0"
    # Check that the current element number range is disjoint from the
    # range of the elements on input: they must not overlap
    @assert (self.element_range[2] < start) ||
            (self.element_range[1] > start+size(conn, 1)-1) "Elements  must be given unique numbers"
    # Update the element range
    self.element_range = (min(self.element_range[1], start),
                            max(self.element_range[2], start+size(conn, 1)-1))
    println(self.ios, "*ELEMENT, TYPE =" * TYPE * ", ELSET=" * ELSET)
    for j=1:size(conn,1)
        print(self.ios, "$(j+start-1),")
        for ixxxx = 1:size(conn,2)-1
        if ixxxx>15
            print(self.ios, "\n")
        end
        print(self.ios, "$(conn[j,ixxxx]),")
        end
        print(self.ios, "$(conn[j,size(conn,2)])\n")
    end
end

function ELEMENT(self::AbaqusExporter, TYPE::AbstractString, ELSET::AbstractString,
    conn::AbstractArray{T, 2}) where {T<:Integer}
    start = self.element_range[2] + 1
    ELEMENT(self, TYPE, ELSET, start, conn)
end

function ELEMENT(self::AbaqusExporter, TYPE::AbstractString, ELSET::AbstractString,
    start::Integer, conn::C) where {C}
    @assert typeof(conn[1])<:Tuple
    c = fill(0, length(conn), length(conn[1]))
    for i = 1:length(conn)
        c[i, :] = [conn[i]...]
    end
    ELEMENT(self, TYPE, ELSET, start, c)
end

function ELEMENT(self::AbaqusExporter, TYPE::AbstractString, ELSET::AbstractString,
    conn::C) where {C}
    start = self.element_range[2] + 1
    ELEMENT(self, TYPE, ELSET, start, c)
end

"""
    NSET_NSET(self::AbaqusExporter, NSET::AbstractString,
      n::AbstractVector{T}) where {T<:Integer}

Write out the `*NSET` option.

`NSET` = name of the set,
`n` = array of the node numbers
"""
function NSET_NSET(self::AbaqusExporter, NSET::AbstractString, n::AbstractVector{T}) where {T<:Integer}
  println(self.ios, "*NSET, NSET=" * NSET)
  for j=1:length(n)-1
    println(self.ios, "$(n[j]),")
  end
  println(self.ios, "$(n[length(n)])")
end

"""
    ELSET_ELSET(self::AbaqusExporter, ELSET::AbstractString, n::AbstractArray{T, 1}) where {T<:Integer}

Write out the `*ELSET` option.

`ELSET` = name of the set,
`n` = array of the node numbers
"""
function ELSET_ELSET(self::AbaqusExporter, ELSET::AbstractString, n::AbstractArray{T, 1}) where {T<:Integer}
  println(self.ios, "*ELSET, ELSET=" * ELSET)
  for j=1:length(n)-1
    println(self.ios, "$(n[j]),")
  end
  println(self.ios, "$(n[length(n)])")
end

"""
    ORIENTATION(self::AbaqusExporter, ORIENTATION::AbstractString,
      a::AbstractArray{T,1}, b::AbstractArray{T,1})

Write out the `*ORIENTATION` option.

Invoke at level: Part,  Part instance,  Assembly
"""
function ORIENTATION(self::AbaqusExporter, ORIENTATION::AbstractString,
  a::AbstractArray{T,1}, b::AbstractArray{T,1}) where {T<:Real}
  println(self.ios, "*ORIENTATION,NAME=" * ORIENTATION)
  println(self.ios, "$(a[1]),$(a[2]),$(a[3]),$(b[1]),$(b[2]),$(b[3])")
  println(self.ios, "1,0.0")
end

"""
    MATERIAL(self::AbaqusExporter, MATERIAL::AbstractString)

Write out the `*MATERIAL` option.
"""
function MATERIAL(self::AbaqusExporter, MATERIAL::AbstractString)
  println(self.ios, "*MATERIAL,NAME=" * MATERIAL);
end

"""
    ELASTIC(self::AbaqusExporter, E1::F, E2::F, E3::F, nu12::F, nu13::F, nu23::F,
      G12::F, G13::F, G23::F) where {F}

Write out the `*ELASTIC,TYPE=ENGINEERING CONSTANTS` option.
"""
function ELASTIC(self::AbaqusExporter, E1::F, E2::F, E3::F, nu12::F, nu13::F, nu23::F,
  G12::F, G13::F, G23::F) where {F}
  println(self.ios, "*ELASTIC,TYPE=ENGINEERING CONSTANTS");
  println(self.ios, "$E1,$E2,$E3,$nu12,$nu13,$nu23,$G12,$G13,");
  println(self.ios, "$G23");
end

"""
    ELASTIC(self::AbaqusExporter, E::F, nu::F) where {F}

Write out the `*ELASTIC,TYPE=ISOTROPIC` option.
"""
function ELASTIC(self::AbaqusExporter, E::F, nu::F) where {F}
  println(self.ios, "*ELASTIC,TYPE=ISOTROPIC ");
  println(self.ios, "$E,$nu");
end

"""
    EXPANSION(self::AbaqusExporter, CTE::F) where {F}

Write out the `*EXPANSION` option.
"""
function EXPANSION(self::AbaqusExporter, CTE::F) where {F}
  println(self.ios, "*EXPANSION");
  println(self.ios, "$CTE,");
end

"""
    DENSITY(self::AbaqusExporter, rho)

Write out the `*DENSITY` option.
"""
function DENSITY(self::AbaqusExporter, rho)
  println(self.ios, "*DENSITY ");
  println(self.ios, "$rho");
end

"""
    SECTION_CONTROLS(self::AbaqusExporter, NAME::AbstractString,
      OPTIONAL::AbstractString)

Write out the `*SECTION CONTROLS` option.

`OPTIONAL` = string, for instance
                     HOURGLASS=ENHANCED
"""
function SECTION_CONTROLS(self::AbaqusExporter, NAME::AbstractString,
  OPTIONAL::AbstractString)
  println(self.ios, "*SECTION CONTROLS, NAME=" * NAME * "," * OPTIONAL);
end

"""
    SOLID_SECTION(self::AbaqusExporter, MATERIAL::AbstractString,
      ORIENTATION::AbstractString, ELSET::AbstractString,
      CONTROLS::AbstractString)

Write out the `*SOLID SECTION` option.

Level: Part,  Part instance

"""
function SOLID_SECTION(self::AbaqusExporter, MATERIAL::AbstractString,
  ORIENTATION::AbstractString, ELSET::AbstractString,
  CONTROLS::AbstractString)
  println(self.ios, "*SOLID SECTION,MATERIAL=" * MATERIAL *
      ",ORIENTATION =" * ORIENTATION * ",ELSET=" * ELSET *
      ", CONTROLS  =" * CONTROLS);
end

"""
    SOLID_SECTION(self::AbaqusExporter, MATERIAL::AbstractString,
      ORIENTATION::AbstractString, ELSET::AbstractString)

Write out the `*SOLID SECTION` option.

Level: Part,  Part instance

"""
function SOLID_SECTION(self::AbaqusExporter, MATERIAL::AbstractString,
  ORIENTATION::AbstractString, ELSET::AbstractString)
  println(self.ios, "*SOLID SECTION,MATERIAL=" * MATERIAL *
      ",ORIENTATION =" * ORIENTATION * ",ELSET=" * ELSET);
end

"""
    SOLID_SECTION(self::AbaqusExporter, MATERIAL::AbstractString,
      ORIENTATION::AbstractString, ELSET::AbstractString)

Write out the `*SOLID SECTION` option.

Level: Part,  Part instance

"""
function SOLID_SECTION(self::AbaqusExporter, MATERIAL::AbstractString,
  ORIENTATION::AbstractString, ELSET::AbstractString, thickness::F) where {F}
  SOLID_SECTION(self, MATERIAL, ORIENTATION, ELSET)
  println(self.ios, "$(thickness),");
end

# """
#     HOURGLASS(self::AbaqusExporter, KIND::AbstractString, VALUE::F) where {F}
#
# Write out the `*HOURGLASS` option.
#
# ```
# Example:
# *SOLID SECTION,ELSET=SOLID3,MATERIAL=MAT,CONTROL=Ac
# *HOURGLASS STIFFNESS
# 5.E8
# ```
# """
# function HOURGLASS(self::AbaqusExporter, KIND::AbstractString, VALUE::F) where {F}
#   println(self.ios, "*HOURGLASS " * KIND);
#   println(self.ios, "$( VALUE )");
# end

"""
    SURFACE_SECTION(self::AbaqusExporter, ELSET::AbstractString)

Write out the `*SURFACE SECTION` option.
"""
function SURFACE_SECTION(self::AbaqusExporter, ELSET::AbstractString)
  println(self.ios, "*SURFACE SECTION, ELSET=" * ELSET);
end

"""
    STEP_PERTURBATION_STATIC(self::AbaqusExporter)

Write out the `*STEP,PERTURBATION` option for linear static analysis.
"""
function STEP_PERTURBATION_STATIC(self::AbaqusExporter)
  println(self.ios, "*STEP,PERTURBATION");
  println(self.ios, "*STATIC");
end

"""
    STEP_PERTURBATION_BUCKLE(self::AbaqusExporter, neigv::Integer)

Write out the `*STEP,PERTURBATION` option for linear buckling analysis.
"""
function STEP_PERTURBATION_BUCKLE(self::AbaqusExporter, neigv::Integer)
  println(self.ios, "*STEP, name=Buckling, nlgeom=NO, perturbation");
  println(self.ios, "*BUCKLE");
  println(self.ios, "$(neigv), , , , ");
end

"""
    STEP_FREQUENCY(self::AbaqusExporter, Nmodes::Integer)

Write out the `*STEP,FREQUENCY` option.
"""
function STEP_FREQUENCY(self::AbaqusExporter, Nmodes::Integer)
  println(self.ios, "*STEP");
  println(self.ios, "*FREQUENCY, EIGENSOLVER=LANCZOS");
  println(self.ios, "$(Nmodes), , ,-1.E6 \n");
end

"""
    BOUNDARY(self::AbaqusExporter, mesh, nodes, is_fixed::AbstractArray{B,2}, fixed_value::AbstractArray{F,2}) where {B, F}

Write out the `*BOUNDARY` option.

The boundary condition is applied to the nodes specified in
the array `nodes`, in the mesh (or node set) `meshornset`.

`meshornset` = mesh or node set (default is empty)
`nodes` = array of node numbers, the node numbers are attached to the mesh label,
`is_fixed`= array of Boolean flags (true means fixed, or prescribed),  one row per node,
`fixed_value`=array of displacements to which the corresponding displacement components is fixed

# Example

```
BOUNDARY(AE, "ASSEM1.INSTNC1", 1:4, fill(true, 4, 1), reshape([uy(fens.xyz[i, :]...) for i in 1:4], 4, 1))
```
"""
function BOUNDARY(self::AbaqusExporter, meshornset, nodes, is_fixed::AbstractArray{B,2}, fixed_value::AbstractArray{F,2}) where {B, F}
	println(self.ios, "*BOUNDARY");
	if meshornset == ""
		meshlabel = ""
	else
		meshlabel = meshornset * "."
	end
	for j in nodes
		for k=1:size(is_fixed,2)
			#<node number>, <first dof>, <last dof>, <magnitude of displacement>
			if is_fixed[j,k]
				println(self.ios, "$(meshlabel)$(j),$k,$k,$(fixed_value[j,k])");
			end
		end
	end
end

function BOUNDARY(self::AbaqusExporter, nodes, is_fixed::AbstractVector{B},  fixed_value::AbstractVector{F}) where {B, F}
	BOUNDARY(self, nodes, reshape(is_fixed, length(is_fixed), 1), reshape(fixed_value, length(fixed_value), 1))
end

"""
    BOUNDARY(self::AbaqusExporter, meshornset, is_fixed::AbstractArray{B,2}, fixed_value::AbstractArray{F,2}) where {B, F}

Write out the `*BOUNDARY` option.

`meshornset` = name of a mesh or a node set,
`is_fixed`= array of Boolean flags (true means fixed, or prescribed),  one row per node, as many columns as there are degrees of freedom per node,
`fixed_value`=array of displacements to which the corresponding displacement components is fixed, as many columns as there are degrees of freedom per node
"""
function BOUNDARY(self::AbaqusExporter, meshornset, is_fixed::AbstractArray{B,2}, fixed_value::AbstractArray{F,2}) where {B, F}
	BOUNDARY(self, meshornset, 1:size(is_fixed, 1), is_fixed, fixed_value)
end

"""
    BOUNDARY(self::AbaqusExporter, NSET::AbstractString, dof::Integer,  fixed_value::FFlt)

Write out the `*BOUNDARY` option.

`NSET` = name of a node set,
`is_fixed`= array of Boolean flags (true means fixed, or prescribed),  one row per node,
`fixed_value`=array of displacements to which the corresponding displacement components is fixed
"""
function BOUNDARY(self::AbaqusExporter, NSET::AbstractString, dof::Integer,  fixed_value::FFlt)
	println(self.ios, "*BOUNDARY");
	println(self.ios, NSET * ",$dof,$dof,$(fixed_value)");
end

"""
    BOUNDARY(self::AbaqusExporter, NSET::AbstractString, dof::Integer)

Write out the `*BOUNDARY` option to fix displacements at zero for a node set.

Invoke at Level: Model,  Step

`NSET`= node set,
`dof`=Degree of freedom, 1, 2, 3
"""
function BOUNDARY(self::AbaqusExporter, NSET::AbstractString, dof::Integer)
	BOUNDARY(self, NSET, dof,  0.0)
end

"""
    BOUNDARY(self::AbaqusExporter, NSET::AbstractString, dof::Integer,
      value::F) where {F}

Write out the `*BOUNDARY` option to fix displacements at nonzero value for a
node set.

`NSET`= node set,
`dof`=Degree of freedom, 1, 2, 3
"""
function BOUNDARY(self::AbaqusExporter, NSET::AbstractString, dof::Integer, value::F) where {F}
	println(self.ios, "*BOUNDARY,TYPE=DISPLACEMENT");
	println(self.ios, NSET * ",$dof,$dof,$value");
end

"""
    DLOAD(self::AbaqusExporter, ELSET::AbstractString,
      traction::AbstractVector{F}) where {F}

Write out the `*DLOAD` option.
"""
function DLOAD(self::AbaqusExporter, ELSET::AbstractString, traction::AbstractVector{F}) where {F}
	println(self.ios, "*DLOAD, follower=NO");
	nt = norm(traction)
	print(self.ios, ELSET * ",TRVEC," * "$(nt),")
	println(self.ios, "$(traction[1]/nt),$(traction[2]/nt),$(traction[3]/nt)");
end

"""
    CLOAD(self::AbaqusExporter, NSET::AbstractString, dof::Integer,
      magnitude::F) where {F}

Write out the `*CLOAD` option.

NSET=Node set
dof= 1, 2, 3,
magnitude= signed multiplier
"""
function CLOAD(self::AbaqusExporter, NSET::AbstractString, dof::Integer,
  magnitude::F) where {F}
  println(self.ios, "*CLOAD");
  println(self.ios, NSET * ",$dof,$magnitude");
end

"""
    CLOAD(self::AbaqusExporter, nodenumber::Integer, dof::Integer,
      magnitude::F) where {F}

Write out the `*CLOAD` option.

nodenumber=Number of node
dof= 1, 2, 3,
magnitude= signed multiplier
"""
function CLOAD(self::AbaqusExporter, nodenumber::Integer, dof::Integer,
  magnitude::F) where {F}
  println(self.ios, "*CLOAD");
  println(self.ios, "$(nodenumber),$dof,$magnitude");
end

"""
    TEMPERATURE(self::AbaqusExporter, nlist::AbstractArray{I, 1},
      tlist::AbstractArray{F, 1}) where {I, F}

Write out the `*TEMPERATURE` option.
"""
function TEMPERATURE(self::AbaqusExporter, Classifier::AbstractString,
  nlist::AbstractArray{I, 1},
  tlist::AbstractArray{F, 1}) where {I, F}
  @assert length(nlist) == length(tlist)
  println(self.ios, "*TEMPERATURE");
  for ixxxx = 1:length(nlist)
    println(self.ios, Classifier * "$(nlist[ixxxx]),$(tlist[ixxxx])");
  end
end

"""
    NODE_PRINT(self::AbaqusExporter, NSET::AbstractString)

Write out the `*NODE PRINT` option.
"""
function NODE_PRINT(self::AbaqusExporter, NSET::AbstractString; KEYS = "U")
  println(self.ios, "*NODE PRINT, NSET=" * NSET);
  println(self.ios, KEYS);
end

"""
    EL_PRINT(self::AbaqusExporter, ELSET::AbstractString, KEYS::AbstractString)

Write out the `*EL PRINT` option.
"""
function EL_PRINT(self::AbaqusExporter, ELSET::AbstractString, KEYS::AbstractString)
  println(self.ios, "*EL PRINT, ELSET=" * ELSET * ", POSITION=INTEGRATION POINTS, SUMMARY= YES");
  println(self.ios, "");
  println(self.ios, KEYS);
end

"""
    ENERGY_PRINT(self::AbaqusExporter)

Write out the `*ENERGY PRINT` option.
"""
function ENERGY_PRINT(self::AbaqusExporter)
  println(self.ios, "*ENERGY PRINT");
end

"""
    END_STEP(self::AbaqusExporter)

Write out the `*END STEP` option.
"""
function END_STEP(self::AbaqusExporter)
  println(self.ios, "*END STEP");
end

"""
    close(self::AbaqusExporter)

Close  the stream opened by the exporter.
"""
function close(self::AbaqusExporter)
  close(self.ios)
end

end # Abaqus

module CSV
################################################################################
# CSV export
################################################################################

"""
    savecsv(name::String; kwargs...)

Save arrays as a CSV file.
"""
function savecsv(name::String; kwargs...)
    colnames = Symbol[]
    columns = Any[]
    for apair in pairs(kwargs)
        sy, val = apair
        push!(colnames, sy)
        push!(columns, val)
    end
    ncol = length(colnames)
    nrow = length(columns[1])
    for j = 1:ncol
        @assert length(columns[j]) == nrow "Columns must have the same number of rows"
    end
    if !occursin(r"^.*\.csv$", name) && !occursin(r"^.*\.CSV$", name)
        name = name * ".csv"
    end
    open(name, "w") do fid
        for (j, c) in enumerate(colnames)
            print(fid, "\"", c, "\"")
            j < ncol && print(fid, ",")
        end
        print(fid, "\n")
        for r = 1:nrow
            for j = 1:ncol
                print(fid, columns[j][r])
                j < ncol && print(fid, ",")
            end
            print(fid, "\n")
        end
        print(fid, "\n")
    end
    return true
end

end # CSV

module NASTRAN
################################################################################
# NASTRAN export
################################################################################
using ...FTypesModule: FInt, FFlt, FCplxFlt, FFltVec, FIntVec, FFltMat, FIntMat, FMat, FVec, FDataDict
import ...FESetModule: AbstractFESet, FESetP1, FESetL2, FESetT3, FESetQ4, FESetT4, FESetH8, FESetQ8, FESetL3, FESetT6, FESetT10, FESetH20, connasarray
import ...FENodeSetModule: FENodeSet
import Base.close
using Printf

"""
NASTRANExporter

Export mesh to Abaqus.
"""
mutable struct NASTRANExporter
    filename::AbstractString
    ios::IO
    element_range::Tuple{Int64, Int64}
    function NASTRANExporter(filename::AbstractString)
        if match(r".*\.nas$", filename) == nothing
            filename = filename * ".nas"
        end
        ios = open(filename,"w+")
        return new(deepcopy(filename), ios, (typemax(Int64), 0))
    end
end

NASTRANtypemap = Dict{DataType, AbstractString}(FESetT4=>"CTETRA", FESetT10=>"CTETRA")

"""
    CEND(self::NASTRANExporter)

Terminate the Executive Control section.
"""
function CEND(self::NASTRANExporter)
    println(self.ios, "CEND")
end

"""
    BEGIN_BULK(self::NASTRANExporter)

Terminate the Case Control section by starting the bulk section.
"""
function BEGIN_BULK(self::NASTRANExporter)
    println(self.ios, "BEGIN BULK")
end

"""
    ENDDATA(self::NASTRANExporter)

Terminate the bulk section.
"""
function ENDDATA(self::NASTRANExporter)
    println(self.ios, "ENDDATA")
end

"""
    GRID(self::NASTRANExporter, n::Int, xyz::Vector{FFlt})

Write a grid-point statement.
"""
function GRID(self::NASTRANExporter, n::Int, xyz::Vector{FFlt})
    @printf self.ios "GRID,%d,,%g,%g,%g\n" n xyz[1] xyz[2] xyz[3]
end

"""
    PSOLID(self::NASTRANExporter, pid::Int, mid::Int)

Write solid-property statement.
"""
function PSOLID(self::NASTRANExporter, pid::Int, mid::Int)
    @printf self.ios "PSOLID,%d,%d\n" pid mid
end

"""
    MAT1(self::NASTRANExporter, mid::Int, E::FFlt, G::FFlt, nu::FFlt, rho::FFlt, A::FFlt, TREF::FFlt, GE::FFlt)

Write a statement for an isotropic elastic material.
"""
function MAT1(self::NASTRANExporter, mid::Int, E::FFlt, G::FFlt, nu::FFlt, rho::FFlt, A::FFlt, TREF::FFlt, GE::FFlt)
    @printf self.ios "MAT1,%d,%g,%g,%g,%g,%g,%g,%g\n" mid E G nu rho A TREF GE
end

"""
    MAT1(self::NASTRANExporter, mid::Int, E::FFlt, nu::FFlt, rho::FFlt = 0.0, A::FFlt = 0.0, TREF::FFlt = 0.0)

Write a statement for an isotropic elastic material.
"""
function MAT1(self::NASTRANExporter, mid::Int, E::FFlt, nu::FFlt, rho::FFlt = 0.0, A::FFlt = 0.0, TREF::FFlt = 0.0, GE::FFlt = 0.0)
    @printf self.ios "MAT1,%d,%g,,%g,%g,%g,%g,%g\n" mid E nu rho A TREF GE
end

function CTETRA(self::NASTRANExporter, eid::Int, pid::Int, conn::Vector{Int})
    nc = length(conn)
    @printf self.ios "CTETRA,%d,%d" eid pid
    for k = 1:nc
        @printf self.ios ",%d" conn[k]
        if k == 6
            @printf self.ios "\n"
        end
    end
    @printf self.ios "\n"
end

"""
    close(self::NASTRANExporter)

Close  the stream opened by the exporter.
"""
function close(self::NASTRANExporter)
    close(self.ios)
end

end # NASTRAN

module STL
################################################################################
# STL export
################################################################################
using ...FTypesModule: FInt, FFlt, FCplxFlt, FFltVec, FIntVec, FFltMat, FIntMat, FMat, FVec, FDataDict
using Printf
import LinearAlgebra: norm, cross
import Base.close

"""
STLExporter

Export surface mesh as STL file.
"""
mutable struct STLExporter
    filename::AbstractString
    ios::IO
    element_range::Tuple{Int64, Int64}
    function STLExporter(filename::AbstractString)
        if match(r".*\.stl$", filename) == nothing
            filename = filename * ".stl"
        end
        ios = open(filename,"w+")
        return new(deepcopy(filename), ios, (typemax(Int64), 0))
    end
end

function solid(self::STLExporter, name::AbstractString = "thesolid")
    @printf self.ios "solid %s\n" name
end

function facet(self::STLExporter, v1::Vector{FFlt}, v2::Vector{FFlt}, v3::Vector{FFlt})
    V = v2 - v1
    W = v3 - v1
    normal = cross(V, W)
    normal = normal / norm(normal)
    @printf self.ios "facet normal %f %f %f\n" normal[1] normal[2] normal[3]
    @printf self.ios "    outer loop\n"
    @printf self.ios "        vertex %f %f %f\n" v1[1] v1[2] v1[3]
    @printf self.ios "        vertex %f %f %f\n" v2[1] v2[2] v2[3]
    @printf self.ios "        vertex %f %f %f\n" v3[1] v3[2] v3[3]
    @printf self.ios "    endloop\n"
    @printf self.ios "endfacet\n"
end

function endsolid(self::STLExporter, name::AbstractString = "thesolid")
    @printf self.ios "endsolid %s\n" name
end

"""
    close(self::STLExporter)

Close  the stream opened by the exporter.
"""
function close(self::STLExporter)
    close(self.ios)
end

end # STL

module H2Lib
################################################################################
# H2Lib triangular-surface export
################################################################################
using Printf
import Base.BitSet

mutable struct _HypFace
    n::Int # serial number of the hyperface
    hf::Array{Int} # numbers of vertices defining the hyperface
    a::Int # anchor node number
    o::BitSet # numbers of the other nodes on the hyperface
end

mutable struct _HypFaceContainer
    nv::Int # total number of vertices per hyperface
    d::Dict{Int, Array{_HypFace}} # dictionary
    n::Int # total number of hyperfaces in the container
end

makehypfacedict(nv) = _HypFaceContainer(nv, Dict{Int, Array{_HypFace}}(), 0);

function hypfacen(container, hypf)
    h = sort([i for i in hypf])
    anchor = h[1]; other = BitSet(h[2:end]);
    ca = get(container.d, anchor, _HypFace[]);
    for k = 1:length(ca)
        if (ca[k].a == anchor) && (ca[k].o == other)
            return ca[k].n # do have this hyper face: here is its serial number
        end
    end
    return 0 # do not have this hyper face
end

function inserthypface!(container, hypf)
    if hypfacen(container, hypf) == 0
        @assert length(hypf) == container.nv "Hyperface does not have the right number of vertices"
        h = sort([i for i in hypf])
        anchor = h[1]; other = BitSet(h[2:end]);
        ca = get(container.d, anchor, _HypFace[]);
        container.n = container.n + 1
        push!(ca, _HypFace(container.n, hypf, anchor, other));
        container.d[anchor] = ca
    end
    return container
end

function hypfacedicttoarray(container)
    a = fill(0, container.n, container.nv)
    for ca in values(container.d)
        for i = 1:length(ca)
            a[ca[i].n, :] = ca[i].hf
        end
    end
    return a
end

function h2libexporttri(theFile::String, Connectivity, Points)
    @assert size(Connectivity, 2) == 3 "Only triangles accepted"
    adjust = 1 # the library is C-based and expects indexes to be zero-based

    ed = makehypfacedict(2)
    for i = 1:size(Connectivity, 1)
        inserthypface!(ed, Connectivity[i, [1, 2]])
        inserthypface!(ed, Connectivity[i, [2, 3]])
        inserthypface!(ed, Connectivity[i, [3, 1]])
    end
    ea = hypfacedicttoarray(ed)

    fid=open(theFile,"w");
    if (fid == -1)
        error(["Could not open " * theFile])
        return nothing
    end

    print(fid, size(Points, 1), " ", size(ea, 1), " ", size(Connectivity, 1), "\n");

    for i= 1:size(Points, 1)
        for j= 1:size(Points,2)-1
            print(fid,Points[i,j]," ");
        end
        print(fid,Points[i,end],"\n");
    end

    for i= 1:size(ea, 1)
        for j= 1:size(ea,2)-1
            print(fid, ea[i,j]-adjust, " ");
        end
        print(fid, ea[i,end]-adjust, "\n");
    end

    for i= 1:size(Connectivity, 1)
        s = [hypfacen(ed, Connectivity[i, [2, 3]]) hypfacen(ed, Connectivity[i, [3, 1]]) hypfacen(ed, Connectivity[i, [1, 2]])]
        for j= 1:size(Connectivity,2)
            print(fid, Connectivity[i,j]-adjust, " ");
        end
        for j= 1:length(s)-1
            print(fid, s[j]-adjust, " ");
        end
        print(fid, s[end]-adjust, "\n");
    end

    fid=close(fid);
    return true
end

end # H2Lib

module MESH
################################################################################
# MESH export
################################################################################
using DelimitedFiles
using ...FTypesModule: FInt, FFlt, FCplxFlt, FFltVec, FIntVec, FFltMat, FIntMat, FMat, FVec, FDataDict
import ...FESetModule: AbstractFESet, FESetP1, FESetL2, FESetT3, FESetQ4, FESetT4, FESetH8, FESetQ8, FESetL3, FESetT6, FESetT10, FESetH20, connasarray
import ...FENodeSetModule: FENodeSet

MESHtypemap = Dict{DataType, String}(FESetP1=>"P1", FESetL2=>"L2", FESetT3=>"T3",
    FESetQ4=>"Q4", FESetT4=>"T4", FESetH8=>"H8", FESetQ8=>"Q8", FESetL3=>"L3", FESetT6=>"T6",
    FESetT10=>"T10", FESetH20=>"H20")

function write_MESH(meshfile::String, fens::FENodeSet, fes::T) where {T<:AbstractFESet}
    meshfilebase, ext = splitext(meshfile)
    dinfo = [
    meshfilebase * "-xyz.dat",
    MESHtypemap[typeof(fes)],
    meshfilebase * "-conn.dat"]
    open(dinfo[1], "w") do file
        writedlm(file, fens.xyz, ' ')
    end
    open(dinfo[3], "w") do file
        writedlm(file, connasarray(fes), ' ')
    end
    open(meshfilebase * ".mesh", "w") do file
        writedlm(file, dinfo)
    end
    return true
end

end

end


# Helper functions for inferring a network from a data file

"""
    get_nodes(data_file_path::String; <keyword arguments>)

Gets an array of all Nodes from a data file. It is assumed that the first
line of the file is headers (which are discarded) and the subsequent lines
each represent one node, and are of the form:

Label    data_value1  data_value2 ...

though a different delimiter may be specified.

Arguments:
* `data_file_path`: path to the data file
* `delim=false`: the file's delimiter. Leave as false if it is whitespace
* `discretizer="bayesian_blocks"`: algorithm for discretizing the data
* `estimator="maximum_likelihood"`: algorithm for estimating probabilities
* `number_of_bins=10`: will be overwritten if using "bayesian_blocks"
* `get_values`: if true, will return an array with measurement values for use in e.g. getting correlation

The "maximum_likelihood" estimator is recommended for PUC and PIDC.
"""
function get_nodes(data_file_path::String; delim::Union{Char,Bool} = false, discretizer = "bayesian_blocks",
    estimator = "maximum_likelihood", number_of_bins = 10, get_values = false)

    if delim == false
        lines = readdlm(open(data_file_path); skipstart = 1)
    else
        lines = readdlm(open(data_file_path), delim; skipstart = 1)
    end
    number_of_nodes = size(lines, 1)
    nodes = Array{Node}(undef, number_of_nodes)

    if !get_values
        for i in 1:number_of_nodes
            nodes[i] = Node(lines[i,:], discretizer, estimator, number_of_bins)
        end
        return nodes
    else
        values = Array{Float64}(undef,number_of_nodes,size(lines,2)-1)
        for i in 1:number_of_nodes
            nodes[i] = Node(lines[i,:], discretizer, estimator, number_of_bins)
            values[i,:] = lines[i,2:end]
        end
        return nodes, values
    end

end

"""
    write_network_file(file_path::String, inferred_network::InferredNetwork)

Writes a network file from an InferredNetwork type. Each line of the file
will contain an edge, and since networks are assumed undirected, each edge
will be written in both directions with the same weight:

...

LabelX   LabelY  WeightXY

LabelY   LabelX  WeightXY

...

Arguments:
* `file_path`: path to the output file
* `inferred_network`: network that was inferred
"""
function write_network_file(file_path::String, inferred_network::InferredNetwork)

    out_file = open(file_path, "w")

    for edge in inferred_network.edges
        nodes = edge.nodes
        write(out_file, string(
            nodes[1].label, "\t", nodes[2].label, "\t",
            edge.weight, "\n",
            nodes[2].label, "\t", nodes[1].label, "\t",
            edge.weight, "\n"
        ))
    end

    close(out_file)

end

"""
    read_network_file(file_path::AbstractString)

Reads a network file and creates an InferredNetwork type. Assumes that the input
is such that each line contains an edge and each edge is written in both
directions with the same weight:

...

LabelX   LabelY  WeightXY

LabelY   LabelX  WeightXY

...
"""
function read_network_file(file_path::AbstractString)
    mat = readdlm(file_path)[1:2:end, :]
    edges = []
    nodes = Set()

    for i in 1:size(mat,1)
        n1_label, n2_label, weight = mat[i, :]
        n1_label = string(n1_label)
        n2_label = string(n2_label)
        n1 = Node(n1_label, [], 0, [])
        n2 = Node(n2_label, [], 0, [])
        new_edge = Edge([n1, n2], weight)
        push!(edges, new_edge)
        push!(nodes, n1_label, n2_label)
    end

    nodes = [Node(n, [], 0, []) for n in nodes]
    return InferredNetwork(nodes, edges)
end

"""
    get_adjacency_matrix(inferred_network::InferredNetwork, threshold = 1.0; <keyword arguments>)

Gets an adjacency matrix given an InferredNetwork and a threshold.

Arguments:
* `inferred_network`: network that was inferred
* `threshold=0.1`: threshold above which to keep edges in the network
* `absolute=false`: interpret threshold as an absolute confidence score

If `absolute` is false, threshold will be interpreted as the percentage of edges to keep.
"""
function get_adjacency_matrix(inferred_network::InferredNetwork, threshold = 0.1; absolute = false)

    number_of_nodes = length(inferred_network.nodes)
    adjacency_matrix = zeros(Bool, (number_of_nodes, number_of_nodes))

    labels_to_ids = Dict{String,Int}()
    ids_to_labels = Dict{Int,String}()
    i = 1
    for node in inferred_network.nodes
        labels_to_ids[node.label] = i
        ids_to_labels[i] = node.label
        i += 1
    end

    number_of_edges = absolute ?
        findfirst(x -> x.weight < threshold, inferred_network.edges) - 1 :
        Int(round(length(inferred_network.edges) * threshold))

    for edge in inferred_network.edges[1 : number_of_edges]
        node1 = labels_to_ids[edge.nodes[1].label]
        node2 = labels_to_ids[edge.nodes[2].label]
        adjacency_matrix[node1, node2] = true
        adjacency_matrix[node2, node1] = true
    end

    return adjacency_matrix, labels_to_ids, ids_to_labels

end

"""
    infer_network(data_file_path::String, inference::AbstractNetworkInference; <keyword arguments>)

Infers a network, given a data file and a network inference algorithm. It
is assumed that the first line of the file is headers (which are
discarded) and the subsequent lines each represent one node, and are of
the form:

Label    data_value1  data_value2 ...

though a different delimiter may be specified.

Arguments:
* `data_file_path`: path to the data file
* `inference`: network inference algorithm (e.g. `PIDCNetworkInference()`)
* `delim=false`: the file's delimiter. Leave as false if it is whitespace
* `discretizer="bayesian_blocks"`: algorithm for discretizing the data
* `estimator="maximum_likelihood"`: algorithm for estimating probabilities
* `number_of_bins=10`: will be overwritten if using "bayesian_blocks"
* `base=2`: base for the information measures
* `out_file_path=""`: path to output file. If empty, will not write a file

The "maximum_likelihood" estimator is recommended for PUC and PIDC.
"""
function infer_network(data_file_path::String, inference::AbstractNetworkInference; delim::Union{Char,Bool} = false,
    discretizer = "bayesian_blocks", estimator = "maximum_likelihood", number_of_bins = 10, base = 2,
    out_file_path = "")

    println("Getting nodes...")
    if typeof(inference) != CorrelationNetworkInference
        nodes = get_nodes(
            data_file_path,
            delim = delim,
            discretizer = discretizer,
            estimator = estimator,
            number_of_bins = number_of_bins
        )
    elseif typeof(inference) == CorrelationNetworkInference
        nodes, values = get_nodes(
            data_file_path,
            delim = delim,
            get_values = true
        )
        inference.values = values
    end

    println("Inferring network...")
    if typeof(inference) != CorrelationNetworkInference
        inferred_network = InferredNetwork(inference, nodes, estimator = estimator, base = base)
    elseif typeof(inference) == CorrelationNetworkInference
        inferred_network = InferredNetwork(inference, nodes)
    end

    if length(out_file_path) > 1
        println("Writing network to file...")
        write_network_file(out_file_path, inferred_network)
    end

    return inferred_network

end


@testset "Spinorama" begin
    function compare_magdb(a, b, c)
        # expects common domains
        errs = abs.(data(amp2db.(a[:, c]) .- amp2db.(b[:, c])))
        mae = mean(errs)
        maxerr = maximum(errs)
        errs_ = SpectrumArray(errs .|> db2amp, rate(a), domain(a), [:error])
        (id=c, mae=mae, maxerr=maxerr, errs=errs_)
    end

    function compare_curves(a, b, cmpfunc)
        common_channels = collect(intersect(Set(names(a)), Set(names(b)))) |> sort
        res = [cmpfunc(unify_domains(a, b)..., c) for c in common_channels]
        sort!(res; by=e -> e.maxerr, rev=true)
        res
    end

    function test_spinorama(measurement_dir, threshold_max, threshold_mae;
            template_hor=Regex(raw"VituixCAD_PolarFR hor (.*)\.frd"), template_ver=Regex(raw"VituixCAD_PolarFR ver (.*)\.frd"))
        data_dir = joinpath(test_dir, measurement_dir)
        spinorama = load_spinorama_from_FRDs(data_dir, template_hor=template_hor, template_ver=template_ver)
        spinorama_curves = evaluate(spinorama)
        spinorama_curves_vituix = load_vituix_spinorama_curves(joinpath(data_dir, "spinorama"))
        errs = compare_curves(spinorama_curves, spinorama_curves_vituix, compare_magdb)

        worst_maxerr = errs[1].maxerr
        worst_mae = errs[1].mae
        println("  \"$(measurement_dir)\"")
        println("\tMAX: $(worst_maxerr),\tMAE: $(worst_mae) WORST CASE: ", NamedTuple{(:id, :maxerr, :mae)}(errs[1]))
        if worst_maxerr > threshold_max || worst_mae > threshold_mae
            return false
        end
        return true
    end

    println("VITUIX SPINORAMA TESTS")
    @test test_spinorama("T/polar", 0.2, 0.08)
    @test test_spinorama("T/polar_noXO", 0.2, 0.08)
    @test test_spinorama("T/polar_r20", 0.3, 0.08)
    @test test_spinorama("T/polar_noXO_r-80_t-80", 0.15, 0.035)
    @test test_spinorama("T/polar_noXO_r20", 0.25, 0.075)
    @test test_spinorama("T/polar_r10_t15", 0.15, 0.07)

    @test test_spinorama("M/polar", 0.15, 0.05)
    @test test_spinorama("M/polar_noXO", 0.15, 0.05)
    @test test_spinorama("M/polar_r10", 0.15, 0.05)
    @test test_spinorama("M/polar_r170", 0.45, 0.075)
    @test test_spinorama("M/polar_y-118", 0.15, 0.035)
    @test test_spinorama("M/polar_y-118_r170", 0.4, 0.075)
    @test test_spinorama("M/polar_y-118_r180", 1.0, 0.15)
    @test test_spinorama("M/polar_noXO_r-170", 0.5, 0.075)
    @test test_spinorama("M/polar_noXO_r170", 0.45, 0.075)
    @test test_spinorama("M/polar_noXO_y-118_r170", 0.4, 0.065)
    @test test_spinorama("M/polar_x95_y-118_z150_r12_t-15", 0.15, 0.05)

    @test test_spinorama("W/polar", 0.8, 0.055)
    @test test_spinorama("W/polar_noXO", 0.8, 0.055)
    @test test_spinorama("W/polar_y-348", 0.6, 0.055)
    @test test_spinorama("W/polar_y-348_r14", 0.5, 0.045)
    @test test_spinorama("W/polar_noXO_y-348_r14", 0.5, 0.045)
    @test test_spinorama("W/polar_x-150_y-348_z-35_r-90_t3", 0.15, 0.08)

    @test test_spinorama("total/polar", 0.15, 0.045)
    @test test_spinorama("total_rotated/polar", 0.15, 0.04)
    @test test_spinorama("total_tilted/polar", 0.15, 0.045)
    @test test_spinorama("total_rotated_tilted/polar", 0.15, 0.04)

    @test test_spinorama("total_noXO/polar", 0.15, 0.05)
    @test test_spinorama("total_noXO_rotated/polar", 0.4, 0.06)
    @test test_spinorama("total_noXO_tilted/polar", 0.15, 0.05)
    @test test_spinorama("total_noXO_rotated_tilted/polar", 0.4, 0.05)

end


module CellValuesReindex

using Gridap
using Gridap.Helpers
using Gridap.CellValuesGallery
using Gridap.CellMapApply: CellMapFromKernel
using Gridap.CellMapApply: IndexCellMapFromKernel
using Base: @pure

export reindex
export IndexLike

import Base: iterate
import Base: length
import Base: size
import Base: getindex
import Base: IndexStyle

const IndexLike = Union{Integer,CartesianIndex}

function reindex(values::IndexCellValue, indices::Vector{<:IndexLike})
  i = CellValueFromArray(indices)
  reindex(values,i)
end

function reindex(values::IndexCellValue, indices::CellValue{<:IndexLike})
  @notimplemented
end

function reindex(values::IndexCellValue, indices::IndexCellValue{<:IndexLike})
  @notimplemented
end

# Efficient implementations of reindex for concrete types

function reindex(cm::CellMapFromKernel,indices::CellValue{<:IndexLike})
  _reindex_cmfk(cm,indices)
end

function reindex(cm::IndexCellMapFromKernel,indices::CellValue{<:IndexLike})
  _reindex_cmfk(cm,indices)
end

function reindex(cm::IndexCellMapFromKernel,indices::IndexCellValue{<:IndexLike})
  _reindex_cmfk(cm,indices)
end

function _reindex_cmfk(cm,indices)
  k = cm.kernel
  rs = [ reindex(mi,indices) for mi in cm.cellvalues ]
  apply(k,rs...)
end

end # module


@testset "HW5" begin
    using DMUStudent.HW5: mc
    using CommonRLInterface
    using Compose

    @test mc isa AbstractEnv

    A = actions(mc)
    done = false
    rsum = 0.0
    reset!(mc)
    s = observe(gw)
    t = 1
    γ = 0.95
    max_t = 200
    while t <= max_t && !terminated(mc)
        a = rand(A)

        r = act!(mc, a)
        sp = observe(mc)

        rsum += γ^t*r

        s = sp
        t += 1
    end
    @test -5.0 <= rsum <= 1000.0

    # rendering
    rndr = render(mc)
    @test rndr isa Context
    sprint((io, x)->show(io, MIME("text/html"), x), rndr)

    @test HW5.evaluate(s->0.0).score < 0.0
    @test HW5.evaluate(s->0.0, "zachary.sunberg@colorado.edu").score < 0.0
    @test HW5.evaluate(s->rand(actions(mc)), "zachary.sunberg@colorado.edu").score < 0.0

    # test whether it has enough energy
    disc = 1.0
    rsum = 0.0
    mc.s = [1.56, 0.0]
    while !terminated(mc) && disc > 0.001
        r = act!(mc, -1.0)
        rsum += disc*r
        disc *= 0.99
    end
    @test rsum > 200.0

    disc = 1.0
    rsum = 0.0
    mc.s = [1.4, 0.0]
    while !terminated(mc) && disc > 0.001
        r = act!(mc, -1.0)
        rsum += disc*r
        disc *= 0.99
    end
    @test rsum < 0.0
end


#=
function offline_tracking(wt,max_whiskers=10)

    for i=1:size(wt.vid,3)
        image_preprocessing(wt.vid,i)
    end

    for i=1:size(wt.vid,3)
        #Need to transpose becuase row major in C vs column major in julia
        wt.all_whiskers[i]=WT_trace(i,wt.vid[:,:,i]',wt.min_length,wt.pad_pos,wt.mask)
        println(string(i,"/",size(wt.vid,3)))
    end

    reorder_whiskers(wt)

    nothing
end

function offline_tracking_parallel(wt,max_whiskers=10)

    pmap(t->image_preprocessing(wt.vid,t),1:size(wt.vid,3),batch_size=ceil(Int,size(wt.vid,3)/nworkers()))
    #pmap(t->image_preprocessing(wt.vid,t),1:10)
    println("Preprocessing complete")

    wt.all_whiskers=pmap(t->WT_trace(t,wt.vid[:,:,t]',wt.min_length,wt.pad_pos,wt.mask),1:size(wt.vid,3),
    batch_size=ceil(Int,size(wt.vid,3)/nworkers()))
    #wt.all_whiskers=pmap(t->WT_trace(t,wt.vid[:,:,t]',wt.min_length,wt.pad_pos,wt.mask),1:10)

    reorder_whiskers(wt)

    nothing
end
=#
function image_preprocessing(vid,i)

    temp_img = zeros(Float64,size(vid,1),size(vid,2))
    temp_img2=zeros(Float64,size(vid,1),size(vid,2))

    #Adjust contrast

    for j=1:size(vid,1)
        for k=1:size(vid,2)
            temp_img[j,k] = convert(Float64,vid[j,k,i])
        end
    end

    local_contrast_enhance!(temp_img,temp_img)

    #anisotropic diffusion to smooth while perserving edges
    anisodiff!(temp_img, 20,20.0,0.05,1,temp_img2)

    for j=1:size(vid,1)
        for k=1:size(vid,2)
            vid[j,k,i] = round(UInt8,temp_img[j,k])
        end
    end

    nothing
end

function mean_image_uint8(han::Tracker_Handles)
    #round.(UInt8,squeeze(mean(han.wt.vid,3),3)) #These should use the normalized from teh whole video
end

function mean_image(han::Tracker_Handles)
    #squeeze(mean(han.wt.vid,3),3) #These should use the normalized from the whole video
end


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

module FEMBase

using LinearAlgebra

macro lintpragma(s)
end

import Base: getindex, setindex!, convert, length, size, isapprox,
             first, last, vec,
             ==, +, -, *, /, haskey, copy, push!, isempty, empty!,
             append!, read

using TimerOutputs
export @timeit, print_timer

import FEMBasis
import FEMQuad

include("fields.jl")
export interpolate, update!, DCTI, DCTV, DVTI, DVTV, CVTV, DVTId, DVTVd, field
include("types.jl")
include("sparse.jl")
include("elements.jl")
export element_info!
include("elements_lagrange.jl")
include("integrate.jl")

include("problems.jl")
export set_gdofs!, get_gdofs, get_formulation_type, add_element!, add_elements!

include("assembly.jl")
export assemble_prehook!, assemble_posthook!, assemble_elements!

include("solvers.jl")
export LinearSystem, AbstractLinearSystemSolver, solve!

include("analysis.jl")
export AbstractAnalysis, Analysis, add_problems!, get_problems, run!,
       AbstractResultsWriter, add_results_writer!, get_results_writers,
       write_results!, get_problem

export FieldProblem, BoundaryProblem, Problem, Element, Assembly
export add!, group_by_element_type,
       get_unknown_field_name, get_unknown_field_dimension,
       get_integration_points, initialize!, assemble!
export SparseMatrixCOO, SparseVectorCOO, Node, BasisInfo,
       IP, AbstractProblem, IntegrationPoint, AbstractField
export is_field_problem, is_boundary_problem, get_elements,
       get_connectivity,
       get_parent_field_name, get_reference_coordinates,
       get_reference_element_coordinates,
       get_assembly, get_nonzero_rows, get_nonzero_columns,
       eval_basis!, get_basis, get_dbasis, grad!,
       assemble_mass_matrix!, get_local_coordinates, inside,
       get_element_type, filter_by_element_type, get_element_id,
       resize_sparse, resize_sparsevec

include("test.jl")

function eliminate_boundary_conditions! end
export eliminate_boundary_conditions!

function add_slave_elements! end
export add_slave_elements!

function get_slave_elements end
export get_slave_elements

function add_master_elements! end
export add_master_elements!

function get_master_elements end
export get_master_elements

using FEMBasis
export Poi1, Seg2, Seg3, Tri3, Tri6, Tri7, Quad4, Quad8, Quad9,
       Tet4, Tet10, Pyr5, Wedge6, Wedge15, Hex8, Hex20, Hex27

include("deprecated.jl")

end


using Pkg
Pkg.update()
ENV["PYTHON_JL_RUNTIME_PYTHON"] = Sys.which("python")
Pkg.build("PyCall")
module HHNSGA
    using Pkg
    using Plots
    using UnicodePlots
    using OrderedCollections
    using LinearAlgebra
    using UnicodePlots
    using PyCall
    using NSGAII
    export NSGAII

    export SNN
    Pkg.add(url="https://github.com/Datseris/SpikeSynchrony.jl")
    using SpikeSynchrony
    #import Pkg;
    Pkg.add("OrderedCollections")
    Pkg.add("NSGAII")#` to install the NSGAII package.

    include("../src/SpikingNeuralNetworks.jl")
    include("../src/units.jl")
    include("../src/plot.jl")

    SNN = SpikingNeuralNetworks.SNN
    using Random
    using PyCall
    py"""
    import copy
    import matplotlib
    import matplotlib.pyplot as plt
    import numpy as np
    matplotlib.get_backend()
    import sys
    import os
    sys.path.append(os.getcwd())
    """
    Main.include("../src/SpikingNeuralNetworks.jl")
    Main.eval("SNN = SpikingNeuralNetworks.SNN")

    """
    ##model = py"IZModel()"
    simple = py"IZModel()"

    py"""
    import pickle
    import numpy as np
    import random
    from sciunit.scores.collections import ScoreArray
    from sciunit import TestSuite
    import sciunit
    import random

    from neuronunit.tests.fi import RheobaseTestP
    from collections import OrderedDict
    #from neuronunit import get_neab
    #import pdb
    #pdb.set_trace()
    cell_tests = pickle.load(open('multicellular_constraints.p','rb'))
    for test in cell_tests.values():
        if "Rheobase test" in test.keys():
            temp_test = {k:v for k,v in test.items()}
            break
    rt = temp_test["Rheobase test"]
    #rtp = RheobaseTestP(rt.observation)
    JUIZI = {
        'a': 0.02,
        'b': 0.2,
        'c': -65,
        'd': 8,
    }

    #ranges = OrderedDict({k:[v-0.5*np.abs(v),v+0.5*np.abs(v)] for k,v in copy.copy(JUIZI).items()})
    temp = {'a':[0.02,0.1],'b':[0.2,0.26],'c':[-65,-50],'d':[0.05,8]}
    ranges = OrderedDict(temp)

    N = len(JUIZI)
    """
    N = py"N"
    ranges = py"ranges"
    #H1=[values(ranges)]
    #current_params = py"rt.params"
    #simple.attrs = py"JUIZI"
    py"""
    from izhi import IZModel

    def evaluate(test,god):
        model = IZModel(attrs=god)
        model.attrs = god
        rt = test['Rheobase test']

        rheo = rt.generate_prediction(model)

        scores_ = []
        test = {k:v for k,v in test.items() if v.observation is not None}
        for temp_test in test.values():
            if 'injected_square_current' not in temp_test.params.keys():
               temp_test.params['injected_square_current'] = {}
               temp_test.params['amplitude'] = rheo['value']

            if temp_test.name == "InjectedCurrentAPWidthTest":
               temp_test.params['injected_square_current']['amplitude'] = rheo['value']
               temp_test.params['amplitude'] = rheo['value']

            if temp_test.name == "Injected CurrentAPThresholdTest":
               temp_test.params['injected_square_current']['amplitude'] = rheo['value']
               temp_test.params['amplitude'] = rheo['value']

            if temp_test.name == "InjectedCurrentAPAmplitudeTest":
               temp_test.params['injected_square_current']['amplitude'] = rheo['value']
               temp_test.params['amplitude'] = rheo['value']

            if temp_test.name == "RheobaseTest":
               temp_test.params['injected_square_current']['amplitude'] = rheo['value']
               temp_test.params['amplitude'] = rheo['value']

        to_map = [(tt,model) for tt in test.values() ]
        def map_score(content):
            temp_test = content[0]
            model = content[1]
            temp_test.score_type = sciunit.scores.ZScore
            try:
                score = temp_test.judge(model)
                score = np.abs(score.log_norm_score)
            except:
                try:
                    score = np.abs(float(score.raw_score))
                except:
                    score = 100
            if isinstance(score, sciunit.scores.incomplete.InsufficientDataScore):
                score = 100
            return score

        scores_ = list(map(map_score,to_map))
        tt = TestSuite(list(test.values()))
        SA = ScoreArray(tt, scores_)
        errors = tuple(SA.values,)
        return errors
    """

    py"""
    def z_py(evaluate,god):

        cell_tests = pickle.load(open('multicellular_constraints.p','rb'))
        tests = cell_tests['Neocortex pyramidal cell layer 5-6']

        model_type="RAW"
        #from neuronunit.optimisation import make_sim_tests
        fps = ['a','b','c','d']
        #sim_tests, OM, target = make_sim_tests.test_all_objective_test(fps,model_type=model_type)
        SA = evaluate(tests,god)
        return SA
    """

    export transdict
    function transdict(x)
        bc = BinaryCoding(4, [:Int,:Int,:Int,:Int], py"lower_list", py"upper_list")

        py"""
        def map_dict(x):
            genes_out = x
            genes_out_dic = OrderedDict({k:genes_out[i] for i,k in enumerate(ranges.keys()) })
            return genes_out, genes_out_dic
        """
        decoded = x

        try
           decoded = NSGAII.decode(x,bc)
        catch
           decoded = x
        end
        _,god_dict = py"map_dict"(decoded)
        #bincoded = NSGAII.encode(genes_out, bc)
        return god_dict#, genes_out_dic
    end
    py"""
    lower_list = [v[0] for k,v in ranges.items()]
    upper_list = [v[1] for k,v in ranges.items()]
    """

    export init_function
    function init_function()
        #const
        bc = BinaryCoding(4, [:Int,:Int,:Int,:Int], py"lower_list", py"upper_list")

        py"""
        def pheno_map():

            lower_list = [v[0] for k,v in ranges.items()]
            upper_list = [v[1] for k,v in ranges.items()]
            gene_out = [random.uniform(lower, upper) for lower, upper in zip(lower_list, upper_list)]
            #gene_out_dic = OrderedDict({k:gene_out[i] for i,k in enumerate(ranges.keys()) })
            return gene_out#, gene_out_dic
        """
        gene_out = py"pheno_map"()
        bincoded = NSGAII.encode(gene_out, bc)
        return bincoded
    end

    function references_for_H()
        bc = BinaryCoding(4, [:Int,:Int,:Int,:Int], py"lower_list", py"upper_list")

        py"""
        from sklearn.model_selection import ParameterGrid
        grid = ParameterGrid(ranges)
        genes = []
        for g in grid:
            genes.append(list(g.values()))
        genes_out_dic = OrderedDict({k:genes[i] for i,k in enumerate(ranges.keys()) })

        """
        genes_out = py"genes"
        bcd = []
        for g in genes_out
             bincoded = NSGAII.encode(g, bc)
             append!(bcd,bincoded)
        end
        return bcd
    end

    export z
    function z(x)
        god = transdict(x)
        contents = py"z_py"(py"evaluate",god)
        return contents
    end

end
#create_sysimage(:HHNSGA, sysimage_path="sys_HHNSGA.so", precompile_execution_file="precompile_HHNSGA.jl")


include("cv.jl")

# glass

# Source: UCI / Glass Identification
# # of classes: 6
# # of data: 214
# # of features: 9
# Files:
# glass.scale
# > C=32, bias=1
# vote: 0.6261682242990654
# ctmc: 0.6355140186915887
# mean: 0.6355140186915887
# prod: 0.6261682242990654
features, labels = loaddata("data/glass.scale", 9)
cv(features, labels, C=2^5, bias=1)

# iris

# Source: UCI / Iris Plant
# # of classes: 3
# # of data: 150
# # of features: 4
# Files:
# iris.scale
# > C=32, bias=1
# vote: 0.98
# ctmc: 0.98
# mean: 0.98
# prod: 0.98
features, labels = loaddata("data/iris.scale", 4)
cv(features, labels, C=2^5, bias=1)

# segment

# Source: Statlog / Segment
# # of classes: 7
# # of data: 2,310
# # of features: 19
# Files:
# segment.scale
# > C=16, bias=1
# vote: 0.9575757575757575
# ctmc: 0.9575757575757575
# mean: 0.9571428571428572
# prod: 0.9571428571428572
features, labels = loaddata("data/segment.scale", 19)
cv(features, labels, C=2^5, bias=1)

# vehicle

# Source: Statlog / Vehicle
# # of classes: 4
# # of data: 846
# # of features: 18
# Files:
# vehicle.scale
# > C=32, bias=1
# vote: 0.8226950354609929
# ctmc: 0.8250591016548463
# mean: 0.8191489361702128
# prod: 0.8191489361702128
features, labels = loaddata("data/vehicle.scale", 18)
cv(features, labels, C=2^5, bias=1)

# vowel

# Source: UCI / Vowel
# Preprocessing: First 528 instances are used as training and the remaining instances are for testing. Scaling training data first and adjust testing data accordingly.
# # of classes: 11
# # of data: 528 / 462 (testing)
# # of features: 10
# Files:
# vowel
# vowel.t (testing)
# vowel.scale (scaled to [-1,1])
# vowel.scale.t (testing)
# > C=8, bias=1
# vote: 0.7537878787878788
# ctmc: 0.7424242424242424
# mean: 0.7443181818181818
# prod: 0.7443181818181818
features, labels = loaddata("data/vowel.scale", 10)
cv(features, labels, C=2^5, bias=1)

# wine

# Source: UCI / Wine Recognition
# # of classes: 3
# # of data: 178
# # of features: 13
# Files:
# wine.scale
# > C=0.0625, bias=1
# vote: 0.9775280898876404
# ctmc: 0.9775280898876404
# mean: 0.9775280898876404
# prod: 0.9775280898876404
features, labels = loaddata("data/wine.scale", 13)
cv(features, labels, C=2.0^-2, bias=1)

#####################

# dna

# Source: Statlog / Dna
# Preprocessing: Training data is further separated into two sets, tr and val. [CWH01a]
# # of classes: 3
# # of data: 2,000 / 1,186 (testing) / 1,400 (tr) / 600 (val)
# # of features: 180
# Files:
# dna.scale
# dna.scale.t (testing)
# dna.scale.tr (tr)
# dna.scale.val (val)
# > C=0.0078125, bias=4
# vote: 0.952
# ctmc: 0.9525
# mean: 0.9525
# prod: 0.9525
features, labels = loaddata("data/dna.scale", 180)
cv(features, labels, C=2.0^-7, bias=2^2)

# satimage

# Source: Statlog / Satimage
# Preprocessing: Training data is further separated into two sets, tr and val. [CWH01a]
# # of classes: 6
# # of data: 4,435 / 2,000 (testing) / 3,104 (tr) / 1,331 (val)
# # of features: 36
# Files:
# satimage.scale
# satimage.scale.t (testing)
# satimage.scale.tr (tr)
# satimage.scale.val (val)
# > C=8, bias=0.001
# vote: 0.8683201803833145
# ctmc: 0.86967305524239
# mean: 0.8683201803833145
# prod: 0.8683201803833145
features, labels = loaddata("data/satimage.scale", 36)
cv(features, labels, C=2^3, bias=.001)

# letter

# Source: Statlog / Letter
# Preprocessing: Training data is further separated into two sets, tr and val. [CWH01a]
# # of classes: 26
# # of data: 15,000 / 5,000 (testing) / 10,500 (tr) / 4,500 (val)
# # of features: 16
# Files:
# letter.scale
# letter.scale.t (testing)
# letter.scale.tr (tr)
# letter.scale.val (val)
# > C=8, bias=1
# vote: 0.8450666666666666
# ctmc: 0.8438666666666667
# mean: 0.8445333333333334
# prod: 0.8445333333333334
features, labels = loaddata("data/letter.scale", 16)
cv(features, labels, C=2^3, bias=1)

# shuttle

# Source: Statlog / Shuttle
# Preprocessing: Training data is further separated into two sets, tr and val. [CWH01a]
# # of classes: 7
# # of data: 43,500 / 14,500 (testing) / 30,450 (tr) / 13,050 (val)
# # of features: 9
# Files:
# shuttle.scale
# shuttle.scale.t (testing)
# shuttle.scale.tr (tr)
# shuttle.scale.val (val)
# > C=64, bias=1
# vote: 0.9693103448275862
# ctmc: 0.9695172413793104
# mean: 0.9717701149425287
# prod: 0.9717931034482759
features, labels = loaddata("data/shuttle.scale", 9)
cv(features, labels, C=2^6, bias=1)


# For generating symbols that work as dataframe column names; symbols
# generated by gensym() don't work for this.
global _rvnum = 0

"""
    _make_rvname(name)

Return a unique name for random variable `rv`.
"""
function _make_rvname(name)
    global _rvnum += 1
    return Symbol("$(name)!$(_rvnum)")
end

"""
    _make_dims(args)

Return a vector of dimensions from `args`.
"""
function _make_dims(args)
    dims = Vector{Any}()
    for arg in args
        # println("Arg: $arg")

        if @capture(arg, i_Int)  # scalar (must be integer)
            # println("arg is Int")
            dim = i

        elseif @capture(arg, first_Int:last_)   # last can be an int or 'end', which is converted to 0
            # println("arg is range")
            last = last == :end ? 0 : last
            dim = :($first:$last)

        elseif @capture(arg, first_Int:step_:last_)
            # println("arg is step range")
            last = last == :end ? 0 : last
            dim = :($first:$step:$last)

        elseif @capture(arg, s_Symbol)
            if arg == :(:)
                # println("arg is Colon")
                dim = Colon()
            else
                # println("arg is Symbol")
                dim = :($(QuoteNode(s)))
            end

        elseif @capture(arg, s_String)
            dim = s

        elseif isa(arg, Expr) && arg.head == :tuple  # tuple of Strings/Symbols (@capture didn't work...)
            argtype = typeof(arg.args[1])            # ensure all are same type as first element
            if ! isempty(filter(s -> typeof(s) != argtype, arg.args))
                error("A parameter dimension tuple must be all Strings or all Symbols (got $arg)")
            end
            dim = :(convert(Vector{$argtype}, $(arg.args)))

        else
            error("Unrecognized stochastic parameter specification: $arg")
        end
        push!(dims, dim)
    end
    # println("dims = $dims")
    return dims
end

"""
    defsim(expr::Expr)

Define a Mimi `SimulationDef` with the expressions in `expr`.
"""
macro defsim(expr::Expr)
    let # to make vars local to each macro invocation
        local _rvs        = []
        local _transforms = []
        local _saves      = []
        local _sim_args  = Dict{Symbol, Any}()

        simdatatype = nothing

        # distilled into a function since it's called from two branches below
        function saverv(rvname, distname, distargs)
            expr = :(RandomVariable($(QuoteNode(rvname)), $distname($(distargs...))))
            push!(_rvs, esc(expr))
        end

        @capture(expr, elements__)
        for elt in elements
            # Meta.show_sexpr(elt)
            # println("")
            # e.g.,  rv(name1) = Normal(10, 3)
            if @capture(elt, rv(rvname_) = distname_(distargs__))
                saverv(rvname, distname, distargs)

            elseif @capture(elt, save(vars__))
                for var in vars
                    # println("var: $var")
                    if @capture(var, comp_.datum_)
                        expr = :($(QuoteNode(comp)), $(QuoteNode(datum)))
                        push!(_saves, esc(expr))
                    else
                        error("Save arg spec must be of the form comp_name.datum_name; got ($var)")
                    end
                end

            # handle vector of distributions
            elseif @capture(elt, extvar_ = [items__])
                for pair in items
                    if (@capture(pair, [dims__] => distname_(distargs__)) ||
                        @capture(pair,     dim_ => distname_(distargs__)))

                        dims = _make_dims(dims === nothing ? [dim] : dims)

                        rvname = _make_rvname(extvar)
                        saverv(rvname, distname, distargs)

                        if @capture(extvar, compname_.name_)
                            expr = :(TransformSpec($(QuoteNode(compname)), $(QuoteNode(name)), :(=), $(QuoteNode(rvname)), [$(dims...)]))
                        else
                            expr = :(TransformSpec($(QuoteNode(extvar)), :(=), $(QuoteNode(rvname)), [$(dims...)]))
                        end
                        push!(_transforms, esc(expr))
                    end
                end

            # e.g., ext_var5[2010:2050, :] *= name2
            elseif (@capture(elt, extvar_  = rvname_Symbol) ||
                    @capture(elt, extvar_ += rvname_Symbol) ||
                    @capture(elt, extvar_ *= rvname_Symbol) ||
                    @capture(elt, extvar_  = distname_(distargs__)) ||
                    @capture(elt, extvar_ += distname_(distargs__)) ||
                    @capture(elt, extvar_ *= distname_(distargs__)))

                # For "anonymous" RVs, e.g., ext_var2[2010:2100, :] *= Uniform(0.8, 1.2), we
                # gensym a name based on the model parameter name and process it as a named RV.
                if rvname === nothing
                    param_name = @capture(extvar, name_[args__]) ? name : extvar
                    rvname = _make_rvname(param_name)
                    saverv(rvname, distname, distargs)
                end

                op = elt.head
                # println("Ref:  $name, $args")
                # Meta.show_sexpr(extvar)
                # println("")
                # if extvar.head == :ref, extvar.args must be one of the following,
                # where the extvar could be paramname or compname.paramname:
                    # - a scalar value, e.g., name[2050] => (:ref, :name, 2050)
                    #   convert to tuple of dimension specifiers (:name, 2050)
                    # - a slice expression, e.g., name[2010:2050] => (:ref, :name, (:(:), 2010, 2050))
                    #   convert to (:name, 2010:2050) [convert it to actual UnitRange instance]
                    # - a tuple of symbols, e.g., name[(US, CHI)] => (:ref, :name, (:tuple, :US, :CHI))
                    #   convert to (:name, (:US, :CHI))
                    # - combinations of these, e.g., name[2010:2050, (US, CHI)] => (:ref, :name, (:(:), 2010, 2050), (:tuple, :US, :CHI))
                    #   convert to (:name, 2010:2050, (:US, :CHI))

                if @capture(extvar, compname_.name_[args__])
                    dims = _make_dims(args)
                    expr = :(TransformSpec($(QuoteNode(compname)), $(QuoteNode(name)), $(QuoteNode(op)), $(QuoteNode(rvname)), [$(dims...)]))
                elseif @capture(extvar, compname_.name_)
                    expr = :(TransformSpec($(QuoteNode(compname)), $(QuoteNode(name)), $(QuoteNode(op)), $(QuoteNode(rvname))))

                elseif @capture(extvar, name_[args__])
                    dims = _make_dims(args)
                    expr = :(TransformSpec($(QuoteNode(name)), $(QuoteNode(op)), $(QuoteNode(rvname)), [$(dims...)]))
                else
                    expr = :(TransformSpec($(QuoteNode(extvar)), $(QuoteNode(op)), $(QuoteNode(rvname))))
                end
                push!(_transforms, esc(expr))

            elseif @capture(elt, sampling(simdatatype_, simargs__))
                # General method for setting an SA method's required parameters
                for arg in simargs
                    if !@capture(arg, name_=value_)
                        error("simargs must be given as keyword arguments (got $simargs)")
                    end
                    _sim_args[name] = __module__.eval(value)
                end
            else
                error("Unrecognized expression '$elt' in @defsim")
            end
        end

        # set default
        simdatatype = (simdatatype == nothing) ? nameof(MCSData) : simdatatype

        # call constructor on given args
        data = :($simdatatype(; $(_sim_args)...))

        return :(SimulationDef{$simdatatype}(
                    [$(_rvs...)],
                    TransformSpec[$(_transforms...)],
                    Tuple{Symbol, Symbol}[$(_saves...)],
                    $data))
    end
end

#
# Simulation Definition update methods
#

function _update_nt_type!(sim_def::SimulationDef{T}) where T <: AbstractSimulationData
    names = (keys(sim_def.rvdict)...,)
    types = [eltype(fld) for fld in values(sim_def.rvdict)]
    sim_def.nt_type = NamedTuple{names, Tuple{types...}}
    nothing
end

"""
    delete_RV!(sim_def::SimulationDef, name::Symbol)

Delete the random variable with name `name` from the Simulation definition `sim_def`.
Transformations using this RV are deleted, and the Simulation's
NamedTuple type is updated to reflect the dropped RV.
"""
function delete_RV!(sim_def::SimulationDef, name::Symbol)
    if !haskey(sim_def.rvdict, name)
        @warn("Simulation def does not have RV :$name. Nothing being deleted.")
    else
        delete_transform!(sim_def, name)
        delete!(sim_def.rvdict, name)
        _update_nt_type!(sim_def)
    end
end

"""
    add_RV!(sim_def::SimulationDef, rv::RandomVariable)

Add random variable definition `rv` to Simulation definition `sim_def`. The
Simulation's NamedTuple type is updated to include the RV.
"""
function add_RV!(sim_def::SimulationDef, rv::RandomVariable)
    name = rv.name
    haskey(sim_def.rvdict, name) && error("Simulation def already has RV :$name. Use replace_RV! to replace it.")
    sim_def.rvdict[name] = rv
    _update_nt_type!(sim_def)
end

"""
    add_RV!(sim_def::SimulationDef, name::Symbol, dist::Distribution)

Add a random variable with name `name` and distribution `dist` to Simulation definition
`sim_def`. The Simulation's NamedTuple type is updated to include the RV.
"""
add_RV!(sim_def::SimulationDef, name::Symbol, dist::Distribution) = add_RV!(sim_def, RandomVariable(name, dist))

"""
    replace_RV!(sim_def::SimulationDef, rv::RandomVariable)

Replace the RV with the given `rv`s name in the Simulation definition `sim_def` with
`rv` and update the Simulation's NamedTuple type accordingly.
"""
function replace_RV!(sim_def::SimulationDef, rv::RandomVariable)
    name = rv.name
    haskey(sim_def.rvdict, name) || error("Simulation def does not have has RV :$name. Use add_RV! to add it.")
    sim_def.rvdict[rv.name] = rv
    _update_nt_type!(sim_def)
end

"""
    replace_RV!(sim_def::SimulationDef, name::Symbol, dist::Distribution)

Replace the rv with name `name` in the Simulation definition `sim_def` with a new RV
with `name` and distribution `dist`. Update the Simulation's NamedTuple
type accordingly.
"""
replace_RV!(sim_def::SimulationDef, name::Symbol, dist::Distribution) = replace_RV!(sim_def, RandomVariable(name, dist))

"""
    delete_transform!(sim_def::SimulationDef, name::Symbol)

Delete all data transformations--i.e., replacement, addition or multiplication
of original data values with values drawn from the RV named `name`. Update the
Simulation definition's NamedTuple type accordingly.
"""
function delete_transform!(sim_def::SimulationDef, name::Symbol)
    pos = findall(t -> t.rvname == name, sim_def.translist)
    if isempty(pos)
        @warn("Simulation def does not have and transformations using RV :$name. Nothing being deleted.")
    else
        deleteat!(sim_def.translist, pos)
        _update_nt_type!(sim_def)
    end
end

"""
    add_transform!(sim_def::SimulationDef, t::TransformSpec)

Add the data transformation `t` to the Simulation definition `sim_def`, and update the
Simulation's NamedTuple type. The TransformSpec `t` must refer to an
existing RV.
"""
function add_transform!(sim_def::SimulationDef, t::TransformSpec)
    push!(sim_def.translist, t)
    _update_nt_type!(sim_def)
end

"""
    add_transform!(sim_def::SimulationDef, paramname::Symbol, op::Symbol, rvname::Symbol, dims::Vector{T}=[]) where T

Create a new TransformSpec based on `paramname`, `op`, `rvname` and `dims` to the
Simulation definition `sim_def`, and update the Simulation's NamedTuple type.
The symbol `rvname` must refer to an existing random variable, and `paramname`
must refer to an existing shared model parameter that can be accessed by that
name. Use the  signature that includes `compname` if your `paramname`
is an unshared model parameter specific to a component. If `dims` are
provided, these must be legal subscripts of `paramname`. Op must be one of :+=, :*=,
or :(=).
"""
function add_transform!(sim_def::SimulationDef, paramname::Symbol, op::Symbol, rvname::Symbol, dims::Vector{T}=[]) where T
    add_transform!(sim_def, TransformSpec(nothing, paramname, op, rvname, dims))
end

"""
    add_transform!(sim_def::SimulationDef, compname_paramname::Tuple{Symbol, Symbol}, op::Symbol, rvname::Symbol, dims::Vector{T}=[]) where T

Create a new TransformSpec based on `compname`, `paramname`, `op`, `rvname` and `dims` to the
Simulation definition `sim_def`, and update the Simulation's NamedTuple type. The symbol
`rvname` must refer to an existing RV, and `compname` and `paramname` must refer to
an existing parameter for a given component, parameter pair. If `dims` are provided,
these must be legal subscripts of `paramname`. Op must be one of :+=, :*=, or :(=).
"""
function add_transform!(sim_def::SimulationDef, compname::Symbol, paramname::Symbol, op::Symbol, rvname::Symbol, dims::Vector{T}=[]) where T
    add_transform!(sim_def, TransformSpec(compname, paramname, op, rvname, dims))
end

"""
    delete_save!(sim_def::SimulationDef, key::Tuple{Symbol, Symbol})

Delete from Simulation definition `sim_def` a "save" instruction for the given `key`, comprising
component `comp_name` and parameter or variable `datum_name`. This result will no
longer be saved to a CSV file at the end of the simulation.
"""
function delete_save!(sim_def::SimulationDef, key::Tuple{Symbol, Symbol})
    pos = findall(isequal(key), sim_def.savelist)
    isempty(pos) ? @warn("Simulation def doesn't have $key in its save list. Nothing being deleted.") : deleteat!(sim_def.savelist, pos)
    _update_nt_type!(sim_def)
end

"""
    delete_save!(sim_def::SimulationDef, comp_name::Symbol, datum_name::Symbol)

Delete from Simulation definition `sim_def` a "save" instruction for component `comp_name` and parameter
or variable `datum_name`. This result will no longer be saved to a CSV file at the end
of the simulation.
"""
delete_save!(sim_def::SimulationDef, comp_name::Symbol, datum_name::Symbol) = delete_save!(sim_def, (comp_name, datum_name))

"""
    add_save!(sim_def::SimulationDef, key::Tuple{Symbol, Symbol})

Add to Simulation definition `sim_def` a "save" instruction for the given `key`, comprising
component `comp_name` and parameter or variable `datum_name`. This result will
be saved to a CSV file at the end of the simulation.
"""
function add_save!(sim_def::SimulationDef, key::Tuple{Symbol, Symbol})
    pos = findall(isequal(key), sim_def.savelist)
    !isempty(pos) ? @warn("Simulation def already has $key in its save list. Nothing being added.") : push!(sim_def.savelist, key)
end

"""
    add_save!(sim_def::SimulationDef, comp_name::Symbol, datum_name::Symbol)

Add to Simulation definition`sim_def` a "save" instruction for component `comp_name` and parameter or
variable `datum_name`. This result will be saved to a CSV file at the end of the simulation.
"""
add_save!(sim_def::SimulationDef, comp_name::Symbol, datum_name::Symbol) = add_save!(sim_def, (comp_name, datum_name))

"""
    get_simdef_rvnames(sim_def::SimulationDef, name::Union{String, Symbol})

A helper function to support finding the keys in a Simulation Definition `sim_def`'s
rvdict that contain a given `name`. This can be particularly helpful if the random
variable was set via the shortcut syntax ie. my_parameter = Normal(0,1) and thus the
`sim_def` has automatically created a key in the format `:my_parameter!xx`. In this
case this function is useful to get the key and carry out modification functions
such as `replace_rv!`.
"""
function get_simdef_rvnames(sim_def::SimulationDef, name::Union{String, Symbol})
    names = String.([keys(sim_def.rvdict)...])
    matches = Symbol.(filter(x -> occursin(String(name), x), names))
end


using Gurobi
using QuadraticToBinary

const GRB_OPTIMIZER = Gurobi.Optimizer()
MOI.set(GRB_OPTIMIZER, MOI.Silent(), true)
const GRB_CACHE = MOIU.UniversalFallback(MOIU.Model{Float64}())
const GRB_CACHED = MOIU.CachingOptimizer(GRB_CACHE, GRB_OPTIMIZER)
const GRB_BRIDGED = MOI.Bridges.full_bridge_optimizer(GRB_CACHED, Float64)

push!(solvers, (opt = GRB_BRIDGED, mode = BilevelJuMP.SOS1Mode()))
push!(solvers_sos, (opt = GRB_BRIDGED, mode = BilevelJuMP.SOS1Mode()))
push!(solvers_quad, (opt = GRB_BRIDGED, mode = BilevelJuMP.SOS1Mode()))
push!(solvers_sos_quad, (opt = GRB_BRIDGED, mode = BilevelJuMP.SOS1Mode()))

push!(solvers_sos_quad_bin, (
    opt = QuadraticToBinary.Optimizer{Float64}(GRB_BRIDGED),
    mode = BilevelJuMP.SOS1Mode()))

push!(solvers, (opt = GRB_BRIDGED, mode = BilevelJuMP.PositiveSOS1Mode()))
push!(solvers_sos, (opt = GRB_BRIDGED, mode = BilevelJuMP.PositiveSOS1Mode()))
push!(solvers_quad, (opt = GRB_BRIDGED, mode = BilevelJuMP.PositiveSOS1Mode()))
push!(solvers_sos_quad, (opt = GRB_BRIDGED, mode = BilevelJuMP.PositiveSOS1Mode()))


###################################
# Routing module
###################################

"""
Route selector

Finds a route by using a choosen mode (fastest, shortest or based on Google Distances API) and returns intersections indeces for the route

**Arguments**
* `start_node` : unique start node id selected for an agent
* `waypoint` :  unique  node id of a waypoint or nothing when agent is driving directly from *start_node* to *fin_node*
* `fin_node` :  unique finish id selected for an agent
* `activity` : string with category of  waypoint or nothing when agent is driving directly from *start_node* to *fin_node*
* `sim_data` : `SimData` object
* `buffer` : array with already chosen routes stored as a `Road` object
* `routing_mode` : string determining a way how the route will be selected (fastest, shortest or based on Google Distances API)

"""
function get_route(start_node::Int,
					waypoint::Union{Int,Nothing},
					fin_node::Int,
					activity::Union{String,Nothing},
                    sim_data::OpenStreetMapXSim.SimData,
                    buffer::Array{OpenStreetMapXSim.Road,1},
                    routing_mode::String)
    if routing_mode == "fastest"
		if isa(waypoint,Nothing)
			route_nodes, distance, route_time = OpenStreetMapX.fastest_route(sim_data.map_data.network, start_node, fin_node)
		else
			route_nodes, distance, route_time = OpenStreetMapX.fastest_route(sim_data.map_data.network, start_node, waypoint, fin_node)
		end
        road = OpenStreetMapXSim.Road(start_node,fin_node, activity, routing_mode,route_nodes, 1)
        push!(buffer,road)
        return route_nodes
    elseif routing_mode == "shortest"
		if isa(waypoint,Nothing)
			route_nodes, distance, route_time = OpenStreetMapX.shortest_route(sim_data.map_data.network, start_node, fin_node)
		else
			route_nodes, distance, route_time = OpenStreetMapX.shortest_route(sim_data.map_data.network, start_node, waypoint, fin_node)
		end
        road = OpenStreetMapXSim.Road(start_node,fin_node, activity, routing_mode,route_nodes, 1)
        push!(buffer,road)
        return route_nodes
	else
		if isa(waypoint,Nothing)
			route_nodes,routing_mode = OpenStreetMapXSim.get_google_route(start_node, fin_node, sim_data.map_data, sim_data.googleapi_key)
		else
			route_nodes,routing_mode = OpenStreetMapXSim.get_google_route(start_node, fin_node, waypoint, sim_data.map_data, sim_data.googleapi_key)
		end
        road = OpenStreetMapXSim.Road(start_node,fin_node, activity, routing_mode,route_nodes, 1)
        push!(buffer,road)
        return route_nodes
    end
end

"""
Waypoint selector

Selects waypoint by minimizing the length of the route from DA_start by waypoint to DA_fin

**Arguments**
* `start_node` :  unique start node id selected for an agent
* `fin_node` :  unique finish node id selected for an agent
* `activity` : string with category of sought waypoint
* `sim_data` : `SimData` object
* `exact` : boolean value indicing how the waypoint will be chosen (with approximate or exact algorithm)

"""
function get_waypoint(start_node::Int,
					fin_node::Int,
					activity::String,
					sim_data::OpenStreetMapXSim.SimData,
					exact::Bool)
	waypoints = OpenStreetMapX.filter_graph_features(sim_data.features, sim_data.feature_to_intersections,sim_data.feature_classes,activity)
	if exact
		return waypoint = OpenStreetMapX.find_optimal_waypoint_exact(sim_data.map_data.network, sim_data.map_data.network.w, start_node, fin_node, waypoints)
	else
		return waypoint = OpenStreetMapX.find_optimal_waypoint_approx(sim_data.map_data.network, sim_data.map_data.network.w, start_node, fin_node, waypoints)
	end
end

"""
Route module selector

Selects routing mode for two points from the following options: fastest route, shortest route or google route and returns a node indices of the choosen route

**Arguments**
* `DA_start` : unique DA unique id selected for an agent
* `DA_fin` : unique DA unique id selected for an agent
* `sim_data` : simulation data struct
* `DAs_to_intersection` : dictionary mapping each DA to nearest graph node
* `buffer` : array with already chosen routes stored as a `Road` object

**Assumptions**
- the probability of selecting each routing mode is equal

"""
function select_route(DA_start::Int, DA_fin::Int,
                    sim_data::OpenStreetMapXSim.SimData,
                    buffer::Array{OpenStreetMapXSim.Road,1}; google::Bool = false)
    start_node = sim_data.DAs_to_intersection[DA_start]
    fin_node = sim_data.DAs_to_intersection[DA_fin]
    waypoint = activity = nothing
    if google
		routing_mode = rand(["shortest", "fastest", "google"])
	else
		routing_mode = rand(["shortest", "fastest"])
	end
	indice = findfirst(road -> (road.start_node == start_node) &&  (road.fin_node == fin_node) && (road.waypoint == waypoint)  && (road.mode == routing_mode), buffer)
    if isa(indice,Nothing)
		return OpenStreetMapXSim.get_route(start_node, waypoint, fin_node, activity, sim_data, buffer, routing_mode)
    else
		buffer[indice].count += 1
		return buffer[indice].route
    end
end

"""
Route module selector for three points

Selects routing mode for three points from the following options: fastest route, shortest route or google route and returns a node indices of the choosen route

**Arguments**
* `DA_start` :  unique DA id selected for an agent
* `DA_fin` :  unique DA id selected for an agent
* `network` : routing network based on OSM data
* `DAs_to_intersection` : dictionary mapping each DA to nearest graph node
* `features` : dictionary with all features existing in simulation
* `feature_classes` : dictionary mapping each category to proper integer number
* `feature_to_intersections` : dictionary mapping each feature to nearest graph node
* `buffer` : array with already chosen routes stored as a `Road` object

**Assumptions**
- the probability of selecting each routing mode is equal
-agent is choosing a waypoint based on previously selected activity
-waypoint is approximately minimizing the length of the route from DA_start by waypoint to DA_fin

"""
function select_route(DA_start::Int, DA_fin::Int,
                    activity::String,
                    sim_data::OpenStreetMapXSim.SimData,
                    buffer::Array{OpenStreetMapXSim.Road,1}; google::Bool = false)
    start_node = sim_data.DAs_to_intersection[DA_start]
    fin_node = sim_data.DAs_to_intersection[DA_fin]
	if google
		routing_mode = rand(["shortest", "fastest", "google"])
	else
		routing_mode = rand(["shortest", "fastest"])
	end
    indice = findfirst(road -> (road.start_node == start_node) &&  (road.fin_node == fin_node) && (road.waypoint == activity)  && (road.mode == routing_mode), buffer)
    if isa(indice,Nothing)
		waypoint = OpenStreetMapXSim.get_waypoint(start_node,fin_node,activity,sim_data,false)
		return OpenStreetMapXSim.get_route(start_node, waypoint, fin_node, activity, sim_data, buffer, routing_mode)
    else
		buffer[indice].count += 1
		return buffer[indice].route
    end
end