# Here goes all the stuff that connects this code to ExtendableGrids like
# - definition of additional adjacency types and their instantiation
# - connections between CellGeometries and FaceGeometries of their faces
# - formulas to compute Volumes and Normal Vectors etc.
# (some of this might become native in the ExtendableGrids module itself at some point)

# additional ElementGeometryTypes with parent information
#abstract type Vertex0DWithParent{Parent <: AbstractElementGeometry} <: Vertex0D end
#abstract type Vertex0DWithParents{Parent1 <: AbstractElementGeometry, Parent2 <: AbstractElementGeometry} <: Vertex0D end
#export Vertex0DWithParent, Vertex0DWithParents

#function AddParent(FEG::Type{<:Vertex0D}, CEG::Type{<:AbstractElementGeometry})
#    return Vertex0DWithParent{CEG}
#end

#abstract type Edge1DWithParent{Parent <: AbstractElementGeometry} <: Edge1D end
#abstract type Edge1DWithParents{Parent1 <: AbstractElementGeometry, Parent2 <: AbstractElementGeometry} <: Edge1D end
#export Edge1DWithParent, Edge1DWithParents

#function AddParent(FEG::Type{<:Edge1D}, CEG::Type{<:AbstractElementGeometry})
#    return Edge1DWithParent{CEG}
#end

const GridEGTypes = Vector{DataType}
const GridRegionTypes{Ti} = Union{VectorOfConstants{Ti}, Array{Ti,1}}

# additional ExtendableGrids adjacency types
abstract type CellEdges <: AbstractGridAdjacency end
abstract type CellFaces <: AbstractGridAdjacency end
abstract type CellFaceSigns <: AbstractGridAdjacency end
abstract type CellFaceOrientations <: AbstractGridAdjacency end
abstract type CellEdgeSigns <: AbstractGridAdjacency end
abstract type CellVolumes <: AbstractGridFloatArray1D end
abstract type UniqueCellGeometries <: AbstractElementGeometries end

abstract type FaceNodes <: AbstractGridAdjacency end
abstract type FaceVolumes <: AbstractGridFloatArray1D end
abstract type FaceCells <: AbstractGridAdjacency end
abstract type FaceEdges <: AbstractGridAdjacency end
abstract type FaceNormals <: AbstractGridFloatArray2D end
abstract type FaceGeometries <: AbstractElementGeometries end
abstract type FaceRegions <: AbstractElementRegions end
abstract type UniqueFaceGeometries <: AbstractElementGeometries end

abstract type BFaceFaces <: AbstractGridIntegerArray1D end
abstract type BFaceCellPos <: AbstractGridIntegerArray1D end # position of bface in adjacent cell
abstract type BFaceVolumes <: AbstractGridFloatArray1D end
abstract type UniqueBFaceGeometries <: AbstractElementGeometries end

abstract type EdgeNodes <: AbstractGridAdjacency end
abstract type EdgeVolumes <: AbstractGridFloatArray1D end
abstract type EdgeCells <: AbstractGridAdjacency end
abstract type EdgeTangents <: AbstractGridFloatArray2D end
abstract type EdgeRegions <: AbstractElementRegions end
abstract type EdgeGeometries <: AbstractElementGeometries end
abstract type UniqueEdgeGeometries <: AbstractElementGeometries end

abstract type BEdgeNodes <: AbstractGridAdjacency end
abstract type BEdgeEdges <: AbstractGridIntegerArray1D end
#abstract type BEdgeCellPos <: AbstractGridIntegerArray1D end # position of bface in adjacent cell
abstract type BEdgeVolumes <: AbstractGridFloatArray1D end
abstract type BEdgeRegions <: AbstractElementRegions end
abstract type BEdgeGeometries <: AbstractElementGeometries end
abstract type UniqueBEdgeGeometries <: AbstractElementGeometries end

abstract type NodePatchGroups <: AbstractGridIntegerArray1D end

## grid item types to dispatch certain things to the correct GridComponents
abstract type ITEMTYPE_NODE end
abstract type ITEMTYPE_CELL end
abstract type ITEMTYPE_FACE end
abstract type ITEMTYPE_BFACE end
abstract type ITEMTYPE_EDGE end
abstract type ITEMTYPE_BEDGE end

abstract type PROPERTY_NODES end
abstract type PROPERTY_VOLUME end
abstract type PROPERTY_REGION end
abstract type PROPERTY_GEOMETRY end
abstract type PROPERTY_UNIQUEGEOMETRY end

GridComponent4TypeProperty(::Type{ITEMTYPE_CELL},::Type{PROPERTY_NODES}) = CellNodes
GridComponent4TypeProperty(::Type{ITEMTYPE_CELL},::Type{PROPERTY_VOLUME}) = CellVolumes
GridComponent4TypeProperty(::Type{ITEMTYPE_CELL},::Type{PROPERTY_REGION}) = CellRegions
GridComponent4TypeProperty(::Type{ITEMTYPE_CELL},::Type{PROPERTY_GEOMETRY}) = CellGeometries
GridComponent4TypeProperty(::Type{ITEMTYPE_CELL},::Type{PROPERTY_UNIQUEGEOMETRY}) = UniqueCellGeometries

GridComponent4TypeProperty(::Type{ITEMTYPE_FACE},::Type{PROPERTY_NODES}) = FaceNodes
GridComponent4TypeProperty(::Type{ITEMTYPE_FACE},::Type{PROPERTY_VOLUME}) = FaceVolumes
GridComponent4TypeProperty(::Type{ITEMTYPE_FACE},::Type{PROPERTY_REGION}) = FaceRegions
GridComponent4TypeProperty(::Type{ITEMTYPE_FACE},::Type{PROPERTY_GEOMETRY}) = FaceGeometries
GridComponent4TypeProperty(::Type{ITEMTYPE_FACE},::Type{PROPERTY_UNIQUEGEOMETRY}) = UniqueFaceGeometries

GridComponent4TypeProperty(::Type{ITEMTYPE_BFACE},::Type{PROPERTY_NODES}) = BFaceNodes
GridComponent4TypeProperty(::Type{ITEMTYPE_BFACE},::Type{PROPERTY_VOLUME}) = BFaceVolumes
GridComponent4TypeProperty(::Type{ITEMTYPE_BFACE},::Type{PROPERTY_REGION}) = BFaceRegions
GridComponent4TypeProperty(::Type{ITEMTYPE_BFACE},::Type{PROPERTY_GEOMETRY}) = BFaceGeometries
GridComponent4TypeProperty(::Type{ITEMTYPE_BFACE},::Type{PROPERTY_UNIQUEGEOMETRY}) = UniqueBFaceGeometries

GridComponent4TypeProperty(::Type{ITEMTYPE_EDGE},::Type{PROPERTY_NODES}) = EdgeNodes
GridComponent4TypeProperty(::Type{ITEMTYPE_EDGE},::Type{PROPERTY_VOLUME}) = EdgeVolumes
GridComponent4TypeProperty(::Type{ITEMTYPE_EDGE},::Type{PROPERTY_REGION}) = EdgeRegions
GridComponent4TypeProperty(::Type{ITEMTYPE_EDGE},::Type{PROPERTY_GEOMETRY}) = EdgeGeometries
GridComponent4TypeProperty(::Type{ITEMTYPE_EDGE},::Type{PROPERTY_UNIQUEGEOMETRY}) = UniqueEdgeGeometries

GridComponent4TypeProperty(::Type{ITEMTYPE_BEDGE},::Type{PROPERTY_NODES}) = BEdgeNodes
GridComponent4TypeProperty(::Type{ITEMTYPE_BEDGE},::Type{PROPERTY_VOLUME}) = BEdgeVolumes
GridComponent4TypeProperty(::Type{ITEMTYPE_BEDGE},::Type{PROPERTY_REGION}) = BEdgeRegions
GridComponent4TypeProperty(::Type{ITEMTYPE_BEDGE},::Type{PROPERTY_GEOMETRY}) = BEdgeGeometries
GridComponent4TypeProperty(::Type{ITEMTYPE_BEDGE},::Type{PROPERTY_UNIQUEGEOMETRY}) = UniqueBEdgeGeometries

function get_facegrid(source_grid::ExtendableGrid{Tc,Ti}) where {Tc,Ti}
    facegrid=ExtendableGrid{Tc,Ti}()
    facegrid[Coordinates]=source_grid[Coordinates]
    facegrid[CellNodes]=source_grid[FaceNodes]
    facegrid[CoordinateSystem]=source_grid[CoordinateSystem]
    facegrid[CellGeometries]=source_grid[FaceGeometries]
    facegrid[UniqueCellGeometries]=source_grid[UniqueFaceGeometries]
    facegrid[CellRegions] = source_grid[FaceRegions]
    # todo: facegrid[CellFaces] = source_grid[FaceEdges]
    return facegrid
end

function get_bfacegrid(source_grid::ExtendableGrid{Tc,Ti})  where {Tc,Ti}
    bfacegrid=ExtendableGrid{Tc,Ti}()
    bfacegrid[Coordinates]=source_grid[Coordinates]
    bfacegrid[CellNodes]=source_grid[BFaceNodes]
    bfacegrid[CoordinateSystem]=source_grid[CoordinateSystem]
    bfacegrid[CellGeometries]=source_grid[BFaceGeometries]
    bfacegrid[UniqueCellGeometries]=source_grid[UniqueBFaceGeometries]
    bfacegrid[CellRegions] = source_grid[BFaceRegions]
    # todo: bfacegrid[CellFaces] = source_grid[BFaceEdges] (or sub-view of FaceEdges ?)
    return bfacegrid
end

function get_edgegrid(source_grid::ExtendableGrid{Tc,Ti}) where {Tc,Ti}
    edgegrid=ExtendableGrid{Tc,Ti}()
    edgegrid[Coordinates]=source_grid[Coordinates]
    edgegrid[CellNodes]=source_grid[EdgeNodes]
    edgegrid[CoordinateSystem]=source_grid[CoordinateSystem]
    edgegrid[CellGeometries]=source_grid[EdgeGeometries]
    edgegrid[UniqueCellGeometries]=source_grid[UniqueedgeGeometries]
    edgegrid[CellRegions] = source_grid[EdgeRegions]
    return edgegrid
end

# show function for ExtendableGrids and defined Components in its Dict
function showmore(io::IO, xgrid::ExtendableGrid{Tc,Ti}) where {Tc,Ti}

    dim = size(xgrid[Coordinates],1)
    nnodes = num_sources(xgrid[Coordinates])
    ncells = num_sources(xgrid[CellNodes])

	println("ExtendableGrid information");
    println("==========================");
	println("dim: $(dim)")
	println("nnodes: $(nnodes)")
    println("ncells: $(ncells)")
    if haskey(xgrid.components,FaceNodes)
        nfaces = num_sources(xgrid[FaceNodes])
        println("nfaces: $(nfaces)")
    else
        println("nfaces: (FaceNodes not instantiated)")
    end
    if haskey(xgrid.components,EdgeNodes)
        nedges = num_sources(xgrid[EdgeNodes])
        println("nedges: $(nedges)")
    else
        println("nedges: (EdgeNodes not instantiated)")
    end
    println("")
    println("Components");
    println("==========");
    for tuple in xgrid.components
        println("> $(tuple[1])")
    end
end

# FaceNodes = nodes for each face (implicitly defines the enumerations of faces)
function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{FaceNodes}) where {Tc,Ti}

    xCellNodes = xgrid[CellNodes]
    ncells = num_sources(xCellNodes)
    nnodes = num_sources(xgrid[Coordinates])
    xCellGeometries = xgrid[CellGeometries]

    # transpose CellNodes to get NodeCells
    xNodeCells = atranspose(xCellNodes)
    max_ncell4node::Ti = max_num_targets_per_source(xNodeCells)

    # instantiate new empty adjacency fields
    xFaceCells = zeros(Ti,0) # cells are appended and at the end rewritten into 2,nfaces array

    # find unique face enumeration rules
    EG = unique(xCellGeometries)
    dim::Ti = dim_element(EG[1])
    face_rules = Array{Array{Ti,2},1}(undef,length(EG))
    maxfacenodes::Ti = 0
    FEG = []
    for j = 1 : length(EG)
        face_rules[j] = local_cellfacenodes(EG[j])
        maxfacenodes = max(size(face_rules[j],2),maxfacenodes)
        for k = 1 : num_faces(EG[j])
            append!(FEG,[facetype_of_cellface(EG[j],j)])
        end
    end
    unique!(FEG)

    # check if only one type of cell geometry is present in grid
    if length(EG) == 1
        singleEG = true
    else
        singleEG = false
    end
    # check if only one type of face geometry is present in grid
    if length(FEG) == 1
        singleFEG = true
    else
        singleFEG = false
    end

    if singleFEG
        xFaceNodes = zeros(Ti,0)
    else
        xFaceNodes = VariableTargetAdjacency(Ti)
        xFaceGeometries::Array{DataType,1} = []
    end
    if singleEG == true && singleFEG == true
        # only one geometry type allows for much faster code
        xCellFaces = zeros(Ti,num_faces(EG[1]),ncells)
        xCellFaceSigns = zeros(Ti,num_faces(EG[1]),ncells)
    else
        xCellFaces = VariableTargetAdjacency(Ti)
        xCellFaceSigns = VariableTargetAdjacency(Ti)
        # pre-allocate xCellFaces
        cellEG = xCellGeometries[1]
        for cell = 1 : ncells
            cellEG = xCellGeometries[cell]
            append!(xCellFaces,zeros(Ti,num_faces(cellEG)))
            append!(xCellFaceSigns,zeros(Ti,num_faces(cellEG)))
        end
    end

    # temporary variables
    # pre-initialised ready to work for singleEG and singleFEG
    node::Ti = 0
    face::Ti = 0
    cell::Ti = 0
    cell2::Ti = 0
    cellEG = EG[1]
    cell2EG = EG[1]
    faceEG = FEG[1]
    faceEG2 = FEG[1]
    face_rule::Array{Ti,2} = face_rules[1]
    face_rule2::Array{Ti,2} = face_rules[1]
    iEG::Ti = 1
    nneighbours::Ti = 0
    faces_per_cell::Ti = num_faces(cellEG)
    faces_per_cell2::Ti = num_faces(cellEG)
    nodes_per_cellface::Ti = num_nodes(faceEG)
    current_item::Array{Ti,1} = zeros(Ti,maxfacenodes) # should be large enough to store largest nnodes per cellface
    flag4item::Array{Bool,1} = zeros(Bool,nnodes)
    no_neighbours_found::Bool = true
    same_face::Bool = false

    # loop over cells
    for cell = 1 : ncells
        # find EG index for geometry
        if !singleEG
            cellEG = xCellGeometries[cell]
            faces_per_cell = num_faces(cellEG)
            for j=1:length(EG)
                if cellEG == EG[j]
                    iEG = j
                    break;
                end
            end
            face_rule = face_rules[iEG]
        end

        # loop over cell faces
        for k = 1 : faces_per_cell

            # check if face is already known to cell
            if xCellFaces[k,cell] > 0
                continue;
            end

            # get face geometry
            if !singleFEG
                faceEG = facetype_of_cellface(cellEG, k)
                nodes_per_cellface = num_nodes(faceEG)
            end

            # flag face nodes and commons4cells
            for j = nodes_per_cellface:-1:1
                node = xCellNodes[face_rule[j,k],cell]
                current_item[j] = node
                flag4item[node] = true;
            end

            # get neighbours for first node
            nneighbours = num_targets(xNodeCells,node)

            # loop over neighbours
            no_neighbours_found = true
            for n = 1 : nneighbours
                cell2 = xNodeCells[n,node]

                # skip if cell2 is the same as cell
                if (cell == cell2)
                    continue;
                end

                # find face enumeration rule
                if !singleEG
                    cell2EG = xCellGeometries[cell2]
                    faces_per_cell2 = num_faces(cell2EG)
                    for j=1:length(EG)
                        if cell2EG == EG[j]
                            iEG = j
                            break;
                        end
                    end
                    face_rule2 = face_rules[iEG]
                end

                # loop over faces face2 of adjacent cell2
                for f2 = 1 : faces_per_cell2

                    # check if face f2 is already known to cell2
                    if xCellFaces[f2,cell2] != 0
                        continue;
                    end

                    # check if face f2 has same geometry
                    if !singleFEG
                        faceEG2 = facetype_of_cellface(cell2EG, f2)
                        if faceEG != faceEG2
                            continue;
                        end
                    end

                    # otherwise compare nodes of face and face2
                    same_face = true
                    for j = 1 : nodes_per_cellface
                        if flag4item[xCellNodes[face_rule2[j,f2],cell2]] == false
                            same_face = false
                            break;
                        end
                    end

                    # if all nodes are the same, register face
                    if (same_face)
                        no_neighbours_found = false
                        face += 1
                        push!(xFaceCells,cell)
                        push!(xFaceCells,cell2)
                        if singleEG == false
                            xCellFaces.colentries[xCellFaces.colstart[cell]+k-1] = face
                            xCellFaces.colentries[xCellFaces.colstart[cell2]+f2-1] = face
                            xCellFaceSigns.colentries[xCellFaceSigns.colstart[cell]+k-1] = 1
                            xCellFaceSigns.colentries[xCellFaceSigns.colstart[cell2]+f2-1] = -1
                            if singleFEG == false
                                push!(xFaceGeometries,faceEG)
                            end
                        else
                            xCellFaces[k,cell] = face
                            xCellFaces[f2,cell2] = face
                            xCellFaceSigns[k,cell] = 1
                            xCellFaceSigns[f2,cell2] = -1
                        end
                        append!(xFaceNodes,view(current_item,1:nodes_per_cellface))
                        break;
                    end
                end
            end

            # if no common neighbour cell is found, register face (boundary faces)
            if no_neighbours_found == true
                face += 1
                push!(xFaceCells,cell)
                push!(xFaceCells,0)
                if singleEG == false
                    xCellFaces.colentries[xCellFaces.colstart[cell]+k-1] = face
                    xCellFaceSigns.colentries[xCellFaceSigns.colstart[cell]+k-1] = 1
                    if singleFEG == false
                        push!(xFaceGeometries,faceEG)
                    end
                else
                    xCellFaces[k,cell] = face
                    xCellFaceSigns[k,cell] = 1
                end
                append!(xFaceNodes,view(current_item,1:nodes_per_cellface))
            end

            # reset flag4item
            for j = 1:nodes_per_cellface
                flag4item[current_item[j]] = false
            end
        end
    end

    if singleFEG
        xFaceNodes = reshape(xFaceNodes,(nodes_per_cellface,Ti(length(xFaceNodes)/nodes_per_cellface)))
        xgrid[FaceGeometries] = VectorOfConstants(facetype_of_cellface(EG[1], 1), face)
    else
        xgrid[FaceGeometries] = xFaceGeometries
    end
    xgrid[CellFaces] = xCellFaces
    xgrid[CellFaceSigns] = xCellFaceSigns
    xgrid[FaceCells] = reshape(xFaceCells,(2,face))
    xFaceNodes
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{NodePatchGroups}) where {Tc,Ti}
    xCellNodes::Adjacency = xgrid[CellNodes]
    xNodeCells = atranspose(xCellNodes)
    nnodes = size(xgrid[Coordinates],2)
    ncells = num_sources(xCellNodes)
    ncells4node::Ti = 0
    group4node = zeros(Ti,nnodes)
    cgroup::Ti = 0
    cell_in_group = zeros(Bool,ncells)
    take_into = false
    cgroup = 0
    while minimum(group4node) == 0
        cell_in_group .= false
        cgroup += 1
        for node = 1 : nnodes
            if group4node[node] == 0
                ncells4node = num_targets(xNodeCells,node)
                take_into = true
                for c = 1 : ncells4node
                    if cell_in_group[xNodeCells[c,node]] == true
                        take_into = false
                        break
                    end
                end
                if take_into
                    group4node[node] = cgroup
                    for c = 1 : ncells4node
                        cell_in_group[xNodeCells[c,node]] = true
                    end
                end
            end
        end
    end
    group4node
end

# FaceNodes = nodes for each face (implicitly defines the enumerations of faces)
function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{EdgeNodes}) where {Tc,Ti}

    xCellNodes = xgrid[CellNodes]
    ncells::Ti = num_sources(xCellNodes)
    nnodes::Ti = num_sources(xgrid[Coordinates])
    xCellGeometries = xgrid[CellGeometries]
    dim::Ti = dim_element(xCellGeometries[1])

    # in 1D edges = cells
    if dim == 1
        xgrid[EdgeNodes] = xgrid[CellNodes]
        xgrid[EdgeGeometries] = xgrid[CellGeometries]
        xgrid[CellEdges] = reshape(collect(Ti,1:size(xCellNodes,2)), (1,size(xCellNodes,2)))
        xgrid[EdgeCells] = xgrid[CellEdges]
        return xgrid[EdgeNodes]
    end

    # in 2D edges = faces
    if dim == 2
        xgrid[CellEdges] = xgrid[CellFaces]
        xgrid[EdgeCells] = xgrid[FaceCells]
        xgrid[EdgeNodes] = xgrid[FaceNodes]
        xgrid[EdgeGeometries] = xgrid[FaceGeometries]
        return xgrid[EdgeNodes]
#        return  prepare_edges!(xgrid) # keep compatible to VoronoiFVM
    end

    # transpose CellNodes to get NodeCells
    xNodeCells = atranspose(xCellNodes)
    max_ncell4node::Ti = max_num_targets_per_source(xNodeCells)

    # init EdgeCells
    xEdgeCells = VariableTargetAdjacency(Ti)

    # find unique edge enumeration rules for each cell geometry
    EG::Array{DataType,1} = unique(xCellGeometries)
    edge_rules::Array{Array{Ti,2},1} = Array{Array{Ti,2},1}(undef,length(EG))
    maxedgenodes::Ti = 0
    for j = 1 : length(EG)
        edge_rules[j] = local_celledgenodes(EG[j])
        maxedgenodes = max(size(edge_rules[j],1),maxedgenodes)
    end

    # pre-allocate xCellEdges and xCellEdgeSigns
    if length(EG) == 1
        singleEG = true
        xCellEdges = zeros(Ti,num_edges(EG[1]),ncells)
        xCellEdgeSigns = zeros(Ti,num_edges(EG[1]),ncells)
    else
        singleEG = false
        xCellEdges = VariableTargetAdjacency(Ti)
        cellEG = xCellGeometries[1]
        for cell = 1 : ncells
            cellEG = xCellGeometries[cell]
            append!(xCellEdges,zeros(Ti,num_edges(cellEG)))
        end
        xCellEdgeSigns = deepcopy(xCellEdges)
    end

    # init xEdgeNodes
    xEdgeNodes::Array{Ti,1} = zeros(Ti,0)

    edge_rule::Array{Ti,2} = edge_rules[1]
    edge_rule2::Array{Ti,2} = edge_rules[1]
    current_item::Array{Ti,1} = zeros(Ti,2)
    flag4item::Array{Bool,1} = zeros(Bool,nnodes)
    cellEG = EG[1]
    node::Ti = 0
    cell2::Ti = 0
    nneighbours::Ti = 0
    edges_per_cell::Ti = num_edges(EG[1])
    edges_per_cell2::Ti = num_edges(EG[1])
    common_nodes::Ti = 0
    cells_with_common_edge::Array{Ti,1} = zeros(Ti,max_ncell4node)
    pos_in_cells_with_common_edge::Array{Ti,1} = zeros(Ti,max_ncell4node)
    sign_in_cells_with_common_edge::Array{Ti,1} = zeros(Ti,max_ncell4node)
    ncells_with_common_edge::Ti = 0
    edge::Ti = 0
    iEG::Ti = 0

    # loop over cells
    for cell = 1 : ncells

        # find EG index for geometry
        if singleEG == false
            cellEG = xCellGeometries[cell]
            edges_per_cell = num_edges(cellEG)
            for j=1:length(EG)
                if cellEG == EG[j]
                    iEG = j
                    break;
                end
            end
            edge_rule = edge_rules[iEG]
        end

        # loop over cell edges
        for k = 1 : edges_per_cell

            # check if edge is already known to cell
            if xCellEdges[k,cell] > 0
                continue;
            end
            ncells_with_common_edge = 1
            cells_with_common_edge[1] = cell
            pos_in_cells_with_common_edge[1] = k
            sign_in_cells_with_common_edge[1] = 1

            # flag edge nodes and commons4cells
            for j = 2 : - 1 : 1
                node = xCellNodes[edge_rule[j,k],cell]
                current_item[j] = node
                flag4item[node] = true;
            end

            # get first node and its neighbours
            nneighbours = num_targets(xNodeCells,node)

            # loop over neighbours
            for n = 1 : nneighbours
                cell2 = xNodeCells[n,node]

                # skip if cell2 is the same as cell
                if (cell == cell2)
                    continue;
                end

                # loop over edges of cell2
                if singleEG == false
                    cellEG = xCellGeometries[cell2]
                    edges_per_cell2 = num_edges(cellEG)

                    # find edge enumeration rule
                    for j=1:length(EG)
                        if cellEG == EG[j]
                            iEG = j
                            break;
                        end
                    end
                    edge_rule2 = edge_rules[iEG]
                end

                for f2 = 1 : edges_per_cell2
                    # compare nodes of edge and edge2
                    common_nodes = 0
                    for j = 1 : 2
                        if flag4item[xCellNodes[edge_rule2[j,f2],cell2]]
                            common_nodes += 1
                        else
                            continue;
                        end
                    end

                    # if all nodes are the same, register edge
                    if (common_nodes == 2)
                        ncells_with_common_edge += 1
                        cells_with_common_edge[ncells_with_common_edge] = cell2
                        pos_in_cells_with_common_edge[ncells_with_common_edge] = f2
                        if xCellNodes[edge_rule2[1,f2],cell2] == current_item[1]
                            sign_in_cells_with_common_edge[ncells_with_common_edge] = 1
                        else
                            sign_in_cells_with_common_edge[ncells_with_common_edge] = -1
                        end
                    end
                end
            end

            # register edge
            edge += 1
            for c = 1 : ncells_with_common_edge
                xCellEdges[pos_in_cells_with_common_edge[c],cells_with_common_edge[c]] = edge
                xCellEdgeSigns[pos_in_cells_with_common_edge[c],cells_with_common_edge[c]] = sign_in_cells_with_common_edge[c]
            end
            append!(xEdgeCells,view(cells_with_common_edge,1:ncells_with_common_edge))
            append!(xEdgeNodes,current_item)

            #reset flag4item
            for j = 1 : 2
                flag4item[current_item[j]] = false
            end
        end
    end
    xgrid[CellEdges] = xCellEdges
    xgrid[EdgeCells] = xEdgeCells
    xgrid[CellEdgeSigns] = xCellEdgeSigns
    xgrid[EdgeGeometries] = VectorOfConstants(Edge1D,edge)
    reshape(xEdgeNodes,(2,edge))
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{CellFaceOrientations}) where {Tc,Ti}
    xCellFaceSigns = xgrid[CellFaceSigns]
    xCellGeometries = xgrid[CellGeometries]
    xCellFaces = xgrid[CellFaces]
    xFaceNodes = xgrid[FaceNodes]
    xCellNodes = xgrid[CellNodes]
    ncells = num_sources(xCellNodes)

    EG = unique(xCellGeometries)
    face_rules = Array{Array{Ti,2},1}(undef,length(EG))
    maxfacenodes = 0
    for j = 1 : length(EG)
        face_rules[j] = local_cellfacenodes(EG[j])
        maxfacenodes = max(size(face_rules[j],2),maxfacenodes)
    end
    singleEG = false
    if typeof(xCellFaceSigns) <: VariableTargetAdjacency
        xCellFaceOrientations = deepcopy(xCellFaceSigns)
    else
        singleEG = true
        xCellFaceOrientations = zeros(Ti, size(xCellFaceSigns,1), size(xCellFaceSigns,2))
    end

    cellEG = EG[1]
    face_rule = face_rules[1]
    ncellfaces::Ti = 0
    nfacenodes::Ti = 0
    face::Ti = 0
    facenodes = zeros(Ti,maxfacenodes)
    found_configuration::Bool = false
    n::Ti = 0
    iEG::Ti = 0
    for cell = 1 : ncells
        cellEG = xCellGeometries[cell]

        # find EG index for geometry
        for j=1:length(EG)
            if cellEG == EG[j]
                iEG = j
                break;
            end
        end
        face_rule = face_rules[iEG] # determines local enumeration of faces

        # determine orientation
        ncellfaces = num_targets(xCellFaces,cell)
        for j = 1 : ncellfaces
            face = xCellFaces[j,cell]
            nfacenodes = num_targets(xFaceNodes,face)
            if xCellFaceSigns[j,cell] == 1
                if singleEG == false
                    xCellFaceOrientations.colentries[xCellFaceOrientations.colstart[cell]+j-1] = 1
                else
                    xCellFaceOrientations[j, cell] = 1
                end
            else
                for k = 1 : nfacenodes
                    facenodes[nfacenodes + 1 - k] = xFaceNodes[k,face]
                end
                found_configuration = false
                n = 0
                while !found_configuration
                    n += 1
                    if facenodes[n] == xCellNodes[face_rule[1,j],cell]
                        found_configuration = true
                    end
                end
                n = mod(n-1,3) + 1
                if singleEG == false
                    xCellFaceOrientations.colentries[xCellFaceOrientations.colstart[cell]+j-1] = 1+n
                else
                    xCellFaceOrientations[j, cell] = 1+n
                end
            end
        end

    end
    return xCellFaceOrientations
end

# FaceEdges = Edges for each face (in 3D)
function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{FaceEdges}) where {Tc,Ti}
    xFaceNodes = xgrid[FaceNodes]
    xFaceCells = xgrid[FaceCells]
    xEdgeNodes = xgrid[EdgeNodes]
    xCellEdges = xgrid[CellEdges]
    xFaceEdges = VariableTargetAdjacency(Ti)
    xFaceGeometries = xgrid[FaceGeometries]
    nfaces::Ti = num_sources(xFaceNodes)
    nnodes::Ti = size(xgrid[Coordinates],2)

    # find unique edge enumeration rules
    EG = unique(xFaceGeometries)
    edge_rules = Array{Array{Ti,2},1}(undef,length(EG))
    maxedgenodes = 0
    for j = 1 : length(EG)
        edge_rules[j] = local_cellfacenodes(EG[j]) # face edges are faces of the face
        maxedgenodes = max(size(edge_rules[j],2),maxedgenodes)
    end
    edge_rule::Array{Ti,2} = edge_rules[1]
    if length(EG) == 1
        singleEG = true
        xFaceEdges = zeros(Ti,num_faces(EG[1]),nfaces)
    else
        singleEG = false
        faceEG = xCellGeometries[1]
        for face = 1 : nfaces
            faceEG = xCellGeometries[cell]
            append!(xCellEdges,zeros(Ti,num_faces(cellEG)))
        end
    end

    nfacenodes::Ti = num_targets(xFaceNodes,1)
    ncelledges::Ti = 0
    nedgenodes::Ti = 0
    nfaceedges::Ti = num_edges(EG[1])
    node::Ti = 0
    cell::Ti = 0
    edge::Ti = 0
    faceedge::Ti = 0
    edge_is_in_face::Bool = false
    found_pos::Bool = false
    flag4face = zeros(Bool,nnodes)
    flag4edge = zeros(Bool,nnodes)
    faceEG = EG[1]
    iEG::Ti = 1
    pos::Ti = 0
    for face = 1 : nfaces

        if singleEG == false
            faceEG = xFaceGeometries[face]
            nfaceedges = num_edges(faceEG)
            # find EG index for geometry
            for j=1:length(EG)
                if faceEG == EG[j]
                    iEG = j
                    break;
                end
            end
            edge_rule = edge_rules[iEG] # determines local enumeration of face edges
            nfacenodes = num_targets(xFaceNodes,face)
        end

        # mark nodes of face
        for j = 1 : nfacenodes
            node = xFaceNodes[j, face]
            flag4face[node] = true;
        end

        # find edges in first adjacent cell
        cell = xFaceCells[1,face]
        ncelledges = num_targets(xCellEdges,cell)
        faceedge = 0
        for cedge = 1 : ncelledges
            edge = xCellEdges[cedge,cell]
            nedgenodes = num_targets(xEdgeNodes,edge)
            edge_is_in_face = true
            for j = 1 : nedgenodes
                if flag4face[xEdgeNodes[j, edge]] == false
                    edge_is_in_face = false
                    break;
                end
            end

            if edge_is_in_face

                # register edge
                # it is important obey same local ordering as in edge_rule
                # to ensure correct dof handling (e.g. for P2-FEM on boundary)

                # mark nodes of edge
                for j = 1 : nedgenodes
                    node = xEdgeNodes[j, edge]
                    flag4edge[node] = true;
                end

                pos = 0
                found_pos = false
                while found_pos == false
                    pos += 1
                    found_pos = true
                    for k = 1 : nedgenodes
                        if flag4edge[xFaceNodes[edge_rule[k,pos], face]] == false
                            found_pos = false
                            break;
                        end
                    end
                end
                if singleEG
                    xFaceEdges[pos,face] = edge
                else
                    xFaceEdges.colentries[xFaceEdges.colstart[face]+pos-1] = edge
                end

                # reset flag4edge
                for j = 1 : nedgenodes
                    node = xEdgeNodes[j, edge]
                    flag4edge[node] = false;
                end

                faceedge += 1
                if faceedge == nfaceedges
                    break;
                end
            end
        end

        #reset flag4face
        for j = 1 : nfacenodes
            node = xFaceNodes[j, face]
            flag4face[node] = false;
        end

    end

    xFaceEdges
end

# CellFaces = faces for each cell
function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{CellFaces}) where {Tc,Ti}
    ExtendableGrids.instantiate(xgrid, FaceNodes)
    xgrid[CellFaces]
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{FaceGeometries}) where {Tc,Ti}
    ExtendableGrids.instantiate(xgrid, FaceNodes)
    xgrid[FaceGeometries]
end

# CellEdges = edges for each cell
function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{CellEdges}) where {Tc,Ti}
    ExtendableGrids.instantiate(xgrid, EdgeNodes)
    xgrid[CellEdges]
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{EdgeCells}) where {Tc,Ti}
    ExtendableGrids.instantiate(xgrid, EdgeNodes)
    xgrid[EdgeCells]
end

# CellEdgeSigns = edges for each cell
function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{CellEdgeSigns}) where {Tc,Ti}
    ExtendableGrids.instantiate(xgrid, EdgeNodes)
    xgrid[CellEdgeSigns]
end

# CellFaceSigns = orientation signs for each face on each cell
function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{CellFaceSigns}) where {Tc,Ti}
    ExtendableGrids.instantiate(xgrid, FaceNodes)
    xgrid[CellFaceSigns]
end

function collectVolumes4Geometries(T::Type{<:Real}, xgrid::ExtendableGrid{Tc,Ti}, ItemType) where {Tc,Ti}
    # get links to other stuff
    xCoordinates = xgrid[Coordinates]
    xCoordinateSystem = xgrid[CoordinateSystem]
    xItemNodes = xgrid[GridComponent4TypeProperty(ItemType,PROPERTY_NODES) ]
    xGeometries = xgrid[GridComponent4TypeProperty(ItemType,PROPERTY_GEOMETRY) ]
    EG = xgrid[GridComponent4TypeProperty(ItemType,PROPERTY_UNIQUEGEOMETRY) ]
    nitems::Ti = num_sources(xItemNodes)

    # get Volume4ElemType handlers
    handlers = Array{Function,1}(undef, length(EG))
    for j = 1: length(EG)
        handlers[j] = Volume4ElemType(xCoordinates, xItemNodes, EG[j], xCoordinateSystem)
    end

    # init Volumes
    xVolumes::Array{T,1} = zeros(T,nitems)

    # loop over items and call handlers
    iEG::Ti = 1
    itemEG = EG[1]
    for item = 1 : nitems
        if length(EG) > 1
            itemEG = xGeometries[item]
            for j=1:length(EG)
                if itemEG == EG[j]
                    iEG = j
                    break;
                end
            end
        end
        xVolumes[item] = handlers[iEG](item)
    end

    xVolumes
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{CellVolumes}) where {Tc,Ti}
    collectVolumes4Geometries(Tc, xgrid, ITEMTYPE_CELL)
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{FaceVolumes}) where {Tc,Ti}
    collectVolumes4Geometries(Tc, xgrid, ITEMTYPE_FACE)
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{BFaceVolumes}) where {Tc,Ti}
    collectVolumes4Geometries(Tc, xgrid, ITEMTYPE_BFACE)
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{EdgeVolumes}) where {Tc,Ti}
    collectVolumes4Geometries(Tc, xgrid, ITEMTYPE_EDGE)
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{BEdgeVolumes}) where {Tc,Ti}
    collectVolumes4Geometries(Tc, xgrid, ITEMTYPE_BEDGE)
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{BFaceFaces}) where {Tc,Ti}
    # get links to other stuff
    xCoordinates = xgrid[Coordinates]
    xFaceNodes = xgrid[FaceNodes]
    xBFaceNodes = xgrid[BFaceNodes]
    nnodes::Ti = num_sources(xCoordinates)
    nbfaces::Ti = num_sources(xBFaceNodes)

    # init BFaceFaces
    xBFaceFaces::Array{Ti,1} = zeros(Ti,nbfaces)
    #xBFaceGeometries = xgrid[BFaceGeometries]
    #if typeof(xBFaceGeometries) == VectorOfConstants{DataType}
    #    EG = xBFaceGeometries[1]
    #    xBFaceGeometries = Array{DataType,1}(undef,nbfaces)
    #    for j = 1 : nbfaces
    #        xBFaceGeometries[j] = EG
    #    end
    #end

    # transpose FaceNodes to get NodeFaces
    xNodeFaces = atranspose(xFaceNodes)

    flag4item::Array{Bool,1} = zeros(Bool,nnodes)
    nodes_per_bface::Ti = 0
    nodes_per_face::Ti = 0
    common_nodes::Ti = 0
    node::Ti = 0
    nneighbours::Ti = 0
    for bface = 1 : nbfaces
        nodes_per_bface = num_targets(xBFaceNodes,bface)
        for j = 1 : nodes_per_bface
            node = xBFaceNodes[j,bface]
            flag4item[node] = true
        end

        # get faces for last node of bface
        nneighbours = num_targets(xNodeFaces,node)

        # loop over faces and find the one that matches the bface
        for n = 1 : nneighbours
            face = xNodeFaces[n,node]
            nodes_per_face = num_targets(xFaceNodes,face)
            common_nodes = 0
            for k = 1 : nodes_per_face
                if flag4item[xFaceNodes[k,face]] == true
                    common_nodes += 1
                else
                    break
                end
            end
            if common_nodes == nodes_per_face
                xBFaceFaces[bface] = face
                break
            end
        end

        if xBFaceFaces[bface] == 0
            println("WARNING(BFaceFaces): found no matching face for bface $bface with nodes $(xBFaceNodes[:,bface])")
        end

        for j = 1 : nodes_per_bface
            flag4item[xBFaceNodes[j,bface]] = false
        end
    end

    # enforce that BFaceNodes have same ordering as FaceNodes
    newBFaceNodes = deepcopy(xBFaceNodes)
    for bface = 1 : nbfaces
        nodes_per_face = num_targets(xBFaceNodes,bface)
        for j = 1 : nodes_per_bface
            if typeof(xBFaceNodes) <: VariableTargetAdjacency
                newBFaceNodes.colentries[newBFaceNodes.colstart[bface]+j-1] = xFaceNodes[j,xBFaceFaces[bface]]
            else
                newBFaceNodes[j,bface] = xFaceNodes[j,xBFaceFaces[bface]]
            end
        end
    end
    xgrid[BFaceNodes] = newBFaceNodes

   # xgrid[BFaceGeometries] = xBFaceGeometries
    xBFaceFaces
end

function instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{BEdgeNodes}) where {Tc,Ti}

    dim = size(xgrid[Coordinates],1)
    if dim==1
        xgrid[BEdgeEdges]=zeros(Ti,0)
        xgrid[BEdgeNodes]=zeros(Ti,0,0)
        return xgrid[BEdgeNodes]
    end

    if dim==2
        xgrid[BEdgeEdges] = xgrid[BFaceFaces]
        xgrid[BEdgeNodes] = xgrid[BFaceNodes]
        return xgrid[BEdgeNodes]
    end

    xBFaceFaces = xgrid[BFaceFaces]
    xEdgeNodes = xgrid[EdgeNodes]
    xFaceEdges = xgrid[FaceEdges]
    xBFaceGeometries = xgrid[BFaceGeometries]
    nbfaces = length(xBFaceFaces)
    xBEdgeEdges = zeros(Ti,0)

    EG = Triangle2D
    edge::Ti = 0
    face::Ti = 0
    nfaceedges::Ti = 0

    for bface = 1 : nbfaces
        EG = xBFaceGeometries[bface]
        nfaceedges = num_edges(EG)
        face = xBFaceFaces[bface]
        for k = 1 : nfaceedges
            edge = xFaceEdges[k,face]
            if !(edge in xBEdgeEdges)
                push!(xBEdgeEdges, edge)
            end

        end
    end

    nbedges = length(xBEdgeEdges)
    xBEdgeNodes = zeros(Ti,2,nbedges)
    for bedge = 1 : nbedges, k = 1 : 2
        xBEdgeNodes[k,bedge] = xEdgeNodes[k,xBEdgeEdges[bedge]]
    end

    xgrid[BEdgeEdges] = xBEdgeEdges
    xBEdgeNodes
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{BEdgeEdges}) where {Tc,Ti}
    ExtendableGrids.instantiate(xgrid, BEdgeNodes)
    xgrid[BEdgeEdges]
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{FaceCells}) where {Tc,Ti}
    ExtendableGrids.instantiate(xgrid, FaceNodes)
    xgrid[FaceCells]
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{FaceRegions}) where {Tc,Ti}
    # interior faces get region number 0, boundary faces get their boundary region
    xBFaceFaces = xgrid[BFaceFaces]
    xBFaceRegions = xgrid[BFaceRegions]
    xFaceRegions = zeros(Ti,num_sources(xgrid[FaceNodes]))
    for j = 1 : length(xBFaceFaces)
        xFaceRegions[xBFaceFaces[j]] = xBFaceRegions[j]
    end
    xFaceRegions
end

### Not a good idea to have them as vectors of constants
# function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{EdgeRegions}) where {Tc,Ti}
#     return VectorOfConstants(Ti(0),num_sources(xgrid[EdgeNodes]))
# end

# function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{BEdgeRegions}) where {Tc,Ti}
#     return VectorOfConstants(Ti(0),num_sources(xgrid[BEdgeNodes]))
# end

# function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{BEdgeGeometries}) where {Tc,Ti}
#     return VectorOfConstants(Edge1D,num_sources(xgrid[BEdgeNodes]))
# end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{BFaceCellPos}) where {Tc,Ti}
    # gets the local position of a bface in the CellFaces row of its adjacent cell

    # get links to other stuff
    xCellFaces = xgrid[CellFaces]
    xFaceCells = xgrid[FaceCells]
    xBFaceFaces = xgrid[BFaceFaces]
    nbfaces = length(xBFaceFaces)

    # init BFaceFaces
    xBFaceCellPos = zeros(Ti,nbfaces)

    cface = 0
    cell = 0
    nfaces4cell = 0
    for bface = 1 : nbfaces
        cface = xBFaceFaces[bface]
        cell = xFaceCells[1,cface]
        nfaces4cell = num_targets(xCellFaces,cell)
        for face = 1 : nfaces4cell
            if cface == xCellFaces[face,cell]
                xBFaceCellPos[bface] = face
                break
            end
        end
    end

    xBFaceCellPos
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{FaceNormals}) where {Tc,Ti}
    dim::Int = size(xgrid[Coordinates],1)
    xCoordinates::Array{Tc,2} = xgrid[Coordinates]
    xFaceNodes::Adjacency{Ti} = xgrid[FaceNodes]
    nfaces::Int = num_sources(xFaceNodes)
    xFaceGeometries::GridEGTypes = xgrid[FaceGeometries]
    xCoordinateSystem = xgrid[CoordinateSystem]
    xFaceNormals::Array{Tc,2} = zeros(Tc,dim,nfaces)
    normal::Array{Tc,1} = zeros(Tc,dim)
    EG = xFaceGeometries[1]
    for face = 1 : nfaces
        EG = xFaceGeometries[face]
        Normal4ElemType!(normal,xCoordinates,xFaceNodes,face,EG,xCoordinateSystem)
        for k = 1 : dim
            xFaceNormals[k, face] = normal[k]
        end
    end
    xFaceNormals
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{EdgeTangents}) where {Tc,Ti}
    dim::Int = size(xgrid[Coordinates],1)
    xCoordinates::Array{Tc,2} = xgrid[Coordinates]
    xEdgeNodes::Adjacency{Ti} = xgrid[EdgeNodes]
    nedges::Int = num_sources(xEdgeNodes)
    xEdgeGeometries::GridEGTypes = xgrid[EdgeGeometries]
    xCoordinateSystem = xgrid[CoordinateSystem]
    xEdgeTangents::Array{Tc,2} = zeros(Tc,dim,nedges)
    EG = xEdgeGeometries[1]
    tangent::Array{Tc,1} = zeros(Tc,dim)
    for edge = 1 : nedges
        EG = xEdgeGeometries[edge]
        Tangent4ElemType!(tangent,xCoordinates,xEdgeNodes,edge,EG,xCoordinateSystem)
        for k = 1 : dim
            xEdgeTangents[k, edge] = tangent[k]
        end
    end
    xEdgeTangents
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{UniqueCellGeometries}) where {Tc,Ti}
    xUniqueCellGeometries = unique(xgrid[CellGeometries])
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{UniqueFaceGeometries}) where {Tc,Ti}
    xUniqueFaceGeometries = unique(xgrid[FaceGeometries])
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{UniqueBFaceGeometries}) where {Tc,Ti}
    xUniqueBFaceGeometries = unique(xgrid[BFaceGeometries])
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{UniqueEdgeGeometries}) where {Tc,Ti}
    xUniqueEdgeGeometries = unique(xgrid[EdgeGeometries])
end

function ExtendableGrids.instantiate(xgrid::ExtendableGrid{Tc,Ti}, ::Type{UniqueBEdgeGeometries}) where {Tc,Ti}
    xUniqueBEdgeGeometries = unique(xgrid[BEdgeGeometries])
end


# MIT License
#
# Copyright (c) 2018 Martin Biel
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

# Scenario-decomposition generation #
# ================================= #
function generate!(stochasticprogram::StochasticProgram{N}, structure::ScenarioDecompositionStructure{N}) where N
    # Generate all stages
    for stage in 2:N
        generate!(stochasticprogram, structure, stage)
    end
    return nothing
end

function generate!(stochasticprogram::TwoStageStochasticProgram, structure::ScenarioDecompositionStructure{2}, stage::Integer)
    stage == 2 || error("Stage $stage not available in two-stage model.")
    # Check generators
    has_generator(stochasticprogram, :stage_1) || error("First-stage problem not defined in stochastic program. Consider @stage 1.")
    has_generator(stochasticprogram, :stage_2) || error("Second-stage problem not defined in stochastic program. Consider @stage 2.")
    # Sanity check on scenario probabilities
    if num_scenarios(structure, stage) > 0
        p = stage_probability(structure, stage)
        abs(p - 1.0) <= 1e-6 || @warn "Scenario probabilities do not add up to one. The probability sum is given by $p"
    end
    # Generate
    generate_horizontal!(scenarioproblems(structure, stage),
                         generator(stochasticprogram, :stage_1),
                         generator(stochasticprogram, :stage_2),
                         stage_parameters(stochasticprogram, 1),
                         stage_parameters(stochasticprogram, 2),
                         structure.decisions[stage-1],
                         subproblem_optimizer(stochasticprogram))
    return nothing
end

function generate_horizontal!(scenarioproblems::ScenarioProblems,
                              stage_one_generator::Function,
                              stage_two_generator::Function,
                              stage_one_params::Any,
                              stage_two_params::Any,
                              decision_map::DecisionMap,
                              optimizer)
    for i in num_subproblems(scenarioproblems)+1:num_scenarios(scenarioproblems)
        push!(scenarioproblems.problems, _WS(stage_one_generator,
                                             stage_two_generator,
                                             stage_one_params,
                                             stage_two_params,
                                             scenario(scenarioproblems,i),
                                             decision_map,
                                             DecisionMap(),
                                             optimizer))
    end
    return nothing
end
function generate_horizontal!(scenarioproblems::DistributedScenarioProblems,
                              stage_one_generator::Function,
                              stage_two_generator::Function,
                              stage_one_params::Any,
                              stage_two_params::Any,
                              ::DecisionMap,
                              optimizer)
    @sync begin
        for w in workers()
            @async remotecall_fetch(
                w,
                scenarioproblems[w-1],
                stage_one_generator,
                stage_two_generator,
                stage_one_params,
                stage_two_params,
                scenarioproblems.decisions[w-1],
                optimizer) do sp, gen_one, gen_two, one_params, two_params, decisions, opt
                    generate_horizontal!(fetch(sp),
                                         gen_one,
                                         gen_two,
                                         one_params,
                                         two_params,
                                         fetch(decisions),
                                         opt)
                end
        end
    end
    return nothing
end

function clear!(structure::ScenarioDecompositionStructure{N}) where N
    # Clear decisions
    clear!(structure.decisions)
    # Clear all stages
    for stage in 2:N
        clear_stage!(structure, stage)
    end
    return nothing
end

function clear_stage!(structure::ScenarioDecompositionStructure{N}, s::Integer) where N
    1 <= s <= N || error("Stage $s not in range 1 to $N.")
    clear!(scenarioproblems(structure, s))
    return nothing
end


using FlightMechanics
using FlightMechanics.Models

##----------------------------------------------------------------------------------------------------
## imports

# Aerodynamics
import FlightMechanics.Models:
    calculate_aerodynamics

# Propulsion
import FlightMechanics.Models:
    get_pfm, get_cj, get_power, get_efficiency, get_tanks,
    get_engine_position, get_engine_orientation, get_engine_gyro_effects,
    calculate_engine

# Aircraft Model
import FlightMechanics.Models:
    get_name, get_wing_area, get_wing_span, get_chord, get_arp,
    get_empty_mass_props,
    set_stick_lon!, set_stick_lat!, set_pedals!, set_thtl!,
    get_controls_ranges_trimmer

##----------------------------------------------------------------------------------------------------
## exports

# Aerodynamics
export F16Aerodynamics, calculate_aerodynamics

# Propulsion
export F16Engine,
    get_pfm, get_cj, get_power, get_efficiency, get_tanks,
    get_engine_position, get_engine_orientation,
    calculate_engine

# FCS
export F16FCS

# Aircraft Model
export F16,
    get_name,
    get_empty_mass_props,
    get_wing_area,
    get_wing_span,
    get_chord,
    get_arp

##----------------------------------------------------------------------------------------------------
## structs

# Aerodynamics
struct F16Aerodynamics<:Aerodynamics    # TODO: CDα, CLα, CYβ... could be included here
    wind_pfm::PointForcesMoments
    wind_coeff_pfm::PointForcesMoments
    body_pfm::PointForcesMoments
    body_coeff_pfm::PointForcesMoments
end

# Propulsion
struct F16Engine<:Engine
    pfm::PointForcesMoments
    cj::Number
    power::Number
    efficiency::Number
    tanks::Array{RigidSolid, 1}
    h::Array{T, 1} where T<:Number  # angular momentum [kg·m²/s]
end

# FCS
mutable struct F16FCS<:FCS      # TODO: implement also rate limits and time constants
    # Cabin controls
    stick_longitudinal::RangeControl
    stick_lateral::RangeControl
    pedals::RangeControl
    thtl::RangeControl

    # Aerodynamic surfaces
    de::RangeControl
    da::RangeControl
    dr::RangeControl
    # Engine
    cpl::RangeControl
    # Configuration
    allow_out_of_range::Bool
    throw_error_on_out_of_range::Bool
end

# Aircraft Model
struct F16<:Aircraft
    mass_props::RigidSolid
    pfm::PointForcesMoments
    aerodynamics::F16Aerodynamics
    propulsion::Propulsion
    fcs::FCS
end

##----------------------------------------------------------------------------------------------------
## constants

const DE_MAX = 25.0  # deg
const DA_MAX = 20.0  # deg  #XXX: In Stevens' book says 21.5 deg (Appendix A Section A.4)
const DR_MAX = 30.0  # deg

##----------------------------------------------------------------------------------------------------
## Aerodynamics

α_data = [-10. -5. 0. 5. 10. 15. 20. 25. 30. 35. 40. 45.]  # deg
β_data = [0. 5. 10. 15. 20. 25. 30.]  # deg

de_data = [-24. -12.   0.  12.  24.]  # deg

# UTILS for interpolations
function get_interp_idx(val, coeff, min_, max_, off_=3)
   s = val * coeff
   k = floor(Int, s)
   k = max(min_, k)
   k = min(k, max_)
   da = s - float(k)
   k += off_
   l = k + Int(sign(da))
   return k, l, da
end

function get_interp_idx2(val, coeff, min_, max_)
   s = abs(val) * coeff
   k = floor(Int, s)
   k = max(min_, k)
   k = min(k, max_)
   da = s - float(k)
   k += 1
   l = k + Int(sign(da))
   return k, l, da
end

function interp1d(val, coeff, min, max, data)
   k, l, da = get_interp_idx(val, coeff, min, max)
   res = data[k] + abs(da) * (data[l] - data[k])
   return res
end

function interp2d(val1, val2, coeff1, coeff2, min1, min2, max1, max2, data, off1=3, off2=3)
   k, l, da = get_interp_idx(val1, coeff1, min1, max1, off1)
   m, n, de = get_interp_idx(val2, coeff2, min2, max2, off2)

   t = data[k, m]
   u = data[k, n]
   v = t + abs(da) * (data[l, m] - t)
   w = u + abs(da) * (data[l, n] - u)
   res = v + (w - v) * abs(de)

   return res
end

function interp2d2(val1, val2, coeff1, coeff2, min1, min2, max1, max2, data)
   k, l, da = get_interp_idx(val1, coeff1, min1, max1)
   m, n, de = get_interp_idx2(val2, coeff2, min2, max2)

   t = data[k, m]
   u = data[k, n]
   v = t + abs(da) * (data[l, m] - t)
   w = u + abs(da) * (data[l, n] - u)
   res = v + (w - v) * abs(de)
   res *= sign(val2)

   return res
end

# DAMPING COEFFICIENTS
CXq_data = [-0.267  -0.110   0.308   1.340   2.080   2.910   2.760   2.050   1.500   1.490   1.830   1.210]
CYr_data = [ 0.882   0.852   0.876   0.958   0.962   0.974   0.819   0.483   0.590   1.210  -0.493  -1.040]
# XXX: -2.27 seems an error in Stenvens. In Morelli it is -0.2270
# CYp_data = [-0.108  -0.108  -0.188   0.110   0.258   0.226   0.344   0.362   0.611   0.529   0.298  -2.270]
CYp_data = [-0.108  -0.108  -0.188   0.110   0.258   0.226   0.344   0.362   0.611   0.529   0.298  -0.227]
CZq_data = [-8.800 -25.800 -28.900 -31.400 -31.200 -30.700 -27.700 -28.200 -29.000 -29.800 -38.300 -35.300]
Clr_data = [-0.126  -0.026   0.063   0.113   0.208   0.230   0.319   0.437   0.680   0.100   0.447  -0.330]
Clp_data = [-0.360  -0.359  -0.443  -0.420  -0.383  -0.375  -0.329  -0.294  -0.230  -0.210  -0.120  -0.100]
Cmq_data = [-7.210  -0.540  -5.230  -5.260  -6.110  -6.640  -5.690  -6.000  -6.200  -6.400  -6.600  -6.000]
Cnr_data = [-0.380  -0.363  -0.378  -0.386  -0.370  -0.453  -0.550  -0.582  -0.595  -0.637  -1.020  -0.840]
Cnp_data = [ 0.061   0.052   0.052  -0.012  -0.013  -0.024   0.050   0.150   0.130   0.158   0.240   0.150]

damp_data = [CXq_data; CYr_data; CYp_data; CZq_data; Clr_data; Clp_data;
             Cmq_data; Cnr_data; Cnp_data]'
damp_data = damp_data'

# CX
# rows: de, columns: alpha
CX_data = [-0.099 -0.081 -0.081 -0.063 -0.025 0.044 0.097 0.113 0.145 0.167 0.174 0.166;
           -0.048 -0.038 -0.040 -0.021  0.016 0.083 0.127 0.137 0.162 0.177 0.179 0.167;
           -0.022 -0.020 -0.021 -0.004  0.032 0.094 0.128 0.130 0.154 0.161 0.155 0.138;
           -0.040 -0.038 -0.039 -0.025  0.006 0.062 0.087 0.085 0.100 0.110 0.104 0.091;
           -0.083 -0.073 -0.076 -0.072 -0.046 0.012 0.024 0.025 0.043 0.053 0.047 0.040]
CX_data = CX_data'

# CZ
# columns: alpha
CZ_data = [0.770 0.241 -0.100 -0.416 -0.731 -1.053 -1.366 -1.646 -1.917 -2.120 -2.248 -2.229]

# Cm
# rows: de columns: alpha
Cm_data = [0.205  0.168  0.186  0.196  0.213  0.251  0.245  0.238  0.252  0.231  0.198  0.192;
           0.081  0.077  0.107  0.110  0.110  0.141  0.127  0.119  0.133  0.108  0.081  0.093;
          -0.046 -0.020 -0.009 -0.005 -0.006  0.010  0.006 -0.001  0.014  0.000 -0.013  0.032;
          -0.174 -0.145 -0.121 -0.127 -0.129 -0.102 -0.097 -0.113 -0.087 -0.084 -0.069 -0.006;
          -0.259 -0.202 -0.184 -0.193 -0.199 -0.150 -0.160 -0.167 -0.104 -0.076 -0.041 -0.005]
Cm_data = Cm_data'

# Cl
# rows: beta columns: alpha
# XXX: found differences in Morelli Cl_data for columns 6-9
# Stevens
Cl_data = [ 0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000;
           -0.001 -0.004 -0.008 -0.012 -0.016 -0.019 -0.020 -0.020 -0.015 -0.008 -0.013 -0.015;
           -0.003 -0.009 -0.017 -0.024 -0.030 -0.034 -0.040 -0.037 -0.016 -0.002 -0.010 -0.019;
           -0.001 -0.010 -0.020 -0.030 -0.039 -0.044 -0.050 -0.049 -0.023 -0.006 -0.014 -0.027;
            0.000 -0.010 -0.022 -0.034 -0.047 -0.046 -0.059 -0.061 -0.033 -0.036 -0.035 -0.035;
            0.007 -0.010 -0.023 -0.034 -0.049 -0.046 -0.068 -0.071 -0.060 -0.058 -0.062 -0.059;
            0.009 -0.011 -0.023 -0.037 -0.050 -0.047 -0.074 -0.079 -0.091 -0.076 -0.077 -0.076]
Cl_data = Cl_data'

# Morelli version
# Cl_data = [ 0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000;
#            -0.001 -0.004 -0.008 -0.012 -0.016 -0.022 -0.022 -0.021 -0.015 -0.008 -0.013 -0.015;
#            -0.003 -0.009 -0.017 -0.024 -0.030 -0.041 -0.045 -0.040 -0.016 -0.002 -0.010 -0.019;
#            -0.001 -0.010 -0.020 -0.030 -0.039 -0.054 -0.057 -0.054 -0.023 -0.006 -0.014 -0.027;
#             0.000 -0.010 -0.022 -0.034 -0.047 -0.060 -0.069 -0.067 -0.033 -0.036 -0.035 -0.035;
#             0.007 -0.010 -0.023 -0.034 -0.049 -0.063 -0.081 -0.079 -0.060 -0.058 -0.062 -0.059;
#             0.009 -0.011 -0.023 -0.037 -0.050 -0.068 -0.089 -0.088 -0.091 -0.076 -0.077 -0.076]
# Cl_data = Cl_data'

# rows: beta columns=alpha
clda_data = [-0.041 -0.052 -0.053 -0.056 -0.050 -0.056 -0.082 -0.059 -0.042 -0.038 -0.027 -0.017;
             -0.041 -0.053 -0.053 -0.053 -0.050 -0.051 -0.066 -0.043 -0.038 -0.027 -0.023 -0.016;
             -0.042 -0.053 -0.052 -0.051 -0.049 -0.049 -0.043 -0.035 -0.026 -0.016 -0.018 -0.014;
             -0.040 -0.052 -0.051 -0.052 -0.048 -0.048 -0.042 -0.037 -0.031 -0.026 -0.017 -0.012;
             -0.043 -0.049 -0.048 -0.049 -0.043 -0.042 -0.042 -0.036 -0.025 -0.021 -0.016 -0.011;
             -0.044 -0.048 -0.048 -0.047 -0.042 -0.041 -0.020 -0.028 -0.013 -0.014 -0.011 -0.010;
             -0.043 -0.049 -0.047 -0.045 -0.042 -0.037 -0.003 -0.013 -0.010 -0.003 -0.007 -0.008]
clda_data /= DA_MAX
clda_data = clda_data'
# Stevens
# rows: beta columns=alpha
cldr_data = [ 0.005  0.017  0.014  0.010 -0.005  0.009  0.019  0.005 -0.000 -0.005 -0.011  0.008;
              0.007  0.016  0.014  0.014  0.013  0.009  0.012  0.005  0.000  0.004  0.009  0.007;
              0.013  0.013  0.011  0.012  0.011  0.009  0.008  0.005 -0.002  0.005  0.003  0.005;  # Morelli diff
              0.018  0.015  0.015  0.014  0.014  0.014  0.014  0.015  0.013  0.011  0.006  0.001;
              0.015  0.014  0.013  0.013  0.012  0.011  0.011  0.010  0.008  0.008  0.007  0.003;
              0.021  0.011  0.010  0.011  0.010  0.009  0.008  0.010  0.006  0.005  0.000  0.001;
              0.023  0.010  0.011  0.011  0.011  0.010  0.008  0.010  0.006  0.014  0.020  0.000]
cldr_data /= DR_MAX
cldr_data = cldr_data'

# Morelli
# rows: beta columns=alpha
# cldr_data = [ 0.005  0.017  0.014  0.010 -0.005  0.009  0.019  0.005 -0.000 -0.005 -0.011  0.008;
#               0.007  0.016  0.014  0.014  0.013  0.009  0.012  0.005  0.000  0.004  0.009  0.007;
#               0.013  0.013  0.011  0.012  0.011  0.009  0.008  0.005  0.000  0.005  0.003  0.005;  # <<
#               0.018  0.015  0.015  0.014  0.014  0.014  0.014  0.015  0.013  0.011  0.006  0.001;
#               0.015  0.014  0.013  0.013  0.012  0.011  0.011  0.010  0.008  0.008  0.007  0.003;
#               0.021  0.011  0.010  0.011  0.010  0.009  0.008  0.010  0.006  0.005  0.000  0.001;
#               0.023  0.010  0.011  0.011  0.011  0.010  0.008  0.010  0.006  0.014  0.020  0.000]
# cldr_data /= DR_MAX
# cldr_data = cldr_data'

# Cn
# rows: beta columns: alpha
Cn_data = [0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000 0.000  0.000  0.000  0.000;
           0.018  0.019  0.018  0.019  0.019  0.018  0.013  0.007 0.004 -0.014 -0.017 -0.033;
           0.038  0.042  0.042  0.042  0.043  0.039  0.030  0.017 0.004 -0.035 -0.047 -0.057;
           0.056  0.057  0.059  0.058  0.058  0.053  0.032  0.012 0.002 -0.046 -0.071 -0.073;
           0.064  0.077  0.076  0.074  0.073  0.057  0.029  0.007 0.012 -0.034 -0.065 -0.041;
           0.074  0.086  0.093  0.089  0.080  0.062  0.049  0.022 0.028 -0.012 -0.002 -0.013;
           0.079  0.090  0.106  0.106  0.096  0.080  0.068  0.030 0.064  0.015  0.011 -0.001]
Cn_data = Cn_data'
# Stevens
# rows: beta columns=alpha
cnda_data = [ 0.001 -0.027 -0.017 -0.013 -0.012 -0.016  0.001  0.017  0.011  0.017  0.008  0.016;
              0.002 -0.014 -0.016 -0.016 -0.014 -0.019 -0.021  0.002  0.012  0.015  0.015  0.011;
             -0.006 -0.008 -0.006 -0.006 -0.005 -0.008 -0.005  0.007  0.004  0.007  0.006  0.006;
             -0.011 -0.011 -0.010 -0.009 -0.008 -0.006  0.000  0.004  0.007  0.010  0.004  0.010;
             -0.015 -0.015 -0.014 -0.012 -0.011 -0.008 -0.002  0.002  0.006  0.012  0.011  0.011;
             -0.024 -0.010 -0.004 -0.002 -0.001  0.003  0.014  0.006 -0.001  0.004  0.004  0.006;
             -0.022  0.002 -0.003 -0.005 -0.003 -0.001 -0.009 -0.009 -0.001  0.003 -0.002  0.001]
cnda_data /= DA_MAX
cnda_data = cnda_data'

# Morelli
# rows: beta columns=alpha
# cnda_data = [ 0.001 -0.027 -0.017 -0.013 -0.012 -0.016  0.001  0.017  0.011  0.017  0.008  0.016;
#               0.002 -0.014 -0.016 -0.016 -0.014 -0.019 -0.021  0.002  0.012  0.015  0.016  0.011; # <<
#              -0.006 -0.008 -0.006 -0.006 -0.005 -0.008 -0.005  0.007  0.004  0.007  0.006  0.006;
#              -0.011 -0.011 -0.010 -0.009 -0.008 -0.006  0.000  0.004  0.007  0.010  0.004  0.010;
#              -0.015 -0.015 -0.014 -0.012 -0.011 -0.008 -0.002  0.002  0.006  0.012  0.011  0.011;
#              -0.024 -0.010 -0.004 -0.002 -0.001  0.003  0.014  0.006 -0.001  0.004  0.004  0.006;
#              -0.022  0.002 -0.003 -0.005 -0.003 -0.001 -0.009 -0.009 -0.001  0.003 -0.002  0.001]
# cnda_data /= DA_MAX
# cnda_data = cnda_data'

# rows: beta columns=alpha
cndr_data = [-0.018 -0.052 -0.052 -0.052 -0.054 -0.049 -0.059 -0.051 -0.030 -0.037 -0.026 -0.013;
             -0.028 -0.051 -0.043 -0.046 -0.045 -0.049 -0.057 -0.052 -0.030 -0.033 -0.030 -0.008;
             -0.037 -0.041 -0.038 -0.040 -0.040 -0.038 -0.037 -0.030 -0.027 -0.024 -0.019 -0.013;
             -0.048 -0.045 -0.045 -0.045 -0.044 -0.045 -0.047 -0.048 -0.049 -0.045 -0.033 -0.016;
             -0.043 -0.044 -0.041 -0.041 -0.040 -0.038 -0.034 -0.035 -0.035 -0.029 -0.022 -0.009;
             -0.052 -0.034 -0.036 -0.036 -0.035 -0.028 -0.024 -0.023 -0.020 -0.016 -0.010 -0.014;
             -0.062 -0.034 -0.027 -0.028 -0.027 -0.027 -0.023 -0.023 -0.019 -0.009 -0.025 -0.010]
cndr_data /= DR_MAX
cndr_data = cndr_data'

"""
   dampings(aero::F16Aerodynamics, α)

Calculates CX_q, CY_r, CY_p, CZ_q, Cl_r, Cl_p, Cm_q, Cn_r, Cn_p for the given
angle of attack, α (deg).
"""
function dampings(aero::F16Aerodynamics, α)
   # k, l, da = get_interp_idx(α, 0.2, -1, 8)
   # dampings = damp_data[k, :] + abs(da) * (damp_data[l, :] - damp_data[k, :])
   # TODO: check if vectorized approach is faster than loop (it shouldn't)
   dampings = Array{Float64}(undef, 9)
   for ii in 1:9
      dampings[ii] = interp1d(α, 0.2, -1, 8, damp_data[ii, :])
   end
   return dampings
end

Cl_da(aero::F16Aerodynamics, α, β) = interp2d(α, β, 0.2, 0.1, -1, -2, 8, 2, clda_data, 3, 4)
Cl_dr(aero::F16Aerodynamics, α, β) = interp2d(α, β, 0.2, 0.1, -1, -2, 8, 2, cldr_data, 3, 4)
Cn_da(aero::F16Aerodynamics, α, β) = interp2d(α, β, 0.2, 0.1, -1, -2, 8, 2, cnda_data, 3, 4)
Cn_dr(aero::F16Aerodynamics, α, β) = interp2d(α, β, 0.2, 0.1, -1, -2, 8, 2, cndr_data, 3, 4)

CXαde(aero::F16Aerodynamics, α, de) = interp2d(α, de, .2, 1.0/12, -1, -1, 8, 1, CX_data)

CYdr(aero::F16Aerodynamics, dr) = 0.086 / DR_MAX * dr
CYda(aero::F16Aerodynamics, da) = 0.021 / DA_MAX * da
CYβ(aero::F16Aerodynamics, β) = -0.02 * β

CZα(aero::F16Aerodynamics, α) = interp1d(α, 0.2, -1, 8, CZ_data)
CZαβ(aero::F16Aerodynamics, α, β) = CZα(aero, α) * (1. - (β*DEG2RAD)^2)
CZde(aero::F16Aerodynamics, de) = -0.19/DE_MAX * de

Clαβ(aero::F16Aerodynamics, α, β) = interp2d2(α, β, .2, .2, -1, 1, 8, 5, Cl_data)
Cldr(aero::F16Aerodynamics, α, β, dr) = Cl_dr(aero, α, β) * dr
Clda(aero::F16Aerodynamics, α, β, da) = Cl_da(aero, α, β) * da

Cmαde(aero::F16Aerodynamics, α, de) = interp2d(α, de, .2, 1.0/12, -1, -1, 8, 1, Cm_data)

Cnαβ(aero::F16Aerodynamics, α, β) = interp2d2(α, β, .2, .2, -1, 1, 8, 5, Cn_data)
Cndr(aero::F16Aerodynamics, α, β, dr) = Cn_dr(aero, α, β) * dr
Cnda(aero::F16Aerodynamics, α, β, da) = Cn_da(aero, α, β) * da

function calculate_aerodynamics(
    ac::Aircraft, aero::F16Aerodynamics, aerostate::AeroState, state::State
    )

   ARP = get_arp(ac)

   qinf = get_qinf(aerostate)  # Pa
   Sw = get_wing_area(ac)  # m²
   b = get_wing_span(ac)  # m
   c = get_chord(ac)  # m

   fcs = get_fcs(ac)
   de = get_value(fcs.de) * RAD2DEG
   da = get_value(fcs.da) * RAD2DEG
   dr = get_value(fcs.dr) * RAD2DEG

   α = get_alpha(aerostate) * RAD2DEG
   β = get_beta(aerostate) * RAD2DEG
   tas = get_tas(aerostate) * M2FT
   # αdot = get_alpha_dot(aerostate)  # rad/s
   p, q, r = get_body_ang_velocity(state)  # rad/s

   c2v = c * M2FT / (2*tas)
   qc2v = q * c2v
   b2v = b * M2FT / (2*tas)

   CX_q, CY_r, CY_p, CZ_q, Cl_r, Cl_p, Cm_q, Cn_r, Cn_p = dampings(aero, α)

   CX = CXαde(aero, α, de) + CX_q * qc2v
   CY = CYβ(aero, β) + CYda(aero, da) + CYdr(aero, dr) + (CY_r*r + CY_p*p) * b2v
   CZ = CZαβ(aero, α, β) + CZde(aero, de) + CZ_q * qc2v

   Cl = Clαβ(aero, α, β) +  Clda(aero, α, β, da) + Cldr(aero, α, β, dr) +
       (Cl_r*r + Cl_p*p) * b2v
   Cm = Cmαde(aero, α, de) + Cm_q*qc2v
      # + CZ * (xcgr - xcg) is taken into account in pfm
   Cn = Cnαβ(aero, α, β) + Cnda(aero, α, β, da) + Cndr(aero, α, β, dr) +
       (Cn_r*r + Cn_p*p) * b2v
      # - CY * (xcgr - xcg) * c/b is taken into account in pfm

   # Pack results
   adim_pfm_body = PointForcesMoments(ARP, [CX, CY, CZ], [Cl, Cm, Cn])

   α = get_alpha(aerostate)
   β = get_beta(aerostate)
   # Generate aerodynamics  object
   aerodynamics_from_body_coeff(adim_pfm_body, α, β, qinf, Sw, b, c, aero)
end

##----------------------------------------------------------------------------------------------------
## Propulsion

function F16Engine()
    F16Engine(
        PointForcesMoments(zeros(3), zeros(3), zeros(3)),
        0,
        0,
        0,
        # Fuel tanks can be obtained from JSBSim model
        [PointMass(0 * LB2KG, [0., 0., 0.] .* IN2M)],
        [160.0*SLUGFT2_2_KGM2, 0.0, 0.0]
        )

end

# TODO: Engine position and orientation
get_engine_position(prop::F16Engine) = [0.35 * 11.32 * FT2M, 0.0, 0.0]
get_engine_orientation(prop::F16Engine) = [0., 0., 0.] .* DEG2RAD
get_engine_gyro_effects(prop::F16Engine) = [160.0*SLUGFT2_2_KGM2, 0.0, 0.0]  # [kg·m²/s]

# ENGINE DATA
"""
    tgear(thtl)

Given the thottle setting (0 ≤ thtl ≤ 1) calculate the commanded power level
(0 ≤ cpow ≤ 100)
"""
function tgear(thtl)
    if thtl <= .77
     cpow = 64.94 * thtl
    else
     cpow = 217.38 * thtl - 117.38
    end
    return cpow
end

"""
    pdot(p3, p1)

Rate of change of power with time using a first order lag (%/s)

# Arguments
p3::Number  Actual power (%)
p1::Number  Power command (%)
"""
function pdot(p3, p1)
    if p1 >= 50.
        if p3 >= 50.
            t = 5.
            p2 = p1
        else
            p2 = 60.
            t = rtau(p2-p3)
        end
    else
        if p3 >= 50.
            t = 5.
            p2 = 40.
        else
            p2 = p1
            t = rtau(p2-p3)
        end
    end
    pd = t * (p2 - p3)
    return pd
end

"""
    rtau(dp)

Calculate the reciprocal time constant (1/s) for a first-order thrust lag given
the difference between the commanded power level (%) and the actual power
level (%).
"""
function rtau(dp)
    if dp <= 25.
        rt = 1.0  # Reciprocal time constant
    elseif dp >= 50.
        rt = 0.1
    else
        rt = 1.9 - 0.036 * dp
    end
    return rt
end

idle_data =
    [ 1060.  670.   880.   1140.  1500.  1860.
      635.   425.   690.   1010.  1330.  1700.
      60.    25.    345.   755.   1130.  1525.
     -1020. -710.  -300.   350.   910.   1360.
     -2700. -1900. -1300. -247.   600.   1100.
     -3600. -1400. -595.  -342.  -200.   700.]'

# Mil data
mil_data =
    [12680. 9150.  6200.  3950.  2450. 1400.
     12680. 9150.  6313.  4040.  2470. 1400.
     12610. 9312.  6610.  4290.  2600. 1560.
     12640. 9839.  7090.  4660.  2840. 1660.
     12390. 10176. 7750.  5320.  3250. 1930.
     11680. 9848.  8050.  6100.  3800. 2310.]'

# Max data
max_data =
    [20000. 15000. 10800. 7000.  4000. 2500.
     21420. 15700. 11225. 7323.  4435. 2600.
     22700. 16860. 12250. 8154.  5000. 2835.
     24240. 18910. 13760. 9285.  5700. 3215.
     26070. 21075. 15975. 11115. 6860. 3950.
     28886. 23319. 18300. 13484. 8642. 5057.]'

"""
    thrust(pow, alt, rmach)

Given the commanded power level (%), the altitude (ft) and the Mach number,
calculate the engine thrust (lbf)
"""
function calculate_thrust(pow, alt, rmach)

    h = 0.0001 * alt
    i = floor(Int, h)
    if i >= 5
        i=4
    end

    dh = h - float(i)

    rm = 5.0 * rmach
    m = floor(Int, rm);
    if m >= 5
        m = 4
    end

    dm = rm - float(m)
    cdh = 1.0 - float(dh)

    i=i+1;
    m=m+1;

    s = mil_data[i, m] * cdh + mil_data[i+1, m] * dh
    t = mil_data[i, m+1] *cdh + mil_data[i+1, m+1] * dh
    tmil = s + (t - s) * dm

    if pow < 50.
        s = idle_data[i, m] * cdh + idle_data[i+1, m] * dh
        t = idle_data[i,m+1] * cdh + idle_data[i+1, m+1] * dh
        tidl = s + (t - s) * dm
        thrst = tidl + (tmil - tidl) * pow * 0.02
    else
        s = max_data[i, m] * cdh + max_data[i+1, m] * dh
        t = max_data[i, m+1] * cdh + max_data[i+1, m+1] * dh
        tmax = s + (t-s) * dm
        thrst = tmil + (tmax - tmil) * (pow-50.) * 0.02
    end

    return thrst
    end

"""
The F-16 engine power response is modelled by a first-order lag (pdot function).
The rest of the model consists of thottle gearing (tgear) and lookup tables for
thrust as a function of operating power level, altitude and Mach.

Lookup tables have rows corresponding to Mach from 0 to 1 in increments of 0.2
and columns correspond to altitudes from 0 to 50000ft in increments of 10000ft.
There is a table for each power level: idle, military and maximum. Results can
be extrapolated beyond the boundaries but results may be unrealistic.

# References

Reimplemented from:

- [1] Stevens, B. L., Lewis, F. L., & Johnson, E. N. (2015). Aircraft control
 and simulation: dynamics, controls design, and autonomous systems. John Wiley
 & Sons. (page 715)
"""
function calculate_engine(
    eng::F16Engine, fcs::FCS, aerostate::AeroState, state::State; consume_fuel=false
    )

    pow = get_thrust(fcs)  # Commanded power: between 0 and 100
    Mach = get_mach(aerostate)
    # TODO: Should be altitude
    height = get_height(state) * M2FT

    thrust = calculate_thrust(pow, height, Mach)
    thrust *= LBF2N
    cj = 0.0
    Pm = thrust * get_tas(aerostate)
    ηp = NaN

    if consume_fuel==true
        error("fuel consumption not implemented")
    else
        tanks = get_tanks(eng)
    end

    # TODO: Torque calcualtion is missing
    pfm = PointForcesMoments(get_engine_position(eng),
                             [thrust, 0, 0],
                             [0, 0, 0]
                             )

    return typeof(eng)(pfm, cj, Pm, ηp, tanks, get_engine_gyro_effects(eng))
end

##----------------------------------------------------------------------------------------------------
## FCS

# TODO: Move to FCS in models
get_thrust(fcs::F16FCS) = get_value(fcs.cpl)

F16FCS(stick_lon, stick_lat, pedals, thtl, de, da, dr, cpl) = F16FCS(
    stick_lon, stick_lat, pedals, thtl, de, da, dr, cpl, false, false
    )

F16FCS() = F16FCS(
    # Cabin Inputs
    RangeControl(0.0, [0, 1]),  # stick_longitudinal
    RangeControl(0.0, [0, 1]),  # stick_lateral
    RangeControl(0.0, [0, 1]),  # pedals
    RangeControl(0.0, [0, 1]),  # thtl
    # Controls
    RangeControl(0.0, [-DE_MAX, DE_MAX] .* DEG2RAD),  # elevator
    RangeControl(0.0, [-DA_MAX, DA_MAX] .* DEG2RAD),  # ailerons
    RangeControl(0.0, [-DR_MAX, DR_MAX] .* DEG2RAD),  # rudder
    # Commanded power level
    RangeControl(0.0, [0.0, 100.]),                # CPL
    )

function set_stick_lon!(fcs::F16FCS, value)
    set_value!(fcs.stick_longitudinal, value)
    min, max = get_value_range(fcs.de)
    range = max - min
    allow_out_of_range = get_allow_out_of_range_inputs(fcs)
    throw_error = get_throw_error_on_out_of_range_inputs(fcs)
    set_value!(fcs.de, min + range * value, allow_out_of_range, throw_error)
end

function set_stick_lat!(fcs::F16FCS, value)
    set_value!(fcs.stick_lateral, value)
    min, max = get_value_range(fcs.da)
    range = max - min
    allow_out_of_range = get_allow_out_of_range_inputs(fcs)
    throw_error = get_throw_error_on_out_of_range_inputs(fcs)
    set_value!(fcs.da, min + range * value, allow_out_of_range, throw_error)
end

function set_pedals!(fcs::F16FCS, value)
    set_value!(fcs.pedals, value)
    min, max = get_value_range(fcs.dr)
    range = max - min
    allow_out_of_range = get_allow_out_of_range_inputs(fcs)
    throw_error = get_throw_error_on_out_of_range_inputs(fcs)
    set_value!(fcs.dr, min + range * value, allow_out_of_range, throw_error)
end

function set_thtl!(fcs::F16FCS, value)
    set_value!(fcs.thtl, value)
    min, max = get_value_range(fcs.thtl)
    range = max - min
    allow_out_of_range = get_allow_out_of_range_inputs(fcs)
    throw_error = get_throw_error_on_out_of_range_inputs(fcs)
    set_value!(fcs.cpl, tgear(value), allow_out_of_range, throw_error)
end

function get_controls_ranges_trimmer(fcs::F16FCS)
    [get_value_range(fcs.stick_longitudinal),
     get_value_range(fcs.stick_lateral),
     get_value_range(fcs.pedals),
     get_value_range(fcs.thtl)]
end

##----------------------------------------------------------------------------------------------------
## Aircraft Model

function F16()
    pfm0 = PointForcesMoments(zeros(3), zeros(3), zeros(3))
    aero0 = F16Aerodynamics(pfm0, pfm0, pfm0, pfm0)
    engine = F16Engine()
    propulsion0 = Propulsion(
            PointForcesMoments(zeros(3), zeros(3), zeros(3)),
            0, 0, 0,
            [engine],
            get_gyro_effects(engine)
            )

    fcs = F16FCS()
    # mass properties cannot be retrieved until ac is created... so:
    ac = F16(RigidSolid(0, zeros(3), zeros(3, 3)),
              pfm0,
              aero0,
              propulsion0,
              fcs)
    mass = get_fuel_mass_props(get_propulsion(ac)) + get_empty_mass_props(ac)
            # + get_payload_mass_props(ac)
    F16(mass, pfm0, aero0, propulsion0, fcs)
end

# Name
function get_name(ac::F16)
    return "F16"
end

# GEOMETRIC PROPERTIES
get_wing_area(ac::F16) = 300.0 * FT2M^2
get_wing_span(ac::F16) = 30. * FT2M
get_chord(ac::F16) = 11.32 * FT2M

# Aerodynamic Reference Point
# Origin assumed leading edge of CMA
get_arp(ac::F16) = [-0.35 * get_chord(ac), 0.0, 0.0]
get_empty_cg(ac::F16) = [-0.35 * get_chord(ac), 0, 0]

# MASS PROPERTIES
function get_empty_mass_props(ac::F16)
    RigidSolid(
        20500 * LB2KG,                                # Empty mass
        get_empty_cg(ac),                             # Empty CG
        [9456.        0.    -982.;                     # Empty inertia
            0.    55814.       0.;
          -982.       0.   63100.]  .* SLUGFT2_2_KGM2
    )
end

# Crew and cargo
# get_pilot_mass_props(ac::F16) = PointMass(0 * LB2KG, [0., 0., 0.] .* IN2M)
# get_cargo_mass_props(ac::F16) = PointMass(0 * LB2KG, [0., 0., 0.] .* IN2M)

# get_payload_mass_props(ac::F16) = get_pilot_mass_props(ac)

##----------------------------------------------------------------------------------------------------


#!/usr/bin/env julia

#######
# CLI #
#######
import Celeste.ArgumentParse
parser = Celeste.ArgumentParse.ArgumentParser()
ArgumentParse.add_argument(
    parser,
    "--ais-output",
    help="Directory containing source-specific AIS sample files",
)
ArgumentParse.add_argument(
    parser,
    "--output-dir",
    help="Directory to output results dataframes",
)
ArgumentParse.add_argument(
    parser,
    "--truth-csv",
    help="True file (coadd, or prior generated file)",
)
ArgumentParse.add_argument(
    parser,
    "--vb-csv",
    help="VB inference file for comparison",
)
ArgumentParse.add_argument(
    parser,
    "--photo-csv",
    help="Photo inference file for comparison"
)
parsed_args = ArgumentParse.parse_args(parser, ARGS)
println(parsed_args)
println("======= scoring MCMC results ============")

#######################
# Analysis Output Dir #
#######################
import JLD, CSV
using DataFrames, StatsBase
import Celeste: AccuracyBenchmark, Model, MCMC

if haskey(parsed_args, "output-dir")
  output_dir = parsed_args["output-dir"]
else
  output_dir = "./mcmc_result_dataframes"
end
println(" ... saving output to ", output_dir)
if !isdir(output_dir)
  mkdir(output_dir)
end

#######################################################################
# collect MCMC files and summarize them into a results dataframe      #
#######################################################################
if haskey(parsed_args, "ais-output")
    ais_output_dir = parsed_args["ais-output"]
else
    ais_output_dir = "ais-output/synthetic-run-10-31"
end
ais_out = filter!(r"\.jld$", readdir(ais_output_dir))
results = [JLD.load(joinpath(ais_output_dir, f))["res"] for f in ais_out];
results = [res for res in results if !res["is_sky_bad"]]
@printf " ... found %d results files in directory %s \n" length(results) ais_output_dir

#####################################################
# Compute prob star/gal (based on model evidence    #
#####################################################
function compute_pstar(res; prior_is_star=.28) #[.28, .72])
    # is_star = [.28, .72] vs [.999, .001] vs [.5, .5]
    type_chain  = zeros(length(res["gal_bootstrap"]))
    lnprob_a    = log(prior_is_star)
    lnprob_nota = log(1.-prior_is_star)
    for n in 1:length(type_chain)
        # normalizing constant is ln p(data | star)
        # so p(star | data) \propto p(data | star) p(star)
        lnprob_star = res["star_bootstrap"][n] + lnprob_a
        lnprob_gal  = res["gal_bootstrap"][n] + lnprob_nota
        lnsum = Model.logsumexp([lnprob_star, lnprob_gal]) # normalize
        type_chain[n] = lnprob_star - lnsum
    end
    ave_pstar = Model.logsumexp(type_chain) - log(length(type_chain))
    return ave_pstar
end

for res in results
  res["ave_pstar"] = compute_pstar(res; prior_is_star=.28)
end

# must correct for off-by-pixel value
star_summary, gal_summary, results_df = MCMC.consolidate_samples(results;
    summarize=median)
#results_df[:dec] -= .396 / 3600.
#results_df[:ra]  -= .397 / 3600.

######################################################################
# Compute the per chain R Hats (TODO put these in the alg itself)    #
######################################################################
function compute_ss_rhat(sdf; num_chains=25, num_samps_per_chain=25)
    chains = []
    si = 1
    for i in 1:num_chains
        endi = si + num_samps_per_chain - 1
        #@printf "chunked %d : %d \n" si endi
        push!(chains, Array(sdf[si:endi,10:10]))
        si += num_samps_per_chain
    end
    return MCMC.potential_scale_reduction_factor(chains)
end

star_psrf = mean(vcat([compute_ss_rhat(res["star_samples"])
                       for res in results]...), 2)
gal_psrf  = mean(vcat([compute_ss_rhat(res["gal_samples"])
                       for res in results]...), 2)

######################################################################
# Load comparison data frames                                        #
######################################################################
if haskey(parsed_args, "truth-csv")
  truth_file = parsed_args["truth-csv"]
else
  truth_file = expanduser("~/Proj/Celeste.jl/benchmark/accuracy/output/prior_6328dda483.csv")
end
println(" ... loaded truth dataframe from ", truth_file)
truth = AccuracyBenchmark.read_catalog(truth_file)

if haskey(parsed_args, "vb-csv")
  vb_file = parsed_args["vb-csv"]
else
  vb_file = expanduser("~/Proj/Celeste.jl/benchmark/accuracy/output/prior_6328dda483_synthetic_6c24c1c9ad_predictions_342302e66a.csv")
end
println(" ... loaded VB comparison DF from ", vb_file)
vbdf = AccuracyBenchmark.read_catalog(vb_file)

if haskey(parsed_args, "photo-csv")
  photo_file = parsed_args["photo-csv"]
else
  println(" ... no photo file passed in --- loading VB as dummy ")
  photo_file = vb_file
end
photodf = AccuracyBenchmark.read_catalog(photo_file)

############################################################################
# Match prediction dataframes first --- throw away the same sources as VB  #
############################################################################
matched_truth, matched_prediction_dfs = AccuracyBenchmark.match_catalogs(
    truth, [vbdf, results_df, photodf]) #, tol=.396/3600*4)
@printf " ... found %d matched sources for comparison \n" size(matched_truth, 1)
truth = matched_truth
vbdf = matched_prediction_dfs[1]
results_df = matched_prediction_dfs[2]
photodf = matched_prediction_dfs[3]
@printf "Found %d nan r fluxes in the VB dataframe\n" sum(isnan.(vbdf[:flux_r_nmgy]))

#bad_idx = isnan.(vbdf[:flux_r_nmgy])
#truth = truth[.~bad_idx,:]
#vbdf = vbdf[.~bad_idx,:]
#results_df = results_df[.~bad_idx,:]
#photodf = photodf[.~bad_idx,:]

############################################
# Compare MCMC results samples to truth    #
############################################
function get_percentile_df(truth, sample_list)

    STDERR_COLUMNS = Set([
        :log_flux_r_stderr,
        :color_ug_stderr,
        :color_gr_stderr,
        :color_ri_stderr,
        :color_iz_stderr,
    ])

    # store these values --- easier that way
    truth[:log_flux_r] = log.(truth[:flux_r_nmgy])

    # go through each truth source, match it to a sample in the results list
    _, _, sample_summary = MCMC.consolidate_samples(sample_list; summarize=mean)
    #sample_summary[:dec] -= .396 / 3600.
    #sample_summary[:ra]  -= .396 / 3600.
    samp_pos = convert(Array, sample_summary[[:ra, :dec]])

    function get_percentile_column(column_name)
        percentiles, names = []  , []
        for i in 1:size(truth, 1)

            # match this truth position to the appropriate set of samples
            pos_i = convert(Array, truth[[:ra, :dec]][i, :])
            dists = sum(broadcast(-, samp_pos, pos_i).^2, 2)
            near_dist, near_idx = findmin(dists)

            # grab samples of appropriate source type
            sdict = results[near_idx]
            pstar = exp(sdict["ave_pstar"])
            samps = (pstar > .5) ? sdict["star_samples"] : sdict["gal_samples"]

            # truth percentile
            true_val = truth[column_name][i]
            true_percentile = sum(true_val .> sort(samps[column_name])) / float(size(samps, 1))

            push!(percentiles, true_percentile)
            push!(names, column_name)
        end
        return percentiles, names
    end

    # assemble dataframe of percentile values
    column_names = [:log_flux_r, :color_ug, :color_gr, :color_ri, :color_iz]
    res = [get_percentile_column(c) for c in column_names]
    pers = vcat([r[1] for r in res]...)
    names = vcat([r[2] for r in res]...)
    return DataFrame([names, pers], [:name, :percentile])
end

###############################################
# Compute scores for comparison data frames   #
###############################################
scores = AccuracyBenchmark.score_predictions(truth, [vbdf, results_df])
println("Comparing vb (first) to MCMC (second)")
println(repr(scores))

# uncertainties
vb_udf = AccuracyBenchmark.get_uncertainty_df(truth, vbdf);
vb_uscore = AccuracyBenchmark.score_uncertainty(vb_udf);

mc_udf = AccuracyBenchmark.get_uncertainty_df(truth, results_df);
mc_uscore = AccuracyBenchmark.score_uncertainty(mc_udf);
mc_perdf = get_percentile_df(truth, results)

println("VB Uncertainty")
println(vb_uscore)
println("MC Uncertainty")
println(mc_uscore)

# output to output dir
CSV.write(joinpath(output_dir, "uscore_vb.csv"), vb_uscore)
CSV.write(joinpath(output_dir, "uscore_mc.csv"), mc_uscore)
CSV.write(joinpath(output_dir, "udf_vb.csv"), vb_udf)
CSV.write(joinpath(output_dir, "udf_mc.csv"), mc_udf)
CSV.write(joinpath(output_dir, "perdf_mc.csv"), mc_perdf)

########################################################################
# Compute bootstrap resampled uncertainties for prediction estimates   #
########################################################################
function score_predictions_bootstrap(truth::DataFrame,
                                     prediction_dfs::Vector{DataFrame};
                                     num_bootstrap=1000,
                                     lo=2.5,
                                     hi=97.5)
    matched_truth, matched_prediction_dfs = AccuracyBenchmark.match_catalogs(truth, prediction_dfs)

    # in each sample, resample matched truth and matched pred dataframe rows
    scoredfs = []
    for bsamp in 1:num_bootstrap
        nsamps = size(matched_truth,1)
        bidx = [rand(1:nsamps) for n in 1:nsamps]
        mtruth = matched_truth[bidx,:]
        mpreds = [m[bidx,:] for m in matched_prediction_dfs]

        error_dfs = [AccuracyBenchmark.get_error_df(mtruth, predictions) for predictions in mpreds]
        @assert length(prediction_dfs) <= 2
        sdf = AccuracyBenchmark.get_scores_df(
            matched_truth,
            error_dfs[1],
            length(error_dfs) > 1 ? Nullable(error_dfs[2]) : Nullable{DataFrame}(),
        )
        sdf[:sample_i] = ones(Int64, size(sdf,1))*bsamp
        push!(scoredfs, sdf)
    end

    # assemble hi and lo values in the bootstrap samples
    sdf = scoredfs[1]
    boot_dict = Dict(:first_lo => [],
                     :first_hi => [])
                     #:field => [])
    if :second in names(sdf)
        boot_dict[:second_lo] = []
        boot_dict[:second_hi] = []
    end

    nrows = size(sdf, 1)
    for r in 1:nrows
        first_vals  = [sdf[r,:first] for sdf in scoredfs]
        push!(boot_dict[:first_lo], percentile(first_vals, lo))
        push!(boot_dict[:first_hi], percentile(first_vals, hi))
        if :second in names(sdf)
            second_vals = [sdf[r,:second] for sdf in scoredfs]
            push!(boot_dict[:second_lo], percentile(second_vals, lo))
            push!(boot_dict[:second_hi], percentile(second_vals, hi))
        end
        #push!(boot_dict[:field], sdf[r,:field])
    end

    scoremean = AccuracyBenchmark.score_predictions(truth, prediction_dfs)
    score_cis = DataFrame(boot_dict)
    return hcat(scoremean, score_cis), vcat(scoredfs...)
end

lo, hi = 2.5, 97.5
scoredf, scoredf_boot = score_predictions_bootstrap(truth, [vbdf, results_df]);
println("Bootstrap scoredf:")
println(scoredf)
CSV.write(joinpath(output_dir, "scoredf.csv"), scoredf)
CSV.write(joinpath(output_dir, "scoredf_boot.csv"), scoredf_boot)

##############################
# look at star ROC curve     #
##############################
#matched_truth, matched_prediction_dfs = AccuracyBenchmark.match_catalogs(
#    truth, [vbdf, results_df, photodf]) #, tol=.396/3600*4)
#@printf " ... found %d matched sources for comparison \n" size(matched_truth, 1)

# look at ROC curve for pstar
true_star = matched_truth[:is_star]
pstar_vb  = matched_prediction_dfs[1][:is_star]
pstar_mc  = matched_prediction_dfs[2][:is_star]
pstar_df = DataFrame(Dict(:true_star => true_star,
                          :pstar_vb => pstar_vb,
                          :pstar_mc => pstar_mc))
CSV.write(joinpath(output_dir, "pstardf.csv"), pstar_df)
CSV.write(joinpath(output_dir, "matched_truth.csv"), matched_truth)
CSV.write(joinpath(output_dir, "matched_vb.csv"), matched_prediction_dfs[1])
CSV.write(joinpath(output_dir, "matched_mc.csv"), matched_prediction_dfs[2])
CSV.write(joinpath(output_dir, "matched_photo.csv"), matched_prediction_dfs[3])


module BooleanNetworks
export zerocfg, load_bnet, fasync_simulations, outputs_ratios, resolve

struct BooleanNetwork
    nodes::Vector{String}
    index::Dict{String,Int64}
    f::Vector{Function}
    out_influences::Vector{Vector{Int64}} # out-going influences
end

resolve(bn, indexes) = [bn.nodes[i] for i in indexes]

zerocfg(bn) = zeros(Bool, length(bn.f))

BNET_SYMBOL_RE = r"[\w\.:]+"

function load_bnet(filename, sep=",", impl_binary=true)
    data = [split(line, sep) for line in eachline(filename)]
    n = length(data)
    nodes = [strip(d[1]) for d in data]
    index = Dict(((n,i) for (i,n) in enumerate(nodes)))

    impl_expr_bin(expr) = "x -> " * replace(replace(expr,
            "!" => "~"),
             BNET_SYMBOL_RE => x -> "x[$(index[x])]")
    impl_expr_bool(expr) = "x -> " * replace(replace(replace(expr,
            "|" => "||"),
            "&" => "&&"),
             BNET_SYMBOL_RE => x -> "x[$(index[x])]")
    impl_expr = impl_binary ? impl_expr_bin : impl_expr_bool
    make_expr = eval ∘ Meta.parse ∘ impl_expr
    f = [make_expr(d[2]) for d in data]

    make_influences(expr) = [index[m.match] for m in eachmatch(BNET_SYMBOL_RE, expr)]
    influences = [make_influences(d[2]) for d in data]

    BooleanNetwork(nodes, index, f, influences)
end

struct FAsyncSimulationContext
    x::Vector{Bool}
    c::Vector{Int64}
end
new_fasync_simulation(n) = FAsyncSimulationContext(Vector{Bool}(undef, n), Vector{Int64}(undef, n))

function _fasync_step!(x, c, f, n)
    nc = 0
    for i in 1:n
        if f[i](x) ⊻ x[i]
            nc += 1
            c[nc] = i
        end
    end
    if nc == 0
        0
    else
        if nc == 1
            i = c[1]
        else
            i = c[rand(1:nc)]
        end
        x[i] = !x[i]
        i
    end
end

_fasync_unroll!(ctx, f, n, maxsteps) =
    for _ in 1:maxsteps
        if _fasync_step!(ctx.x, ctx.c, f, n) == 0
            break
        end
    end

function _fasync_simulation!(ctx, f, n, x, maxsteps, outputs)
    ctx.x[1:n] = x
    _fasync_unroll!(ctx, f, n, maxsteps)
    @view ctx.x[outputs]
end

function pack_output(binarray)
    x::Int64 = 0
    for i in 1:length(binarray)
        x |= binarray[i] << (i-1)
    end
    x
end
unpack_output(x, letters) = [a for (i,a) in enumerate(letters) if x & (1 << (i-1)) > 0]

function fasync_simulations(bn, outputs, nb_sims, maxsteps, x)
    f = bn.f
    n = length(f)
    res = Array{Int64}(undef, nb_sims)
    ctx = [new_fasync_simulation(n) for _ in 1:Threads.nthreads()]
    Threads.@threads :static for i in 1:nb_sims
        res[i] = pack_output(_fasync_simulation!(ctx[Threads.threadid()], f, n, x, maxsteps, outputs))
    end
    res
end

function outputs_ratios(result, outputs, bn)
    cv = zeros(Int64, 2^length(outputs))
    for k in result
        cv[k] += 1
    end
    make_key(k) = resolve(bn, unpack_output(k, outputs))
    Dict(make_key(k) => c/length(result) for (k,c) in enumerate(cv) if c > 0)
end

end


type wfinfo
    mediumtype::String
    component :: Array{String} # provide information of output data component
    mediumsize :: Array{Int64}
    ext :: Int64
    gridsize :: Array{Float64}
    timesamp :: Int64 # Total time sampling nT
    timepoint :: Array{Float64}
end

function readwfinfo(DataPath::String)
    fid = open(DataPath,"r")
    iflag = read(fid,Int64,1)
    if iflag[1] == 1
        mediumtype = "free surface"
    elseif iflag[1] == 2
        mediumtype = "unlimited"
    else error("Check wavefield file")
    end

    CpntDict = Dict(1=>"P",2=>"Vx",3=>"Vz",
                  4=>"Txx",5=>"Tzz",6=>"Txz")
    nCpnt = read(fid,Int64,1)
    NumCpnt = read(fid,Int64,nCpnt[1])
    component = []
    for i in NumCpnt
      if i == 23
          push!(component,CpntDict[2])
          push!(component,CpntDict[3])
      elseif i == 45
          push!(component,CpntDict[4])
          push!(component,CpntDict[5])
      else push!(component,CpntDict[i])
      end
    end
    mediumsize = read(fid,Int64,2)
    ext = read(fid,Int64,1)
    gridsize = read(fid,Float64,2)
    timesamp = read(fid,Int64,1)
    timepoint = read(fid,Float64,timesamp[1])

    return wfinfo(mediumtype, component, mediumsize,
    ext[1], gridsize, timesamp[1], timepoint)

end
function readwfdata(fid::IOStream,compnumber::Int64,timesamp::Int64,
    startpoint::Int64,length::Int64)
    seek(fid,8*(8+compnumber+timesamp+startpoint))
    requireddata = read(fid,Float64,length)
    return requireddata
end

function readwfdata(fid::IOStream,startpoint::Int64,length::Int64)
    seek(fid,8)
    compnumber = read(fid,Int64,1)
    seek(fid,8*(7+compnumber[1]))
    timesamp = read(fid,Int64,1)
    seek(fid,8*(8+compnumber[1]+timesamp[1]+startpoint))
    requireddata = read(fid,Float64,length)
    return(requireddata)
end

function readwfdata(fid::IOStream,length::Int64)
    requireddata = read(fid,Float64,length)
    return requireddata
end

function readwfdata(DataPath::String)
    fid = open(DataPath,"r")
    iflag = read(fid,Int64,1)
    nCpnt = read(fid,Int64,1)
    NumCpnt = read(fid,Int64,nCpnt[1])
    mediumsize = read(fid,Int64,2)
    ext = read(fid,Int64,1)
    gridsize = read(fid,Float64,2)
    timesamp = read(fid,Int64,1)
    timepoint = read(fid,Float64,timesamp[1])
    CpntDict = Dict(1=>mediumsize[1]*mediumsize[2],
                    2=>mediumsize[1]*(mediumsize[2]-1),
                    3=>(mediumsize[1]-1)*mediumsize[2],
                    4=>mediumsize[1]*mediumsize[2],
                    5=>mediumsize[1]*mediumsize[2],
                    6=>(mediumsize[1]-1)*(mediumsize[2]-1))
    totalCpnt = 0
    for Cpnt in NumCpnt
        if Cpnt == 23
            totalCpnt = totalCpnt + CpntDict[2] + CpntDict[3]
        elseif Cpnt == 45
            totalCpnt = totalCpnt + CpntDict[4] + CpntDict[5]
        else totalCpnt = totalCpnt + CpntDict[Cpnt]
        end
    end
    requireddata = read(fid,Float64,totalCpnt*timesamp[1])
    requireddata = reshape(requireddata, totalCpnt, timesamp[1])
    return requireddata
end