#Include KG_Module
include("../src/KG_Module.jl")

filepath1 = "./data/binary_run_1/binary_bh_rotating_radius_1.5_alpha_10.0_Rorbit_3.0.h5"
filepath2 = "./data/binary_run_1/binary_bh_rotating_radius_1.5_alpha_10.0_Rorbit_5.0.h5"

#Get field at certain positions
time_vector_1 , ene_vector_1 = get_energy( filepath1 , 10 , 15000.0)
time_vector_2 , ene_vector_2 = get_energy( filepath2 , 10 , 15000.0)

#Write to file
open("energy_bhb_rorbit_3.txt", "w") do io
    writedlm(io , [time_vector_1 ene_vector_1])
end

open("energy_bhb_rorbit_5.txt", "w") do io
    writedlm(io , [time_vector_2 ene_vector_2])
end


# This is the module file. It allows communication between different files within the src folder
# Also, determines which functions are provided by AutomotiveInteraction whenever someone says
# using AutomotiveInteraction
module AutomotiveInteraction

using AutomotiveSimulator
using AutomotiveVisualization
using Reel

using DataFrames
using DelimitedFiles
using Parameters
using Random # For AbstractRNG in Base.rand for c-idm

using Distributions # For pdf(Normal) in weight_and_resample
using StatsBase # For weight_and_resample
using LinearAlgebra # Form norm calculation for iterwise particle dist

using Combinatorics # make param pairs for filtering progress pairwise plots
using PGFPlots

export
    MergingEnvironment,
    MergingEnvironmentLower,
    FilteringEnvironment
include("Driving/environment.jl")

export
    keep_vehicle_subset!,
    make_def_models,
    make_IDM_models,
    make_cidm_models,
    make_lmidm_models,
    make_TimLaneChanger_models,
    get_hallucination_scenes,
    run_vehicles,
    compare2truth,
    run_vehicles_curvept_overlay,
    test_barrier_vehicle,
    test_jumpy_vehicle
include("Driving/driving_simulation.jl")

export
    centerlines_txt2tracks,
    make_roadway_interaction,
    make_roadway_ngsim,
    make_roadway_interaction_with_extensions,
    MergingRoadway,
    add_renderable!
include("Data_Processing/roadway_building.jl")

export
    INTERACTIONTrajdata,
    carsinframe,
    car_df_index,
    read_veh_tracks,
    extract_timespan,
    sample_simultaneous_vehs
include("Data_Processing/veh_track_reading.jl")

export
    video_trajdata_replay,
    scenelist2video,
    video_overlay_scenelists,
    scenelist2video_curvepts
include("Driving/visualization.jl")

export
    CooperativeIDM,
    find_merge_vehicle
include("Driving/cooperative_IDM.jl")

export
    MergeOverlay
include("Driving/overlays.jl")

export
    cidm_from_particle,
    hallucinate_a_step,
    weight_and_resample,
    multistep_update,
    obtain_driver_models
include("Filtering/particle_filtering.jl")

export
    initial_pmat,
    plot_pairwise_particles,
    gen_imitation_traj,
    imitation_with_replay,
    compute_rmse,
    rmse_dict2mean,
    test_collision,
    frac_colliding_timesteps,
    pgfplot_vector,
    truncate_vecs,
    pad_zeros,
    reality_metrics,
    replay_scenelist
include("Filtering/utils.jl")

end # End module


@testset "BFloat16sr seeding" begin
    StochasticRounding.seed(12312311)
    O = ones(BFloat16sr,10000)
    one_third = O/3

    # now reseed
    StochasticRounding.seed(12312311)
    @test O/3 == one_third
end

@testset "Float16sr seeding" begin
    StochasticRounding.seed(12312311)
    O = ones(Float16sr,10000)
    one_third = O/3

    # now reseed
    StochasticRounding.seed(12312311)
    @test O/3 == one_third
end

@testset "Float32sr seeding" begin
    StochasticRounding.seed(12312311)
    O = ones(Float32sr,10000)
    one_third = O/3

    # now reseed
    StochasticRounding.seed(12312311)
    @test O/3 == one_third
end


@doc """ Give an Array of pics file paths, return data
""" ->
function read_pics(fls::Array{UTF8String,1})
    imgs =  pmap(load, fls)
    dt = @parallel (vcat) for img in imgs
        data(reinterpret(UInt8, img))[:]'
    end

    convert(Array{Float32,2},dt)
end

@doc """ get train data
""" ->
function train_data()
    pics = readdir(trainresize_dir)
    pics_path = map(x->joinpath(trainresize_dir,x), pics)

    pics = map(y->convert(ASCIIString,y), map(x->x[1:end-4], pics))

    sample_label_dict = JLD.load(sample_label_fl,"sample_label_dict")
    labels = map(x->sample_label_dict[x], pics)
    read_pics(pics_path),labels
end

@doc """ get test data
""" ->
function test_data()
    pics = readdir(testresize_dir)
    pics_path = map(x->joinpath(testresize_dir,x), pics)

    pics,read_pics(pics_path)
end

@doc """ return sample_names=>labels dict
""" ->
function readlabel()
    dt = readcsv(train_lb,ASCIIString)
    dt =  dt[2:end,:]
    classes = sort(unique(dt[:,2]))
    sample_names = dt[:,1]
    num_sample = length(sample_names)
    labels = Array{Int64,1}(num_sample)
    for i=1:num_sample
        lbs = searchsorted(classes,dt[i,2])
        @assert length(lbs) == 1
        labels[i] = lbs[1]-1
    end
    sample_label_dict = Dict(zip(sample_names,labels))
    JLD.save(sample_label_fl,"sample_label_dict",sample_label_dict)

    label_sample_dict = Dict(map(reverse, collect(sample_label_dict)))
    JLD.save(label_sample_fl, "label_sample_dict", label_sample_dict)

    nothing
end


#=
THis file contains all my mesh-manadgement methods.
=#

# mesh related functions
include("utils.jl")

# MESH MANAGEMENT
mutable struct mesh
    vtx::Array{Float64, 2} # vertices
    elt::Array{Array{Int64, 1}, 1} # elt → vtx
    edg::Array{Array{Int64, 1}, 1} # elt → edg
    freeedg::Array{Bool, 1} # elt → free edge
    freenod::Array{Bool, 1} # elt → free node
    bnd::Array{Int64, 1} # list of boundary nodes
    nedg::Int64 # number of edges
end

function searchCommonElt(vtx2elt::Array{Array{Int64,1},1}, n1::Int64,
    n2::Int64, egs::Int64)
    #= Search for the common element. This function may be shortened by making
        better use of the language… =#
    nv2e1 = length(vtx2elt[n1])
    nv2e2 = length(vtx2elt[n2])
    egs0 = 0
    egs1 = 0
    egs2 = 0
    ind0 = 0
    j    = 1
    while ind0 == 0 && j <= nv2e1
        egs1 = vtx2elt[n1][j] # j-th elt of node n1
        if egs1 != egs
            ind = 0; k = 1
            while ind == 0 && k <= nv2e2
                egs2 = vtx2elt[n2][k]  # the candidate
                if egs1 == egs2
                    ind = 1
                else
                    k += 1
                end
            end
            if ind == 1
                ind0 = 1
            else
                j += 1
            end
        else
            j += 1
        end
    end
    #
    if ind0 == 1
        if egs1 == egs2 && egs1 != egs
            egs0 = egs1
        else
            egs0 = 0
        end
    else
        egs0 = 0
    end
    #
    return egs0
end

function getNeighbours(vtx::Array{Float64, 2}, elt::Array{Array{Int64,1},1})
    #= This function returns the elements on each side of an edge. The value 0
        means that there is only one element ≡ the edge is free =#
    nvtx = size(vtx, 2)
    nelt = length(elt)
    # 1st: for each node, determine all neighbour elements
    vtx2elt = [Int64[] for i=1:nvtx]
    for ie=1:nelt # loop over all elements
        for ix in elt[ie] # loop over all nodes of each element
            push!(vtx2elt[ix], ie) # add the element to each of these nodes
        end
    end
    # 2nd: set the correspondance between the edges and the elements
    e2neighs = [Int64[] for i=1:nelt]
    for ie=1:nelt
        neltie = length(elt[ie])
        for k=1:neltie
            kp  = mod_wrap(k+1, neltie)
            nk  = elt[ie][k]
            nkp = elt[ie][kp]
            egs0 = searchCommonElt(vtx2elt, nk, nkp, ie)
            push!(e2neighs[ie], egs0)
        end
    end
    #
    return e2neighs, vtx2elt
end

function getNbEdges(e2neighs::Array{Array{Int64,1},1})
    #= Compute the total number of edges =#
    nelt = length(e2neighs)
    flag = zeros(Bool, nelt)
    nedg = Int64(0)
    for e=1:nelt
        neloc = length(e2neighs[e])
        for eloc=1:neloc
            tmp = e2neighs[e][eloc]
            if tmp != 0
                if flag[tmp] != true
                    nedg += 1
                end
            else
                nedg += 1
            end
        end
        flag[e] = true
    end
    #
    return nedg
end

function getMeshEdges(vtx::Array{Float64, 2}, elt::Array{Array{Int64, 1}, 1},
    nedg::Int64, e2neighs::Array{Array{Int64,1},1})
    #= This function assigns numbers to the edges of the mesh and return the
        correspondance between elements and edges with a sign =#
    nnod = size(vtx, 2)
    nelt = length(elt)
    #
    freeedg = zeros(Bool, nedg)
    freenod = zeros(Bool, nnod)
    nedg = 0 # reinitialize
    e2edg = [Int64[] for i=1:nelt]
    for e=1:nelt
        nodloc   = elt[e]
        e2edg[e] = zeros(Int64,length(nodloc))
    end
    #
    for e=1:nelt
        nodloc   = elt[e]
        xt       = vtx[:, nodloc]
        for eloc = 1:length(nodloc)
            if e2edg[e][eloc] != 0
                continue
            end
            if e2neighs[e][eloc] != 0
                elocp = mod_wrap(eloc+1, length(nodloc))
                xmt   = (xt[:,eloc] + xt[:, elocp])/2
                nodlocneigh = elt[e2neighs[e][eloc]]
                xl    = vtx[:, nodlocneigh]
                for u=1:length(nodlocneigh)
                    up  = mod_wrap(u+1, length(nodlocneigh))
                    xml = (xl[:,u] + xl[:, up])/2
                    if norm(xmt - xml) < 1e-8
                        nedg += 1
                        e2edg[e][eloc] = nedg
                        e2edg[e2neighs[e][eloc]][u] = nedg
                        break
                    end
                end
            else
                nedg += 1
                e2edg[e][eloc] = nedg
                freeedg[nedg]  = true
            end
        end
    end
    # free nodes
    for e=1:nelt
        neloc = length(elt[e])
        for eloc=1:neloc
            if freeedg[e2edg[e][eloc]]
                freenod[elt[e][eloc]] = true
                freenod[elt[e][mod_wrap(eloc+1, neloc)]] = true
            end
        end
    end
    return e2edg, freeedg, freenod
end

function readMesh(fname::String)
    println("Reading mesh file: $fname")
    vtx = zeros(Float64, 1, 1)
    elt = Array{Int64, 1}[];
    bnd = zeros(Int64, 1)
    open(fname, "r") do fid
        ln = readline(fid)
        println(" -> Reading vertices ...")
        # vertices
        nvtx = parse(Int64, ln)
        vtx = zeros(Float64, 2, nvtx)
        for i=1:nvtx
            ln = readline(fid);
            vtx[:, i] .= parse.(Float64, split(ln, " ", keepempty=false))
        end
        #elements
        println(" -> Reading elements ...")
        ln = readline(fid)
        nelt = parse(Int64, ln)
        for i=1:nelt
            ln = readline(fid)
            ln_s = split(ln, " ", keepempty=false)
            push!(elt, parse.(Int64, ln_s))
        end
        # boundary
        println(" -> Reading boundary nodes ...")
        ln = readline(fid)
        nbnd = parse(Int64, ln)
        bnd = zeros(Int64, nbnd)
        for i=1:nbnd
            ln = readline(fid)
            bnd[i] = parse(Int64, ln)
        end
    end
    #
    e2neigh, _ = getNeighbours(vtx, elt)
    nedg    = getNbEdges(e2neigh)
    e2edg, freeedg, freenod = getMeshEdges(vtx, elt, nedg, e2neigh)
    return mesh(vtx, elt, e2edg, freeedg, freenod, bnd, nedg)
end

# compute connectivity table
function connectivityTableOrde0(msh::mesh)
    #= This function returns the connectivity table corresponding to a problem
        of order k = 0. =#
end


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

# using histogram from StatsBase

function Base.:+(h1::Histogram, h2::Histogram)::Histogram
	if h2 == nullHist return deepcopy(h1) end
	if h1 == nullHist return deepcopy(h2) end

	if (h1.closed == h2.closed) && (h1.isdensity == h2.isdensity) && (length(h1.edges) == 1 && length(h2.edges) == 1)
		e1, e2 = h1.edges[1], h2.edges[1] # shorthand
		if length(e1) < length(e2)
			h1, h2, e1, e2 = h2, h1, e2, e1 # swap h1 and h2, so that h1 is the 'longer' histogram
		end
		t1, t2 = typeof(e1), typeof(e2)
		# allow histograms to be added if they have the same bin size, with bins starting at the same point, but ending at different points
		if (t1 == t2) && (e1 != e2) && # note: if e1 == e2 here, then could just use merge(h1,h2)
			((t1 <: UnitRange && e1.start == e2.start) ||
			(t1 <: StepRange && e1.start == e2.start && e1.step == e2.step) ||
			(t1 <: StepRangeLen && e1.ref == e2.ref && e1.step == e2.step))

			h = deepcopy(h1)
			h.weights[1:length(h2.weights)] += h2.weights
			return h
		end
	end

	return merge(h1, h2) # requires that histograms have same field values, except for bin weights which just need to have equal length
end

function Base.:-(h::Histogram)::Histogram
	h2 = deepcopy(h)
	h2.weights = -h2.weights
	return h2
end

function Base.:-(h1::Histogram, h2::Histogram)::Histogram
	return h1 + (-h2)
end


# """
#     An Uniform playing strategy for Bandits: Can be used as benchmark
# """

struct  UniformStrategy <: BanditAlgorithmBase
    noOfArms::Int64
    noOfSteps::Int64
    lastPlayedArm::Int64

    function UniformStrategy( noOfArms::Int64 )
        new( noOfArms,
             0,
             0
        )
    end
end

function get_arm_index( agent::UniformStrategy )
    agent.lastPlayedArm = rand( 1:agent.noOfArms )
    return agent.lastPlayedArm
end

function update_reward!( agent::UniformStrategy, r::Real )
    agent.noOfSteps += 1
    # Do nothing

    nothing
end

function reset!( agent::UniformStrategy )
    agent.lastPlayedArm = 0
    agent.noOfSteps = 0
    # Do nothing

    nothing
end

function info_str( agent::UniformStrategy, latex::Bool )
    return @sprintf( "Uniform Strategy" )
end


using POMDPs

#POMDPs.add("FIB")


function get_cm(C::ClimGrid, cm, surface, center_cs, ncolors)

    if isempty(cm)
      if C[10] == "pr" || C[10]=="huss" || C[10]=="clwvi"
          cm = cmocean.cm.deep

      elseif C[10]=="tasmax" || C[10]=="tasmin" || C[10]=="tas" || C[10]=="tmax" || C[10]=="tmin" || C[10] == "wbgtmean" || C[10] == "wbgtmax" || C[10]=="t2m" || C[10]=="tmean" || C[10]=="dc" || C[10]=="hfls"
          cm = "RdYlBu_r"

      elseif C[10]=="psl" || C[10]=="vp"
          cm = cmocean.cm.deep_r

      elseif C[10]=="ua" || C[10]=="uv"
          cm = cmocean.cm.balance

      elseif C[10]=="clt" # cloud fraction
          cm = "Blues_r"

      elseif C[10]=="rlut" # longwave radiation
          cm = cmocean.cm.balance

      elseif C[10]=="hurs" || C[10]=="evspsbl"# longwave radiation
          cm = cmocean.cm.haline_r
      else
        cm = "viridis"
      end
    end

    # overide colorscale if the user specify to center the colorscale
    if center_cs
        cm = "RdBu_r"
    end

    if surface == :contourf || surface == :contour
        N = 256
    elseif surface == :pcolormesh
        N = ncolors
    end
    cmap = mpl.cm.get_cmap(cm)
    colorlist = cmap(range(0, stop=1, length=N))
    # #
    cm = mpl.colors.LinearSegmentedColormap.from_list("cm_custom", colorlist, N)

    return cm

end

"""
    getcslimits(caxis, data, C)

Returns minimum and maximum values of the colorscale axis. Used internally by [`mapclimgrid`](@ref).
"""
function getcslimits(caxis, data, center_cs)

    if !isempty(caxis)
        vmin = caxis[1]
        vmax = caxis[2]
    else
        vmin=minimum(data[.!isnan.(data)])
        vmax=maximum(data[.!isnan.(data)])
    end

    if center_cs # Hack, we want to center the divergent colorscale
        vmin = -vmax
    end

    return vmin, vmax

end

"""
    titledef(C::ClimGrid)

Returns the title. Used internally by [`mapclimgrid`](@ref).
"""
function titledef(C::ClimGrid)
    if ndims(C[1]) > 2

        if typeof((C[1][Axis{:time}][1])) <: AbstractCFDateTime || typeof((C[1][Axis{:time}][1])) == DateTime
            begYear = string(Dates.year(C[1][Axis{:time}][1]))
            endYear = string(Dates.year(C[1][Axis{:time}][end]))
        elseif typeof((C[1][Axis{:time}][1])) == Dates.Year
            begYear = string(C[1][Axis{:time}][1])[1:4]
            endYear = string(C[1][Axis{:time}][end])[1:4]
        elseif typeof((C[1][Axis{:time}][1])) == Int
            begYear = string(C[1][Axis{:time}][1])[1:4]
            endYear = string(C[1][Axis{:time}][end])[1:4]
        end
        titlestr = string(C[3], " - ", C[5], " - ", C.variable, " - ", begYear, " - ", endYear)

    else
        titlestr = string(C[3], " - ", C[5], " - ", C.variable)
    end

    return titlestr

end

"""
    getunitslabel(C::ClimGrid)

Return verbose label for colorbar. Used internally by [`mapclimgrid`](@ref).
"""
function getunitslabel(C::ClimGrid)

    try
        standardname = C.varattribs["standard_name"]
    catch
        standardname = C.varattribs["long_name"]
    end

    units = Dict(["air_temperature" => "Air temperature",
    "specific_humidity" => "Specific humidity (%)",
    "precipitation" => "Precipitation",
    "precipitation_flux" => "Precipitation flux",
    "air_pressure_at_sea_level" => "Air pressure at sea level",
    "eastward_wind" => "Eastward wind",
    "daily maximum temperature" => "Air temperature"
    ])

    label = ""

    try
        label = string(units[standardname], " (", C[2], ")")
    catch
        label = C[2]
    end

    return label

end

function roundup(i,v)
    return round(Int, i/v)*v
end


mutable struct Ports <: DynDevice
    state::Vector{Symbol}
    inner::Vector
end

function _make_state_mapping(components::Vector{DT}, states::Vector{Symbol}) where DT <: DynamicComponent

    local_state_mapping = Dict{DT, Vector{Int64}}()
    local_state_space_ix = 0
    for c in components
        component_state_index= Vector{Int64}(undef, length(c.states))
        for (ix,i) in enumerate(c.states)
            local_state_space_ix += 1
            component_state_index[ix] = local_state_space_ix
        end
        local_state_mapping[c] = component_state_index
    end

    return local_state_mapping

end

function _make_port_mapping(components::Vector{DT}, states::Vector{Symbol}) where DT <: DynamicComponent

    input_port_mapping = Dict{DT, Vector{Int64}}()

    for c in components
        index_component_in = Vector{Int64}()
        for i in c.ports.state
            tmp = [(ix,var) for (ix,var) in enumerate(states) if var == i]
            isempty(tmp) && continue
            push!(index_component_in, tmp[1][1])
        end
        input_port_mapping[c] = index_component_in
    end

    return input_port_mapping

end


################################################################################
#
#    NfOrd/NfOrd.jl : Orders in absolute number fields
#
# This file is part of Hecke.
#
# Copyright (c) 2015, 2016: Claus Fieker, Tommy Hofmann
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#
#  Copyright (C) 2015, 2016, 2017 Tommy Hofmann
#
################################################################################

export ==, +, basis, basis_mat, basis_mat_inv, discriminant, degree, den,
       gen_index, EquationOrder, index, isequation_order, isindex_divisor, lll,
       lll_basis, maximal_order, MaximalOrder, minkowski_mat, nf,
       norm_change_const, Order, parent, poverorder, pmaximal_overorder,
       ring_of_integers, signature, trace_matrix, different, codifferent,
       reduced_discriminant

################################################################################
#
#  Make NfOrd fully working Nemo ring
#
################################################################################

Nemo.parent_type(::Type{NfAbsOrdElem{S, T}}) where {S, T} = NfAbsOrd{S, T}

Nemo.elem_type(::Type{NfAbsOrd{S, T}}) where {S, T} = NfAbsOrdElem{S, T}

Nemo.elem_type(::NfAbsOrd{S, T}) where {S, T} = NfAbsOrdElem{S, T}

ideal_type(::NfAbsOrd{S, T}) where {S, T} = NfAbsOrdIdl{S, T}

ideal_type(::Type{NfAbsOrd{S, T}}) where {S, T} = NfAbsOrdIdl{S, T}

Nemo.show_minus_one(::Type{NfAbsOrdElem{S, T}}) where {S, T} = true

Nemo.base_ring(::NfAbsOrd) = Union{}

Nemo.needs_parentheses(::NfAbsOrdElem) = true

Nemo.isnegative(::NfAbsOrdElem) = false

################################################################################
#
#  Basic field access
#
################################################################################

doc"""
    nf(O::NfOrd) -> AnticNumberField

Returns the ambient number field of $\mathcal O$.
"""
@inline nf(O::NfAbsOrd) = O.nf

doc"""
    parent(O::NfOrd) -> NfOrdSet

Returns the parent of $\mathcal O$, that is, the set of orders of the ambient
number field.
"""
parent(O::NfOrd) = NfAbsOrdSet(nf(O), false)

doc"""
    isequation_order(O::NfOrd) -> Bool

Returns whether $\mathcal O$ is the equation order of the ambient number
field.
"""
@inline isequation_order(O::NfAbsOrd) = O.isequation_order

@inline ismaximal_known(O::NfAbsOrd) = O.ismaximal != 0

# The following function should actually do more!
@inline ismaximal(O::NfAbsOrd) = O.ismaximal == 1

################################################################################
#
#  Degree
#
################################################################################

doc"""
    degree(O::NfOrd) -> Int

Returns the degree of $\mathcal O$.
"""
degree(O::NfAbsOrd) = degree(O.nf)

################################################################################
#
#  "Assure" functions for fields
#
################################################################################

function assure_has_basis(O::NfAbsOrd)
  if isdefined(O, :basis_ord)
    return nothing
  end
  b = O.basis_nf
  d = degree(O)
  B = Vector{elem_type(O)}(d)
  for i in 1:length(b)
    v = [fmpz(0) for j in 1:d]
    one!(v[i])
    B[i] = NfAbsOrdElem(O, b[i], v)
  end
  O.basis_ord = B
  return nothing
end

function assure_has_basis_mat(O::NfAbsOrd)
  if isdefined(O, :basis_mat)
    return nothing
  end
  A = O.basis_nf
  O.basis_mat = FakeFmpqMat(basis_mat(A))
  return nothing
end

function assure_has_basis_mat_inv(O::NfAbsOrd)
  if isdefined(O, :basis_mat_inv)
    return nothing
  end
  O.basis_mat_inv = inv(basis_mat(O, Val{false}))
  return nothing
end

################################################################################
#
#  Basis
#
################################################################################

function basis_ord(O::NfAbsOrd, copy::Type{Val{T}} = Val{true}) where T
  assure_has_basis(O)
  if copy == Val{true}
    return deepcopy(O.basis_ord)::Vector{elem_type(O)}
  else
    return O.basis_ord::Vector{elem_type(O)}
  end
end

doc"""
    basis(O::NfOrd) -> Array{NfOrdElem, 1}

Returns the $\mathbf Z$-basis of $\mathcal O$.
"""
@inline basis{T}(O::NfAbsOrd, copy::Type{Val{T}} = Val{true}) = basis_ord(O, copy)

doc"""
    basis(O::NfOrd, K::AnticNumberField) -> Array{nf_elem, 1}

Returns the $\mathbf Z$-basis elements of $\mathcal O$ as elements of the
ambient number field.
"""
function basis(O::NfAbsOrd, K::AnticNumberField)
  nf(O) != K && error()
  return deepcopy(O.basis_nf)
end

################################################################################
#
#  (Inverse) basis matrix
#
################################################################################

doc"""
    basis_mat(O::NfOrd) -> FakeFmpqMat

Returns the basis matrix of $\mathcal O$ with respect to the power basis
of the ambient number field.
"""
function basis_mat(O::NfAbsOrd, copy::Type{Val{T}} = Val{true}) where T
  assure_has_basis_mat(O)
  if copy == Val{true}
    return deepcopy(O.basis_mat)
  else
    return O.basis_mat
  end
end

doc"""
    basis_mat_inv(O::NfOrd) -> FakeFmpqMat

Returns the inverse of the basis matrix of $\mathcal O$.
"""
function basis_mat_inv(O::NfAbsOrd, copy::Type{Val{T}} = Val{true}) where T
  assure_has_basis_mat_inv(O)
  if copy == Val{true}
    return deepcopy(O.basis_mat_inv)
  else
    return O.basis_mat_inv
  end
end

################################################################################
#
#  String I/O
#
################################################################################

function show(io::IO, S::NfOrdSet)
  print(io, "Set of orders of the number field ")
  print(io, S.nf)
end

function show(io::IO, O::NfAbsOrd)
  if ismaximal_known(O) && ismaximal(O)
    show_maximal(io, O)
  else
    show_gen(io, O)
  end
end

function show_gen(io::IO, O::NfAbsOrd)
  print(io, "Order of ")
  println(io, nf(O))
  print(io, "with Z-basis ")
  print(io, basis(O))
end

function show_maximal(io::IO, O::NfAbsOrd)
  print(io, "Maximal order of $(nf(O)) \nwith basis $(O.basis_nf)")
end

################################################################################
#
#  Discriminant
#
################################################################################

function assure_has_discriminant(O::NfAbsOrd)
  if isdefined(O, :disc)
    return nothing
  else
    if isequation_order(O) && issimple(nf(O))
      O.disc = numerator(discriminant(nf(O).pol))
    else
      O.disc = discriminant(basis(O))
    end
  end
  return nothing
end

doc"""
    discriminant(O::NfOrd) -> fmpz

Returns the discriminant of $\mathcal O$.
"""
function discriminant(O::NfAbsOrd)
  assure_has_discriminant(O)
  return O.disc
end

#TODO: compute differently in equation orders, this is the rres...
doc"""
   reduced_discriminant(O::NfOrd) -> fmpz
> Returns the reduced discriminant, ie. the largest elementary divisor of the
> trace matrix of $\mathcal O$.
"""
function reduced_discriminant(O::NfOrd)
  if isequation_order(O)
    Zx = PolynomialRing(FlintZZ, cached = false)[1]
    f = Zx(nf(O).pol)
    return rres(f, derivative(f))
  end
  return maximal_elementary_divisor(trace_matrix(O))
end

################################################################################
#
#  (Generalized) index
#
################################################################################

doc"""
    gen_index(O::NfOrd) -> fmpq

Returns the generalized index of $\mathcal O$ with respect to the equation
order of the ambient number field.
"""
function gen_index(O::NfAbsOrd)
  if isdefined(O, :gen_index)
    return deepcopy(O.gen_index)
  else
    O.gen_index = inv(det(basis_mat(O, Val{false})))#FlintQQ(O.basis_mat.den^degree(O), det(O.basis_mat.num))
    return deepcopy(O.gen_index)
  end
end

doc"""
    index(O::NfOrd) -> fmpz

Assuming that the order $\mathcal O$ contains the equation order
$\mathbf Z[\alpha]$ of the ambient number field, this function returns the
index $[ \mathcal O : \mathbf Z]$.
"""
function index(O::NfAbsOrd)
  if isdefined(O, :index)
    return deepcopy(O.index)
  else
    i = gen_index(O)
    !isone(denominator(i)) && error("Order does not contain the equation order")
    O.index = numerator(i)
    return deepcopy(O.index)
  end
end

################################################################################
#
#  Index divisor
#
################################################################################

doc"""
    isindex_divisor(O::NfOrd, d::fmpz) -> Bool
    isindex_divisor(O::NfOrd, d::Int) -> Bool

Returns whether $d$ is a divisor of the index of $\mathcal O$. It is assumed
that $\mathcal O$ contains the equation order of the ambient number field.
"""
function isindex_divisor(O::NfOrd, d::Union{fmpz, Integer})
  i = index(O)
  return i % d == 0
end

################################################################################
#
#  Deepcopy
#
################################################################################

doc"""
    deepcopy(O::NfOrd) -> NfOrd

Makes a copy of $\mathcal O$.
"""
function Base.deepcopy_internal(O::NfAbsOrd{S, T}, dict::ObjectIdDict) where {S, T}
  z = NfAbsOrd{S, T}(O.nf)
  for x in fieldnames(O)
    if x != :nf && isdefined(O, x)
      setfield!(z, x, Base.deepcopy_internal(getfield(O, x), dict))
    end
  end
  return z
end

################################################################################
#
#  Signature
#
################################################################################

doc"""
    signature(O::NfOrd) -> Tuple{Int, Int}

Returns the signature of the ambient number field of $\mathcal O$.
"""
function signature(x::NfOrd)
  if x.signature[1] != -1
    return x.signature
  else
    x.signature = signature(nf(x))
    return x.signature
  end
end

################################################################################
#
#  Minkowski matrix
#
################################################################################

doc"""
    minkowski_mat(O::NfOrd, abs_tol::Int = 64) -> arb_mat

Returns the Minkowski matrix of $\mathcal O$.  Thus if $\mathcal O$ has degree
$d$, then the result is a matrix in $\operatorname{Mat}_{d\times d}(\mathbf
R)$. The entries of the matrix are real balls of type `arb` with radius less
then `2^-abs_tol`.
"""
function minkowski_mat(O::NfOrd, abs_tol::Int = 64)
  if isdefined(O, :minkowski_mat) && O.minkowski_mat[2] > abs_tol
    A = deepcopy(O.minkowski_mat[1])
  else
    T = Vector{Vector{arb}}(degree(O))
    B = O.basis_nf
    for i in 1:degree(O)
      T[i] = minkowski_map(B[i], abs_tol)
    end
    p = maximum([ prec(parent(T[i][j])) for i in 1:degree(O), j in 1:degree(O) ])
    M = ArbMatSpace(ArbField(p, false), degree(O), degree(O), false)()
    for i in 1:degree(O)
      for j in 1:degree(O)
        M[i, j] = T[i][j]
      end
    end
    O.minkowski_mat = (M, abs_tol)
    A = deepcopy(M)
  end
  return A
end

################################################################################
#
#  Inclusion of number field elements
#
################################################################################

# Check if a number field element is contained in O
# In this case, the second return value is the coefficient vector with respect
# to the basis of O
function _check_elem_in_order(a::T, O::NfAbsOrd{S, T},
                              short::Type{Val{U}} = Val{false}) where {S, T, U}
  assure_has_basis_mat_inv(O)
  t = O.tcontain
  elem_to_mat_row!(t.num, 1, t.den, a)
  t = mul!(t, t, O.basis_mat_inv)
  if short == Val{true}
    return isone(t.den)
  elseif short == Val{false}
    if !isone(t.den)
      return false, Vector{fmpz}()
    else
      v = Vector{fmpz}(degree(O))
      for i in 1:degree(O)
        v[i] = deepcopy(t.num[1, i])
      end
      return true, v
    end
  end
end

doc"""
    in(a::nf_elem, O::NfOrd) -> Bool

Checks whether $a$ lies in $\mathcal O$.
"""
function in(a::nf_elem, O::NfOrd)
  return _check_elem_in_order(a, O, Val{true})
end

################################################################################
#
#  Denominator in an order
#
################################################################################

doc"""
    denominator(a::nf_elem, O::NfOrd) -> fmpz

Returns the smallest positive integer $k$ such that $k \cdot a$ is contained in
$\mathcal O$.
"""
function denominator(a::nf_elem, O::NfOrd)
  assure_has_basis_mat_inv(O)
  M = O.tcontain
  elem_to_mat_row!(M.num, 1, M.den, a)
  M = mul!(M, M, O.basis_mat_inv)
  return deepcopy(M.den)
end

function integral_split(a::nf_elem, O::NfOrd)
  assure_has_basis_mat_inv(O)
  M = O.tcontain
  elem_to_mat_row!(M.num, 1, M.den, a)
  M = mul!(M, M, O.basis_mat_inv)
  return M.den, M.num
end

##################################3#############################################
#
#  Norm change constant
#
################################################################################

# For x = \sum_i x_i omega_i let |x|_1 = \sqrt(x_1^2 + ... + x_d^2).
# And let |x|_2 = sqrt(T_2(x))
# Then there exist c1, c2 such that
# |x|_2^2 <= c1 |x|_2^2, |x|_1^2 <= c2 |x|_1^2
# A suitable pair (c1, c2) can be determined using the Minkowski map/matrix
#
# Reference
# Fieker, Friedrichs
# On Reconstruction of Algebraic Numbers
# (in particular p. 288)
#
# TODO: Make this rigorous using interval arithmetic. The only problem is that
#       the characteristic polynomial might not be squarefree.
doc"""
    norm_change_const(O::NfOrd) -> (Float64, Float64)

Returns $(c_1, c_2) \in \mathbf R_{>0}^2$ such that for all
$x = \sum_{i=1}^d x_i \omega_i \in \mathcal O$ we have
$T_2(x) \leq c_1 \cdot \sum_i^d x_i^2$
and
$\sum_i^d x_i^2 \leq c_2 \cdot T_2(x)$,
where $(\omega_i)_i$ is the $\mathbf Z$-basis of $\mathcal O$.
"""
function norm_change_const(O::NfOrd)
  if O.norm_change_const[1] > 0
    return O.norm_change_const
  else
    d = degree(O)
    M = minkowski_mat(O, 64)
    # I don't think we have to swap rows,
    # since permutation matrices are orthogonal
    #r1, r2 = signature(O)
    #for i in 2:2:r2
    #  swap_rows!(M, r1 + i, r1 + 2*r2 - i + 1)
    #end

    M = M*M'

    N = Symmetric([ Float64(M[i, j]) for i in 1:rows(M), j in 1:cols(M) ])
    #forcing N to really be Symmetric helps julia - aparently
    r = sort(eigvals(N))
    if !(r[1] > 0)
      # more complicated methods are called for...
      m = ceil(Int, log(d)/log(2))
      m += m%2
      @assert iseven(m)
      l_max = root(trace(M^m), m) #an upper bound within a factor of 2
                                    #according to a paper by Victor Pan
                                    #https://doi.org/10.1016/0898-1221(90)90236-D
                                    #formula (1) and discussion
      pr = 128
      l_min = l_max
      if isodd(d) d+=1; end
      while true
        try
          M = inv(M)
          l_min = root(trace(M^d), d) #as above...
          if isfinite(l_min)
            z = (Float64(l_max), Float64(l_min))
            O.norm_change_const = z
            return z
          end
          M = minkowski_mat(O, pr)
          pr *= 2
        catch e  # should verify the correct error
          M = minkowski_mat(O, pr)
          pr *= 2
        end
      end
    end

    @assert r[1]>0
#    N = transpose(M)*M
#    N = MatrixSpace(AcbField(prec(base_ring(N))), rows(N), cols(N))(N)
#    chi = charpoly(PolynomialRing(base_ring(N), "x")[1], N)
#    return chi
#    r = roots(chi)
#    # I want upper bound for the largest and lower bound for the smallest root
#
#    tm = arf_struct(0, 0, 0, 0)
#    ccall((:arf_init, :libarb), Void, (Ptr{arf_struct}, ), &tm)
#    ccall((:arb_get_abs_ubound_arf, :libarb), Void, (Ptr{arf_struct}, Ptr{arb}), &tm, &real(r[end]))
#    # 3 is round to infinity
#    c1 = ccall((:arf_get_d, :libarb), Cdouble, (Ptr{arf_struct}, Cint), &tm, 3)
#
#    ccall((:arb_get_abs_ubound_arf, :libarb), Void, (Ptr{arf_struct}, Ptr{arb}), &tm, &(inv(real(r[1]))))
#    c2 = ccall((:arf_get_d, :libarb), Cdouble, (Ptr{arf_struct}, Cint), &tm, 3)
#
#    ccall((:arf_clear, :libarb), Void, (Ptr{arf_struct}, ), &tm)
#
#    z = (c1, c2)
    z = (r[end], inv(r[1]))
    O.norm_change_const = z
    return z
  end
end

################################################################################
#
#  Construction
#
################################################################################

doc"""
    Order(B::Array{nf_elem, 1}, check::Bool = true) -> NfOrd

Returns the order with $\mathbf Z$-basis $B$. If `check` is set, it is checked
whether $B$ defines an order.
"""
function Order(::S, a::Array{T, 1}, check::Bool = true,
               cache::Bool = true) where {S <: Union{AnticNumberField, NfAbsNS}, T}
  K = parent(a[1])
  if check
    b, bmat, bmat_inv, _ = defines_order(K, a)
    if !b
      error("The elements do not define an order")
    else
      return NfAbsOrd(K, bmat, bmat_inv, deepcopy(a), cache)
    end
  else
    return NfAbsOrd(deepcopy(a), cache)
  end
end

function Order(K, a::Vector, check::Bool = true,
               cache::Bool = true)
  local b
  try
    b = map(K, a)
  catch
    error("Cannot coerce elements from array into the number field")
  end
  return Order(K, b, check, cache)
end

doc"""
    Order(K::AnticNumberField, A::FakeFmpqMat, check::Bool = true) -> NfOrd

Returns the order which has basis matrix $A$ with respect to the power basis
of $K$. If `check` is set, it is checked whether $A$ defines an order.
"""
function Order(K::S, a::FakeFmpqMat, check::Bool = true,
               cache::Bool = false) where {S}
  if check
    b, ainv, d = defines_order(K, a)
    if !b
      error("The basis matrix does not define an order")
    else
      return NfAbsOrd(K, deepcopy(a), ainv, d, cache)
    end
  else
    return NfAbsOrd(K, deepcopy(a), cache)
  end
end

doc"""
    Order(K::AnticNumberField, A::fmpq_mat, check::Bool = true) -> NfOrd

Returns the order which has basis matrix $A$ with respect to the power basis
of $K$. If `check` is set, it is checked whether $A$ defines an order.
"""
function Order(K::S, a::fmpq_mat, check::Bool = true,
               cache::Bool = true) where {S}
  return Order(K, FakeFmpqMat(a), cache)
end

doc"""
    Order(K::AnticNumberField, A::fmpz_mat, check::Bool = true) -> NfOrd

Returns the order which has basis matrix $A$ with respect to the power basis
of $K$. If `check` is set, it is checked whether $A$ defines an order.
"""
function Order(K::S, a::fmpz_mat, check::Bool = true,
               cache::Bool = true) where {S}
  return Order(K, FakeFmpqMat(a), check, cache)
end

doc"""
    Order(A::NfOrdFracIdl) -> NfOrd

Returns the fractional ideal $A$ as an order of the ambient number field.
"""
function Order(a::NfOrdFracIdl, check::Bool = true, cache::Bool = true)
  return Order(nf(order(a)), basis_mat(a)*basis_mat(order(a)), check, cache)
end

doc"""
    EquationOrder(K::AnticNumberField) -> NfOrd

Returns the equation order of the number field $K$.
"""
function EquationOrder(K::T, cache::Bool = false) where {T}
  M = FakeFmpqMat(identity_matrix(FlintZZ, degree(K)))
  Minv = FakeFmpqMat(identity_matrix(FlintZZ, degree(K)))
  z = NfAbsOrd{T, elem_type(T)}(K, M, Minv, basis(K), cache)
  z.isequation_order = true
  return z
end

doc"""
   equation_order(M::NfOrd) -> NfOrd
> The equation order of then number field.
"""
equation_order(M::NfAbsOrd) = equation_order(nf(M))

function _order(K::S, elt::Array{T, 1}) where {S, T}
  o = one(K)

  if !(o in elt)
    push!(elt, o)
  end

  n = degree(K)

  closed = false

  dold = fmpq(0)

  # Since 1 is in elt, prods will contain all elements

  while !closed
    prods = T[]
    for u in elt
      for v in elt
        push!(prods, u * v)
      end
    end

    B = basis_mat(prods)
    H = sub(hnf(B), (rows(B) - degree(K) + 1):rows(B), 1:degree(K))

    # TODO: Just take the product of the diagonal
    dd = H.num[1, 1]
    for i in 2:degree(K)
      dd *= H.num[i, i]
    end
    d = dd//H.den^degree(K)

    #d = prod(H.num[i,i] for i=1:degree(K))//(H.den^degree(K))
    #d = det(H)

    if iszero(d)
      error("Elements do not define a module of full rank")
    end

    if dold == d
      closed = true
    else
      dold = d
      elt = T[]
      for i in 1:n
        push!(elt, elem_from_mat_row(K, H.num, i, H.den))
      end
    end
  end

  # Make an explicit check
  @hassert :NfOrd 1 defines_order(K, elt)[1]
  return Order(K, elt, false)
end

################################################################################
#
#  Equality
#
################################################################################

# This is the function which is used in dictionaries
function Base.isequal(R::NfOrd, S::NfOrd)
  return R === S
end

function ==(R::NfAbsOrd, S::NfAbsOrd)
  nf(R) != nf(S) && return false
  assure_has_basis_mat(R)
  assure_has_basis_mat(S)
  return R.basis_mat == S.basis_mat
end

function ==(R::NfAbsOrdSet, S::NfAbsOrdSet)
  return R.nf === S.nf
end

################################################################################
#
#  Trace matrix
#
################################################################################

doc"""
    trace_matrix(O::NfOrd) -> fmpz_mat

Returns the trace matrix of `\mathcal O`, that is, the matrix
$(\operatorname{tr}_{K/\mathbf Q}(b_i \cdot b_j))_{1 \leq i, j \leq d}$.
"""
function trace_matrix(O::NfAbsOrd)
  if isdefined(O, :trace_mat)
    return deepcopy(O.trace_mat)
  end
  K = nf(O)
  b = O.basis_nf
  n = degree(K)
  g = zero_matrix(FlintZZ, n, n)
  for i=1:n
    t = trace(b[i]^2)
    @assert isinteger(t)
    g[i, i] = numerator(t)
    for j in (i + 1):n
      t = trace(b[i]*b[j])
      @assert isinteger(t)
      g[i, j] = numerator(t)
      g[j, i] = numerator(t)
    end
  end
  O.trace_mat = g
  return deepcopy(g)
end

################################################################################
#
#  Addition of orders
#
################################################################################

doc"""
    +(R::NfOrd, S::NfOrd) -> NfOrd

Given two orders $R$, $S$ of $K$, this function returns the smallest order
containing both $R$ and $S$. It is assumed that $R$, $S$ contain the ambient
equation order and have coprime index.
"""
function +(a::NfAbsOrd, b::NfAbsOrd)
  nf(a) != nf(b) && error("Orders must have same ambient number field")
  if isone(gcd(index(a), index(b)))
    aB = basis_mat(a, Val{false})
    bB = basis_mat(b, Val{false})
    d = degree(a)
    m = zero_matrix(FlintZZ, 2*d, d)
    for i=1:d
      for j=1:d
        m[i,j]=bB.den*aB.num[i,j]
        m[i+d,j]= aB.den*bB.num[i,j]
      end
    end
    mat=_hnf_modular_eldiv(m, bB.den*aB.den, :lowerleft)
    c = sub(mat, d + 1:2*d, 1:d)
    O = Order(nf(a), FakeFmpqMat(c, aB.den*bB.den), false)
    O.primesofmaximality = unique(vcat(a.primesofmaximality, b.primesofmaximality))
    O.disc=gcd(discriminant(a), discriminant(b))
    if a.disc<0
      O.disc=-O.disc
    end
    return O
  else
    return _order(nf(a), vcat(a.basis_nf, b.basis_nf))
  end
end

################################################################################
#
#  p-overorder
#
################################################################################

function _poverorder(O::NfAbsOrd, p::fmpz)
  I= pradical(O, p)
  if isdefined(I, :princ_gen) && I.princ_gen==p
    return deepcopy(O)
  end
  R = ring_of_multipliers(I)
  return R
end

function _poverorder(O::NfAbsOrd, p::Integer)
  return _poverorder(O, fmpz(p))
end

doc"""
    poverorder(O::NfOrd, p::fmpz) -> NfOrd
    poverorder(O::NfOrd, p::Integer) -> NfOrd

This function tries to find an order that is locally larger than $\mathcal O$
at the prime $p$: If $p$ divides the index $[ \mathcal O_K : \mathcal O]$,
this function will return an order $R$ such that
$v_p([ \mathcal O_K : R]) < v_p([ \mathcal O_K : \mathcal O])$. Otherwise
$\mathcal O$ is returned.
"""
function poverorder(O::NfAbsOrd, p::fmpz)
  if isequation_order(O) && issimple(nf(O))
    return dedekind_poverorder(O, p)
  else
    return _poverorder(O, p)
  end
end

function poverorder(O::NfAbsOrd, p::Integer)
  return poverorder(O, fmpz(p))
end

################################################################################
#
#  p-maximal overorder
#
################################################################################

doc"""
    pmaximal_overorder(O::NfOrd, p::fmpz) -> NfOrd
    pmaximal_overorder(O::NfOrd, p::Integer) -> NfOrd

This function finds a $p$-maximal order $R$ containing $\mathcal O$. That is,
the index $[ \mathcal O_K : R]$ is not dividible by $p$.
"""
function pmaximal_overorder(O::NfAbsOrd, p::fmpz)
  @vprint :NfOrd 1 "computing p-maximal overorder for $p ... \n"
  if rem(discriminant(O), p^2) != 0
    push!(O.primesofmaximality, p)
    return O
  end

  d = discriminant(O)
  @vprint :NfOrd 1 "extending the order at $p for the first time ... \n"
  OO = poverorder(O, p)
  dd = discriminant(OO)
  v=valuation(dd,p)
  if v==0 || v==1
    push!(OO.primesofmaximality, p)
    return OO
  end
  i = 1
  while d != dd
    i += 1
    @vprint :NfOrd 1 "extending the order at $p for the $(i)th time ... \n"
    d = dd
    OO = poverorder(OO, p)
    dd = discriminant(OO)
    v = valuation(dd,p)
    if v==0 || v==1
      break
    end
  end
  push!(OO.primesofmaximality, p)
  return OO
end

function pmaximal_overorder(O::NfAbsOrd, p::Integer)
  return pmaximal_overorder(O, fmpz(p))
end

function MaximalOrder(O::NfOrd, primes::Array{fmpz, 1})
  OO = deepcopy(O)
  disc = abs(discriminant(O))
  for i in 1:length(primes)
    p = primes[i]
    (j, disc) = remove(disc, p)
    if j == 1
      continue
    end
    @vprint :NfOrd 1 "Computing p-maximal overorder for $p ..."
    O1 = pmaximal_overorder(O, p)
    if valuation(discriminant(O1), p)< valuation(discriminant(OO),p)
      OO += O1
    end
    if !(p in OO.primesofmaximality)
      push!(OO.primesofmaximality,p)
    end
    @vprint :NfOrd 1 "done\n"
  end
  return OO
end

doc"""
***
    MaximalOrder(K::NfOrd) -> NfOrd

Returns the maximal order of $K$.
"""
function MaximalOrder(O::NfAbsOrd)
  OO = deepcopy(O)
  @vtime :NfOrd fac = factor(Nemo.abs(discriminant(O)))
  for (p,j) in fac
    if j == 1
      continue
    end
    @vprint :NfOrd 1 "Computing p-maximal overorder for $p ..."
    O1 = pmaximal_overorder(O, p)
    if valuation(discriminant(O1), p)< valuation(discriminant(OO),p)
      OO += O1
    end
    @vprint :NfOrd 1 "done\n"
  end
  OO.ismaximal = 1
  return OO
end

###############################################################################
#
#  Some LLL-related functions
#
###############################################################################

# don't know what this is doing
#for totally real field, the T_2-Gram matrix is the trace matrix, hence exact.

function _lll_gram(M::NfOrd)
  K = nf(M)
  @assert istotally_real(K)
  g = trace_matrix(M)

  q,w = lll_gram_with_transform(g)
  On = NfOrd(K, FakeFmpqMat(w*basis_mat(M, Val{false}).num, denominator(basis_mat(M, Val{false}))))
  On.ismaximal = M.ismaximal
  return On
end

doc"""
    lll_basis(M::NfOrd) -> Array{nf_elem, 1}
> A basis for $m$ that is reduced using the LLL algorithm for the Minkowski metric.
"""
function lll_basis(M::NfOrd)
  I = ideal(M, parent(basis_mat(M, Val{false}).num)(1))
  return lll_basis(I)
end

doc"""
    lll(M::NfOrd) -> NfOrd
> The same order, but with the basis now being LLL reduced wrt. the Minkowski metric.
"""
function lll(M::NfOrd)
  K = nf(M)

  if istotally_real(K)
    return _lll_gram(M)
  end

  I = ideal(M, 1)

  prec = 100
  while true
    try
      q,w = lll(I, parent(basis_mat(M).num)(0), prec = prec)
      On = NfOrd(K, FakeFmpqMat(w*basis_mat(M, Val{false}).num, denominator(basis_mat(M, Val{false}))))
      On.ismaximal = M.ismaximal
      return On
    catch e
      if isa(e, LowPrecisionLLL)
        prec = Int(round(prec*1.2))
        if prec>1000
          error("precision too large in LLL");
        end
        continue;
      else
        rethrow(e)
      end
    end
  end
end

################################################################################
#
#  Constructors for users
#
################################################################################

doc"""
***
    MaximalOrder(K::AnticNumberField) -> NfOrd

Returns the maximal order of $K$.

# Example

```julia-repl
julia> Qx, xx = FlintQQ["x"];
julia> K, a = NumberField(x^3 + 2, "a");
julia> O = MaximalOrder(K);
```
"""
function MaximalOrder(K::AnticNumberField, cache::Bool = false)
  O = EquationOrder(K)
  @vprint :NfOrd 1 "Computing the maximal order ...\n"
  O = MaximalOrder(O)
  @vprint :NfOrd 1 "... done\n"
  M = NfOrd(K, basis_mat(O), cache)
  M.ismaximal = 1
  return M
end

doc"""
***
    MaximalOrder(K::AnticNumberField, primes::Array{fmpz, 1}) -> NfOrd

Assuming that ``primes`` contains all the prime numbers at which the equation
order $\mathbf{Z}[\alpha]$ of $K = \mathbf{Q}(\alpha)$ is not maximal,
this function returns the maximal order of $K$.
"""
function MaximalOrder(K::AnticNumberField, primes::Array{fmpz, 1})
  O = EquationOrder(K)
  @vprint :NfOrd 1 "Computing the maximal order ...\n"
  O = MaximalOrder(O, primes)
  @vprint :NfOrd 1 "... done\n"
  return NfOrd(K, basis_mat(O))
end

doc"""
***
    maximal_order(K::AnticNumberField) -> NfOrd
    ring_of_integers(K::AnticNumberField) -> NfOrd

Returns the maximal order of $K$.
"""
function maximal_order(K::AnticNumberField)
  try
    c = _get_maximal_order_of_nf(K)::NfOrd
    return c
  catch e
    if !isa(e, AccessorNotSetError)
      rethrow(e)
    end
    #O = MaximalOrder(K)::NfOrd
    O = new_maximal_order(K)::NfOrd
    O.ismaximal=1
    _set_maximal_order_of_nf(K, O)
    return O
  end
end

doc"""
***
    maximal_order(K::AnticNumberField, primes::Array{fmpz, 1}) -> NfOrd
    maximal_order(K::AnticNumberField, primes::Array{Integer, 1}) -> NfOrd

Assuming that ``primes`` contains all the prime numbers at which the equation
order $\mathbf{Z}[\alpha]$ of $K = \mathbf{Q}(\alpha)$ is not maximal
(e.g. ``primes`` may contain all prime divisors of the discriminant of
$\mathbf Z[\alpha]$), this function returns the maximal order of $K$.
"""
function maximal_order(K::AnticNumberField, primes::Array{fmpz, 1})
  O = MaximalOrder(K, primes)
  return O
end

maximal_order(K::AnticNumberField, primes::Array{T, 1}) where {T} =
  maximal_order(K, map(FlintZZ, primes))

doc"""
***
    ring_of_integers(K::AnticNumberField, primes::Array{fmpz, 1}) -> NfOrd
    ring_of_integers(K::AnticNumberField, primes::Array{Integer, 1}) -> NfOrd

Assuming that ``primes`` contains all the prime numbers at which the equation
order $\mathbf{Z}[\alpha]$ of $K = \mathbf{Q}(\alpha)$ is not maximal,
this function returns the maximal order of $K$.
"""
ring_of_integers(x...) = maximal_order(x...)

################################################################################
#
#  Check if something defines an order
#
################################################################################

# This is not optimizied for performance.
# If false, then this returns (false, garbage, garbage).
# If true, then this return (true, basis_mat, basis_mat_inv).
function defines_order(K::S, x::FakeFmpqMat) where {S}
  if rows(x) != degree(K) || cols(x) != degree(K)
    return false, x, Vector{elem_type(K)}()
  end
  local xinv
  try
    xinv = inv(x)
  catch
    return false, x, Vector{elem_type(K)}()
  end
  n = degree(K)
  B_K = basis(K)
  d = Vector{elem_type(K)}(n)
  # Construct the basis elements from the basis matrix
  for i in 1:n
    d[i] = elem_from_mat_row(K, x.num, i, x.den)
  end

  # Check if Z-module spanned by x is closed under multiplcation
  l = Vector{elem_type(K)}(n)
  for i in 1:degree(K)
    for j in 1:degree(K)
      l[j] = d[i]*d[j]
    end
    Ml = basis_mat(l)
    if !isone((Ml * xinv).den)
      return false, x, Vector{elem_type(K)}()
    end
  end
  # Check if 1 is contained in the Z-module
  Ml = basis_mat([one(K)])
  if !isone((Ml * xinv).den)
    return false, x, Vector{elem_type(K)}()
  end
  return true, xinv, d
end

function defines_order(K::S, A::Vector{T}) where {S, T}
  if length(A) != degree(K)
    return false, FakeFmpqMat(), FakeFmpqMat(), A
  else
    B = basis_mat(A)
    b, Binv, _ = defines_order(K, B)
    return b, B, Binv, A
  end
end

################################################################################
#
#  Different
#
################################################################################

doc"""
    different(x::NfOrdElem) -> NfOrdElem

> The different of $x$, ie. $0$ if x is not a primitive element, or
> $f'(x)$ for $f$ the minimal polynomial of $x$ otherwise.
"""
function different(x::NfOrdElem)
  if iszero(x)
    return x
  end
  f = minpoly(x)
  if degree(f) < degree(parent(x))
    return parent(x)(0)
  end
  return derivative(f)(x)
end

doc"""
    different(R::NfOrd) -> NfOrdIdl

> The differnt ideal of $R$, that is, the ideal generated by all differents
> of elements in $R$.
> For the maximal order, this is also the inverse ideal of the co-different.
"""
function different(R::NfOrd)
  D = ideal(R, different(R(gen(nf(R)))))
  d = abs(discriminant(R))
  while norm(D) != d
    #@show D, norm(D), d
    x = rand(R, -10:10)
    y = different(x)
    if !iszero(y)
      D += ideal(R, y)
    end
  end
  return D
end

doc"""
    codifferent(R::NfOrd) -> NfOrdIdeal
> The codiffernt ideal of $R$, ie. the trace-dual of $R$.
"""
function codifferent(R::NfOrd)
  t = trace_matrix(R)
  ti, d = pseudo_inv(t)
  return ideal(R, ti, true)//d
end

###############################################################################
#
#  Buchmann-Lenstra for the computation of maximal orders
#
###############################################################################

function new_maximal_order(K::AnticNumberField)
  O=EquationOrder(K)
  if degree(K)==1
    O.ismaximal=1
    return O
  end
  ds=discriminant(O)
  #First, factorization of the discriminant given by the snf of the trace matrix
  M = trace_matrix(O)
  l = coprime_base(_el_divs(M,ds))
  @vprint :NfOrd 1 "Factors of the discriminant: $l\n "
  l1=fmpz[]
  OO=O
  @vprint :NfOrd 1 "Trial division of the discriminant\n "
  for d in l
    fac = factor_trial_range(d)[1]
    rem= d
    for (p,v) in fac
      rem=divexact(rem, p^v)
    end
    @vprint :NfOrd 1 "Computing the maximal order at $(collect(keys(fac)))\n "
    O1=MaximalOrder(O, collect(keys(fac)))
    OO+=O1
    if abs(rem)!=1
      push!(l1,abs(rem))
    end
  end
  if isempty(l1)
    OO.ismaximal=1
    return OO
  end
  for i=1:length(l1)
    a,b = ispower(l1[i])
    if a>1
      l1[i]=b
    end
  end
  #=
  O1, l=DedekindComposite(O, deepcopy(l1))
  if discriminant(O1)!=discriminant(O)
    OO+=O1
    push!(l, discriminant(O1))
  end
  append!(l1,l)
  l1=coprime_base(l1)
  =#
  O1, Q=_TameOverorderBL(OO, l1)
  if !isempty(Q)
    @vprint :NfOrd 1 "I have to factor $Q\n "
    for el in Q
      d=factor(el).fac
      O1+=MaximalOrder(O1, collect(keys(d)))
    end
  end
  O1.ismaximal=1
  return O1

end

function DedekindComposite(O::NfOrd, l::Array{fmpz,1})

  @vprint :NfOrd 1 "Dedekind criterion with $rem \n"
  O1=O
  l1=fmpz[]
  while true
    @vprint :NfOrd 1 "$l"
    if isempty(l)
      break
    end
    div, f, OO = dedekind_test_composite(O,l[1])
    if div!=1
      @show "Divisor found"
      push!(l1, div)
      push!(l,div)
      l=coprime_base(l)
      continue
    end
    if f
      @show "Can't say"
      filter!(x-> x!=l[1], l)
      continue
    else
      @show "Enlarge!"
      @show index(O1)
      O1+=OO
      filter!(x-> x!=l[1], l)
      continue
    end
  end
  return O, l1
end

function _TameOverorderBL(O::NfOrd, lp::Array{fmpz,1})

  OO=O
  M=coprime_base(lp)
  Q=fmpz[]
  while !isempty(M)
    @vprint :NfOrd 1 M
    q=M[1]
    if isprime(q)
      OO1=pmaximal_overorder(O, q)
      if valuation(discriminant(OO1), q)<valuation(discriminant(OO), q)
        OO+=OO1
      end
      filter!(x-> x!=q, M)
      continue
    end
    OO, q1= _cycleBL(OO,q)
    if q1==1
      filter!(x->x!=q, M)
    elseif q1==q
      push!(Q,q)
      filter!(x-> x!=q, M)
    else
      push!(M, q1)
      push!(M, divexact(q,q1))
      M=coprime_base(M)
      continue
    end
  end
  if isempty(Q)
    OO.ismaximal=1
  end
  return OO, Q

end

function _qradical(O::NfOrd, q::fmpz)

  R=ResidueRing(FlintZZ, q, cached=false)
  #First, we compute the q-radical as the kernel of the trace matrix mod q.
  #By theory, this is free if q is prime; if I get a non free module, I have found a factor of q.
  @vprint :NfOrd 1 "radical computation\n "
  Mat=MatrixSpace(R, degree(O), degree(O), false)(trace_matrix(O))
  M=nullspace(Mat)[1]
  if iszero(M)
    @vprint "The radical is equal to the ideal generated by q"
    return fmpz(1), ideal(O,q)
  end
  if cols(M)!=0
    M=howell_form(transpose(M))
    for i=1:rows(M)
      if iszero_row(M,i)
        break
      end
      j=i
      while iszero(M[i,j])
        j+=1
      end
      if !isone(M[i,j])
        @vprint :NfOrd 1 "Split: $(M[i,j])"
        return lift(M[i,j]), NfOrdIdl()
      end
    end
  end
  # Now, we have the radical.
  # We want to compute the ring of multipliers.
  # So we need to lift the matrix.
  @vprint :NfOrd 1 "Computing hnf of basis matrix \n "
  MatIdeal=zero_matrix(FlintZZ, rows(M)+degree(O), cols(M))
  for i=1:rows(M)
    for j=1:degree(O)
      MatIdeal[i,j]=FlintZZ(M[i,j].data)
    end
  end
  for i=1:degree(O)
    MatIdeal[i+rows(M), i]=q
  end
  gens=[O(q)]
  for i=1:rows(M)
    if !iszero_row(MatIdeal, i)
      push!(gens, elem_from_mat_row(O, MatIdeal, i))
    end
  end
  M2=sub(Hecke._hnf_modular_eldiv(MatIdeal, fmpz(q),  :lowerleft), rows(M)+1:degree(O)+rows(M), 1:degree(O))
  I=NfOrdIdl(O, M2)
  I.gens=gens
  return fmpz(1), I
end

function _cycleBL(O::NfOrd, q::fmpz)

  q1,I=_qradical(O,q)
  if q1!=1
    return O,q1
  elseif isdefined(I, :princ_gens) && q==I.princ_gens
    return O, fmpz(1)
  end
  @vprint :NfOrd 1 "ring of multipliers\n"
  O1=ring_of_multipliers(I)
  @vprint :NfOrd 1 "ring of multipliers computed\n"
  if discriminant(O1)!=discriminant(O)
    if gcd(discriminant(O1), q)== 1
      return O1, fmpz(1)
    else
      return _cycleBL(O1,q)
    end
  end
  @vprint :NfOrd 1 "couldn't enlarge\n"
  # (I:I)=OO. Now, we want to prove tameness (or get a factor of q)
  # We have to test that (OO:a)/B is a free Z/qZ module.
  inva=colon(ideal(O,1),I, true)
  M1=basis_mat_inv(inva)
  @assert M1.den==1
  G1=_el_divs(M1.num,q)
  for i=1:length(G1)
    q1=gcd(q,G1[i])
    if q1!=q && q1!=1
      @vprint :NfOrd 1 "Found the factor $q1"
      return O,fmpz(q1)
    end
  end
  @vprint :NfOrd 1 "...is free\n"
  h=2
  ideals=Array{NfOrdIdl,1}(3)
  ideals[1]=I
  ideals[2]=I^2
  ideals[3]=I^3
  while true
    if h>degree(O)
      error("Not found!")
    end
    I1=(ideals[1]+ideal(O,q))*(ideals[3]+ideal(O,q))
    I2=(ideals[2]+ideal(O,q))^2
    M2=basis_mat(I2, Val{false})*basis_mat_inv(I1, Val{false})
    @assert M2.den==1
    G2=_el_divs(M2.num,q)
    if isempty(G2)
      h+=1
      ideals[1]=ideals[2]
      ideals[2]=ideals[3]
      ideals[3]=ideals[2]*I
      continue
    end
    for i=1:length(G2)
      q1=gcd(q,G2[i])
      if q1!=q && q1!=1
        return O, q1
      end
    end
    break
  end
  f,r = ispower(q,h)
  if f
    return O, r
  else
    return O, q
  end

end

function _el_divs(M::fmpz_mat, d::fmpz)
  M1=_hnf_modular_eldiv(M, d)
  while !isdiag(M1)
    M1= M1'
    hnf_modular_eldiv!(M1, d)
  end
  l=fmpz[]
  for j=1:rows(M1)
    if M1[j,j]!=1
      push!(l, M1[j,j])
    end
  end
  return l
end

function TameOverorderBL(O::NfOrd, lp::Array{fmpz,1}=fmpz[])

  # First, we hope that we can get a factorization of the discriminant by computing
  # the structure of the group OO^*/OO
  OO=O
  list=append!(_el_divs(trace_matrix(OO)),primes_up_to(degree(O)))
  l=coprime_base(list)
  #Some trivial things, maybe useless
  for i=1:length(l)
    a,b=ispower(l[i])
    if a>1
      l[i]=b
    end
    if isprime(l[i])
      @vprint :NfOrd 1 "pmaximal order at $(l[i])\n"
      OO1=pmaximal_overorder(O, l[i])
      if valuation(discriminant(OO1), l[i])<valuation(discriminant(OO), l[i])
        OO+=OO1
      end
      l[i]=0
    end
  end
  push!(l, discriminant(OO))
  append!(l,lp)
  filter!(x-> x!=0, l)
  for i=1:length(l)
    l[i]=abs(l[i])
  end
  M=coprime_base(l)
  Q=fmpz[]
  while !isempty(M)
    @vprint :NfOrd 1 M
    q=M[1]
    if isprime(q)
      OO1=pmaximal_overorder(O, q)
      if valuation(discriminant(OO1), q)<valuation(discriminant(OO), q)
        OO+=OO1
      end
      filter!(x-> x!=q, M)
      continue
    end
    OO, q1= _cycleBL(OO,q)
    if q1==1
      filter!(x->x!=q, M)
    elseif q1==q
      push!(Q,q)
      filter!(x-> x!=q, M)
    else
      push!(M, q1)
      push!(M, divexact(q,q1))
      M=coprime_base(M)
      continue
    end
  end
  if isempty(Q)
    OO.ismaximal=1
  end
  return OO, Q

end


using Flux

"""
    Stack(topo::NNTopo, layers...)

like Flux.Chain, but you can use a NNTopo to define the order/structure of the function called.
"""
struct Stack{T<:Tuple, FS}
    models::T
    topo::NNTopo{FS}
    Stack(topo::NNTopo{FS}, xs...) where FS = new{typeof(xs), FS}(xs, topo)
end

Flux.children(s::Stack) = s.models
Flux.mapchildren(f, s::Stack) = Stack(s.topo, f.(s.models)...)

(s::Stack)(xs...) = s.topo(s.models, xs...)

"return a list of n model with give args"
stack(n, modeltype::DataType, args...; kwargs...) = [modeltype(args...; kwargs...) for i = 1:n]

function Base.show(io::IO, s::Stack)
    print(io, "Stack(")
    join(io, s.models, ", ")
    print(io, ")")
end

"show the structure of the Stack function"
show_stackfunc(s::Stack) = print_topo(s.topo; models=s.models)


"""
    add_sub_heatmap!(f, m)

Internal function to add a heatmap subroutine to the GLE script.
This subroutine only generates one column (resp. one row) of boxes, not the
full grid (that is done by calling the routine repeatedly).
"""
function add_sub_heatmap!(f::Figure, hm::Heatmap, hashid::UInt)
    #=
    zij = 0
    cij = ""
    for i = 1 to ndata(ds)
        zij = datayvalue(ds, i)
        if zij <= 1 then
            cij = firstcolor
        else if zij <=2 then
            cij = secondcolor
        ...
        else
            cij = missingcolor
        end

        move and  draw box(zij, cij)
    end
    =#
    ifpart = """
        if zij <= 1 then
        \t\t    cij\$ = \"$(col2str(hm.cmap[1]))\"
        """
    for k ∈ 2:length(hm.cmap)-1
        ifpart *= """
            \n\t\telse if zij <= $k then
            cij\$ = \"$(col2str(hm.cmap[k]))\"
            """
    end
    ifpart *= """
        \n\t\telse
            cij\$ = \"$(col2str(hm.cmiss))\"
        end if"""

    box_default = """
        amove xg((j-1)*bw) yg(1-i*bh)
        \t\tbox xg(bw)-xg(0) yg(bh)-yg(0) nobox fill cij\$
        """
    box_transpose = """
        amove xg((i-1)*bh) yg(1-j*bw)
        \t\tbox xg(bh)-xg(0) yg(bw)-yg(0) nobox fill cij\$
        """
    boxpart = ifelse(hm.transpose, box_transpose, box_default)

    f.subroutines["hm_$hashid"] = """
        sub hm_$hashid j ds\$ bw bh
            local zij = 0
            local cij = \"\"
            for i = 1 to ndata(ds\$)
                zij = datayvalue(ds\$,i)
                $ifpart
                $boxpart
            next i
        end sub
        """
    return
end


export Abstract_Model2Fit
export parameter_size, accumulate_y!, eval_y, alloc_y

@doc raw"""

Abstract type, base of all model to fit.

# Interface

The interface is very simple

- [`parameter_size`](@ref)
- [`accumulate_y!`](@ref)

# Convenience functions

- [`eval_y`](@ref)

"""
abstract type Abstract_Model2Fit end

# Visit  ================
#
visit_submodel_size(model::Abstract_Model2Fit) = @assert(false,"To implement for $(typeof(model)) type!")
visit_get_submodel(model::Abstract_Model2Fit,submodel_idx::Int) = @assert(false,"To implement for $(typeof(model)) type!")
visit_get_Y(model::Abstract_Model2Fit,submodel_idx::Int,Y::AbstractVector,X::AbstractVector,θ::AbstractVector) = @assert(false,"To implement for $(typeof(model)) type!")
visit_get_X(model::Abstract_Model2Fit,submodel_idx::Int,Y::AbstractVector,X::AbstractVector,θ::AbstractVector) = @assert(false,"To implement for $(typeof(model)) type!")
visit_get_θ(model::Abstract_Model2Fit,submodel_idx::Int,Y::AbstractVector,X::AbstractVector,θ::AbstractVector) = @assert(false,"To implement for $(typeof(model)) type!")

# Public functions ================
#
@doc raw"""
```julia
parameter_size(::Abstract_Model2Fit)::Int
```

Return length of the expected parameter vector `θ`

# Also see
- [`Abstract_Model2Fit`](@ref)
"""
parameter_size(model::Abstract_Model2Fit) = @assert(false,"To implement for $(typeof(model)) type!")

@doc raw"""
```julia
accumulate_y!(::Abstract_Model2Fit,Y::AbstractVector,X::AbstractVector,θ::AbstractVector)::AbstractVector
```

Accumulate model contribution into vector `Y`.

# Also see
- [`Abstract_Model2Fit`](@ref)
- [`eval_y`](@ref)
"""
accumulate_y!(model::Abstract_Model2Fit,Y::AbstractVector,X::AbstractVector,θ::AbstractVector)  = @assert(false,"To implement for $(typeof(model)) type!")

# convenience

function alloc_y(m::Abstract_Model2Fit,X::AbstractVector,θ::AbstractVector)
    zeros(promote_type(eltype(X),eltype(θ)),length(X))
end

@doc raw"""
```julia
eval_y(m::Abstract_Model2Fit,X::AbstractVector,θ::AbstractVector)::AbstractVector
```

A convenience function that call [`accumulate_y!`](@ref) using a zero
initialized `Y` vector. This returned vector contains model values.

# Also see
- [`Abstract_Model2Fit`](@ref)
"""
eval_y(m::Abstract_Model2Fit,X::AbstractVector,θ::AbstractVector) = accumulate_y!(m,alloc_y(m,X,θ),X,θ)


include("../imports.jl")

@testset "keypointpreprocessing.jl" begin

    ks = [
        SVector{2, Float32}(10, 10),
        SVector{2, Float32}(50, 80),
    ]
    sz = (100, 100)

    block = Keypoints{2}(2)
    enc = KeypointPreprocessing(sz)
    ctx = Training()

    testencoding(enc, block, ks)
    y = encode(enc, ctx, block, ks)
    ks_ = decode(enc, ctx, encodedblock(enc, block), y)
    @test ks ≈ ks_

end


using DataFrames
using CSV
using Plots
#%%
# run(`./pi2`)
x=-1:0.1:5
f(x)=exp(-x)-x
y=f.(x)

plot(x,y)
xaxis!("x")
yaxis!("y")

#%%
using Pkg

Pkg.add("ClusterManagers")

#%%
using ClusterManagers
using Distributed

n_processes=5
ENV["HOSTNAME"]="127.0.0.1"
ClusterManagers.addprocs_htc(n_processes)

#%%
@distributed for i=1:n_processes
       run(`hostname`)
       end


@testset "promotion" begin
    for (mr,br) in ((DynamicAxis(10),         OneTo(10)),)
        @testset "$(typeof(br)) lower range" begin
            @test last(promote(br, mr)) == mr
            @test last(promote(mr, br)) == mr
        end
        for (mr2,br2) in ((DynamicAxis(10),         OneTo(10)),
                             )
            if br != br2
                @testset "$(typeof(br)) & $(typeof(br2))" begin
                    br3 = last(promote(br, br2))
                    @test eltype(last(promote(mr, mr2))) == eltype(br3)
                    @test eltype(last(promote(mr2, mr))) == eltype(br3)

                end
            end
        end
    end
    @testset "promote_rule" begin
        for (t1,t2,p) in ((DynamicAxis, OneTo{Int}, OneTo{Int}),
                          (DynamicAxis, OneTo{Int}, OneTo{Int}))
            @testset "$t1 + $t2" begin
                @test (@inferred(promote_rule(t1, t2)) <: p) == true
                @test (@inferred(promote_rule(t2, t1)) <: p) == true
            end
        end
    end
end


using Revise, Parameters, Logging, Test
using CUDA
# include("/home/jakub/Documents/NuclearMedEval/src/utils/CUDAAtomicUtils.jl")

# includet("/home/jakub/Documents/NuclearMedEval/src/utils/BitWiseUtils.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/structs/BasicStructs.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/utils/CUDAGpuUtils.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/utils/IterationUtils.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/utils/ReductionUtils.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/utils/MemoryUtils.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/distanceMetrics/Housdorff/verB/MetaDataUtils.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/distanceMetrics/Housdorff/verB/ResultListUtils.jl")

# includet("/home/jakub/Documents/NuclearMedEval/src/overLap/MainOverlap.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/PairCounting/RandIndex.jl")

# includet("/home/jakub/Documents/NuclearMedEval/src/Probabilistic/ProbabilisticMetrics.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/InformationTheorhetic/InformationTheorhetic.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/volume/VolumeMetric.jl")

# includet("/home/jakub/Documents/NuclearMedEval/src/distanceMetrics/MeansMahalinobis.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/distanceMetrics/Housdorff/verB/HFUtils.jl")

# includet("/home/jakub/Documents/NuclearMedEval/src/distanceMetrics/Housdorff/verB/PrepareArrtoBool.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/distanceMetrics/Housdorff/verB/WorkQueueUtils.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/distanceMetrics/Housdorff/verB/ProcessMainDataVerB.jl")
# includet("/home/jakub/Documents/NuclearMedEval/test/GPUtestUtils.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/distanceMetrics/Housdorff/verB/ScanForDuplicates.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/distanceMetrics/Housdorff/verB/MetadataAnalyzePass.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/distanceMetrics/Housdorff/verB/MainLoopKernel.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/distanceMetrics/Housdorff/verB/Housdorff.jl")

# includet("/home/jakub/Documents/NuclearMedEval/src/kernels/TpfpfnKernel.jl")
# includet("/home/jakub/Documents/NuclearMedEval/src/kernels/InterClassCorrKernel.jl")

includet("C:/GitHub/GitHub/NuclearMedEval/src/utils/CUDAAtomicUtils.jl")

includet("C:/GitHub/GitHub/NuclearMedEval/src/utils/BitWiseUtils.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/structs/BasicStructs.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/utils/CUDAGpuUtils.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/utils/IterationUtils.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/utils/ReductionUtils.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/utils/MemoryUtils.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/distanceMetrics/Housdorff/verB/MetaDataUtils.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/distanceMetrics/Housdorff/verB/ResultListUtils.jl")

includet("C:/GitHub/GitHub/NuclearMedEval/src/overLap/MainOverlap.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/PairCounting/RandIndex.jl")

includet("C:/GitHub/GitHub/NuclearMedEval/src/Probabilistic/ProbabilisticMetrics.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/InformationTheorhetic/InformationTheorhetic.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/volume/VolumeMetric.jl")

includet("C:/GitHub/GitHub/NuclearMedEval/src/distanceMetrics/MeansMahalinobis.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/distanceMetrics/Housdorff/verB/HFUtils.jl")

includet("C:/GitHub/GitHub/NuclearMedEval/src/distanceMetrics/Housdorff/verB/PrepareArrtoBool.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/distanceMetrics/Housdorff/verB/WorkQueueUtils.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/distanceMetrics/Housdorff/verB/ProcessMainDataVerB.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/test/GPUtestUtils.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/distanceMetrics/Housdorff/verB/ScanForDuplicates.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/distanceMetrics/Housdorff/verB/MetadataAnalyzePass.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/distanceMetrics/Housdorff/verB/MainLoopKernel.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/distanceMetrics/Housdorff/verB/Housdorff.jl")

includet("C:/GitHub/GitHub/NuclearMedEval/src/kernels/TpfpfnKernel.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/kernels/InterClassCorrKernel.jl")
includet("C:/GitHub/GitHub/NuclearMedEval/src/MainAbstractions.jl")


@testset "638.shopping-offers.jl" begin
    @test shopping_offers([2, 5], [[3, 0, 5], [1, 2, 10]], [3, 2]) == 14
    @test shopping_offers([2, 3, 4], [[1, 1, 0, 4], [2, 2, 1, 9]], [1, 2, 1]) == 11
    @test shopping_offers([0, 0, 0], [[1, 1, 0, 4], [2, 2, 1, 9]], [1, 1, 1]) == 0
end


"""
```
Network(id)
```

Construct an empty `Network` representing a system of interacting particles. A particle is represented by a `Species`, and an interaction is represented by a `Reaction`.

Add a `Species` or `Reaction` using `<=`:

```
m <= Species("X", 100)
m <= Reaction("birth", 2.0, "X --> X + X")
```
"""
struct Network
  identifier :: Symbol

  species_list  :: OrderedDict{Symbol,Species}
  reaction_list :: OrderedDict{Symbol,Reaction}
  dep_graph :: OrderedDict{Symbol,Vector{Symbol}}

  function Network(id)
    s  = OrderedDict{Symbol,Species}()
    r  = OrderedDict{Symbol,Reaction}()
    dep_graph = OrderedDict{Symbol,Vector{Symbol}}()

    return new(Symbol(id), s, r, dep_graph)
  end
end

"""
```
species_list(x::Network)
```

Retrieve `Species` from a `Network`.
"""
species_list(x::Network) = x.species_list

"""
```
reaction_list(x::Network)
```

Retrieve `Reaction`s from a `Network`.
"""
reaction_list(x::Network) = x.reaction_list

get_species(m::Network, key::AbstractString) = get_species(m, Symbol(key))

function get_species(m::Network, key::Symbol)
  s = get(m.species_list, key, nothing)

  if isnothing(s)
    error("""
    Species $(key) not defined.
    Add it with `m <= Species("$(key)")`.
    """)
  end

  return s
end

get_reaction(m::Network, key::AbstractString) = get_reaction(m, Symbol(key))

function get_reaction(m::Network, key::Symbol)
  r = get(m.reaction_list, key, nothing)

  if isnothing(r)
    error("""
    Reaction $(key) not defined.
    Add it with `m <= Reaction("$(key)", rate, formula)`.
    """)
  end

  return r
end

number_species(x::Network)   = length(species_list(x))
number_reactions(x::Network) = length(reaction_list(x))

function Base.show(io::IO, x::Network)
  print(io, "[ Model: $(x.identifier) ]\n")
  print(io, "  no. species:    $(number_species(x))\n")
  print(io, "  no. reactions:  $(number_reactions(x))")
end

function add_object!(model, object, fieldname)
  dict = getfield(model, fieldname)
  id   = object.identifier
  setindex!(dict, object, Symbol(id))
  return model
end

function (<=)(model::Network, object::Species)
  add_object!(model, object, :species_list)
end

function (<=)(model::Network, object::Reaction)
  reactants = object.reactants
  products  = object.products

  try
    validate(reactants, model)
    validate(products,  model)

    # add the reaction to the list of objects
    add_object!(model, object, :reaction_list)

    id = Symbol(object.identifier)
    dep_graph = model.dep_graph

    if haskey(dep_graph, id)
      # if the key already exists, empty the list of dependents
      empty!(dep_graph[id])

      # and remove the reaction as a dependency
      for (key, r) in reaction_list(model)
        list = dep_graph[key]
        ix = findfirst(list, id)
        ix > 0 && deleteat!(list, ix)
      end
    else
      # if the key does not exist, make a new vector for dependents
      dep_graph[id] = Symbol[]
    end

    # update the dependency graph
    update_dep_graph!(model, object)

  catch ex
    rethrow(ex)
  end
end

function validate(participants, model)
  species_dict = species_list(model)
  for species in keys(participants)
    if !haskey(species_dict, Symbol(species))
      error("$(species) is not defined.")
    end
  end
end

function rmv_object!(model, key, fieldname)
  dict = getfield(model, fieldname)
  if haskey(dict, key)
    delete!(dict, key)
  else
    error("$(key) not found in $(fieldname)")
  end
  return model
end

function update_dep_graph!(model, rj)
  id = Symbol(rj.identifier)

  rj_affects    = rj.affects
  rj_affectedby = rj.affectedby

  for (key, rk) in reaction_list(model)
    rk_affects    = rk.affects
    rk_affectedby = rk.affectedby

    # add edge R_j --> R_k
    if !isempty(intersect(rj_affectedby, rk_affects))
      push!(model.dep_graph[id], key)
    end

    # add edge R_k --> R_j
    if rj != rk && !isempty(intersect(rk_affectedby, rj_affects))
      push!(model.dep_graph[key], id)
    end
  end

  return model
end


using VTKDataIO
using PyCall
using VTKDataTypes
using DashVtk
using Dash
using DashHtmlComponents

#dash = pyimport("dash")
#dash_html = pyimport("dash_html_components")
dash_vtk = pyimport("dash_vtk")
x = y = z = [-2, -1, 0, 1, 2];
rect = VTKRectilinearData((x, y, z));
rect.cell_data["Cell scalar"] = reshape([rand() for i in 1:num_of_cells(rect)], cell_extents(rect));
unstruct = VTKUnstructuredData(rect)
pyunstruct = PyVTK(unstruct)
dash_vtk_utils = pyimport("dash_vtk.utils")

dash_vtk_utils.to_mesh_state
state = dash_vtk_utils.to_mesh_state(pyunstruct)

content = vtk_view([
        vtk_geometryrepresentation([
        vtk_mesh(state=state)
    ]),
])

app = dash()

app.layout = html_div() do
    html_div(
    style = Dict(
        "width" => "100%",
        "height" => "400px"
    ),
    children=[content],
    )
end

run_server(app, "0.0.0.0", debug = true)


using TableSchema
import TableSchema: read, validate

include("../src/DataPackage.jl")
using DataPackage
import DataPackage: add_resource

# Initialise a resource from a CSV file
# Source: https://opendata.swiss/en/dataset/kennzahlen-der-schweizer-pflegeheime
res = Resource("data/health/hopitaux_ch_2016_Sample_fr.csv", strict=false, name="hopitaux")

# Add this resource to a blank data package
p = Package()
add_resource(p, res)

# Read the data from the table
data = DataPackage.read(res)
println( "The number of cells is ", length(data[:,1]) )
println( "Column 8 is called ", res.schema.fields[8].name )
println( "Sum of column 8 is ", sum([ row for row in data[:,8] ]) )

# Validate the table
table = DataPackage.get_table(res)
if validate(table); println("The table is valid according to the Schema"); end


#
# Unit tests for AstrodynamicalModels.jl
#

include("R2BP.jl")
include("CR3BP.jl")
include("NBP.jl")


# addprocs(3)
using Test
# using Compat
# using IncrementalInference

@testset "out of module evalPotential..." begin
    include("TestModuleFunctions.jl")
end

include("testStateMachine.jl")

include("testCompareVariablesFactors.jl")

@testset "Ensure memory return is working properly..." begin
    include("typeReturnMemRef.jl")
end

include("basicGraphsOperations.jl")

include("testPartialFactors.jl")

@testset "basic Bayes tree construction" begin
    include("testBayesTreeiSAM2Example.jl")
end
@testset "Ensure converter types can be run from extending namespaces..." begin
    include("saveconvertertypes.jl")
end
@testset "packing converters work..." begin
    include("testgraphpackingconverters.jl")
end
include("testNLsolve.jl")

@testset "generic root finding by numeric solve of residual functions..." begin
    include("testNumericRootGenericRandomized.jl")
end
@testset "GenericWrapParam functors..." begin
    include("testCommonConvWrapper.jl")
end
@testset "with simple local constraint examples Odo, Obsv2..." begin
    include("testlocalconstraintexamples.jl")
end

include("testFactorMetadata.jl")

include("testExplicitMultihypo.jl")

include("TestCSMMultihypo.jl")

include("testMultiHypo2Door.jl")

include("testMultithreaded.jl")

@testset "partial constraints..." begin
    include("testpartialconstraint.jl")
end
@testset "null hypothesis..." begin
    include("testnullhypothesis.jl")
end
@testset "standardized multihypothesis..." begin
    include("testmultihypothesisapi.jl")
end
@testset "with local Graphs.jl dictionary and arrays only (multicore)..." begin
    include("fourdoortest.jl")
end

@testset "saving to and loading from FileDFG" begin
    saveFolder = "/tmp/dfg_test"
    saveDFG(fg, saveFolder)
    retDFG = GraphsDFG{SolverParams}(params=SolverParams())
    retDFG = loadDFG(saveFolder, IncrementalInference, retDFG)
    @test symdiff(ls(fg), ls(retDFG)) == []
    @test symdiff(lsf(fg), lsf(retDFG)) == []
end

@warn "must return testExpandedJLD.jl to testing -- currently skipped since jld2 files cannot be loaded."
# include("testExpandedJLD.jl")

include("testTexTreeIllustration.jl")

#


@info "Testing PointCloud..."
@testset "PointCloud" begin
    for T in [Float32, Float64], _N in [true, false]
        points = rand(T, 3, 16, 4)
        _normals = rand(T, 3, 16, 4)
        if _N
            normals = _normals
        else
            normals = nothing
        end
        rep = PointCloud(points, normals)
        if normals == nothing
            @test rep.normals isa Nothing
        else
            @test size(rep.normals) == size(_normals)
            @test rep.normals isa Array{Float32,3}
        end
        @test npoints(rep) == size(points, 2)
        @test size(rep.points) == size(points)
        @test rep.points isa Array{Float32,3}

        for i = 1:size(points, 3)
            @test all(isapprox.(rep[i], points[:, :, i]))
        end

        # using other contructor
        rep1 = PointCloud(points = points, normals = normals)
        rep2 = PointCloud(rep)
        @test rep.points == rep1.points
        @test rep.points == rep2.points
        @test rep.normals == rep1.normals
        @test rep.normals == rep2.normals
    end
end

@info "Testing VoxelGrid..."
@testset "VoxelGrid" begin
    res = 4
    voxels = rand(Float32, res, res, res, 2)
    v = VoxelGrid(voxels)
    @test Flux3D._assert_voxel(v)
    @test v isa VoxelGrid{Float32}
    @test v[1] == voxels[:, :, :, 1]
    @test v[2] == voxels[:, :, :, 2]
    @test v.voxels isa Array{Float32,4}
    @test size(v.voxels) == (res, res, res, 2)

    voxels = rand(Float32, res, res, res)
    v2 = VoxelGrid(voxels)
    @test v2 isa VoxelGrid{Float32}
    @test v2[1] == voxels[:, :, :, 1]
    @test size(v2.voxels) == (res, res, res, 1)
end

@info "Testing TriMesh..."
@testset "TriMesh" begin

    T, R = (Float32, Int64)
    verts1 = [
        0.1 0.3 0.5
        0.5 0.2 0.1
        0.6 0.8 0.7
    ]
    verts2 = [
        0.1 0.3 0.3
        0.6 0.7 0.8
        0.2 0.3 0.4
        0.1 0.5 0.3
    ]
    verts3 = [
        0.7 0.3 0.6
        0.2 0.4 0.8
        0.9 0.5 0.2
        0.2 0.3 0.4
        0.9 0.3 0.8
    ]

    verts_list = [T.(verts1'), T.(verts2'), T.(verts3')]

    faces1 = [1 2 3]
    faces2 = [
        1 2 3
        2 3 4
    ]
    faces3 = [
        2 3 1
        1 2 4
        3 4 2
        5 4 3
        5 1 2
        5 4 2
        5 3 2
    ]

    faces_list = [R.(faces1'), R.(faces2'), R.(faces3')]
    m = TriMesh(verts_list, faces_list)

    # IO Tests
    @testset "IO" begin
        mktempdir() do tmpdir
            # FIXME: MeshIO 2dm save/load breaking #62, #63
            # for ext in ["obj", "off", "stl", "ply", "2dm"]
            for ext in ["obj", "off", "stl", "ply"]
                save_trimesh(joinpath(tmpdir, "test.$(ext)"), m)
                m_loaded = load_trimesh(joinpath(tmpdir, "test.$(ext)"))
                @test all(isapprox.(get_verts_packed(m_loaded), verts_list[1]))
                @test get_faces_packed(m_loaded) == faces_list[1]
            end
        end
    end

    @test all(verts_list .== get_verts_list(m))
    @test cat(verts_list...; dims = 2) == get_verts_packed(m)
    _padded = get_verts_padded(m)
    for (i, v) in enumerate(verts_list)
        @test _padded[:, 1:size(v, 2), i] == v
        @test all(_padded[:, size(v, 2)+1:end, i] .== 0)
    end

    @test all(faces_list .== get_faces_list(m))
    _packed = get_faces_packed(m)
    _cur_idx = 1
    _offset = 0
    for (i, f) in enumerate(faces_list)
        @test _packed[:, _cur_idx:_cur_idx+size(f, 2)-1] == (f .+ _offset)
        _cur_idx += size(f, 2)
        _offset += size(verts_list[i], 2)
    end
    _padded = get_faces_padded(m)
    for (i, v) in enumerate(faces_list)
        @test _padded[:, 1:size(v, 2), i] == v
        @test all(_padded[:, size(v, 2)+1:end, i] .== 0)
    end

    _packed = get_faces_packed(m)
    e12 = cat(_packed[1, :], _packed[2, :], dims = 2)
    e23 = cat(_packed[2, :], _packed[3, :], dims = 2)
    e31 = cat(_packed[3, :], _packed[1, :], dims = 2)
    edges = cat(e12, e23, e31, dims = 1)
    edges = sort(edges, dims = 2)
    edges = sortslices(edges; dims = 1)
    edges = unique(edges; dims = 1)
    @test edges == get_edges_packed(m)

    _dict = get_edges_to_key(m)
    for i = 1:size(edges, 1)
        @test i == _dict[Tuple(edges[i, :])]
    end

    _f_edges = get_faces_to_edges_packed(m)
    _packed = get_faces_packed(m)
    for i = 1:size(_f_edges, 1)
        @test edges[_f_edges[i, 1], :] == sort(_packed[[2, 3], i])
        @test edges[_f_edges[i, 2], :] == sort(_packed[[1, 3], i])
        @test edges[_f_edges[i, 3], :] == sort(_packed[[1, 2], i])
    end

    _edges = get_edges_packed(m)
    V = size(get_verts_packed(m), 2)
    L = zeros(V, V)
    deg = zeros(V, V)
    for i = 1:size(_edges, 1)
        L[_edges[i, 1], _edges[i, 2]] = 1
        L[_edges[i, 2], _edges[i, 1]] = 1
    end
    deg = sum(L; dims = 2)
    deg = map(x -> (x > 0 ? 1 / x : x), deg)
    for i = 1:V, j = 1:V
        if i == j
            L[i, j] = -1
        elseif L[i, j] == 1
            L[i, j] = deg[i]
        end
    end
    @test all(isapprox.(L, Array(get_laplacian_packed(m)), rtol = 1e-5, atol = 1e-5))

    # verts_normals, faces_normals, faces_areas
    verts1 = [
        0.1 0.3 0.0
        0.5 0.2 0.0
        0.6 0.8 0.0
        0.0 0.3 0.2
        0.0 0.2 0.5
        0.0 0.8 0.7
        0.5 0.0 0.2
        0.6 0.0 0.5
        0.8 0.0 0.7
        0.0 0.0 0.0
        0.0 0.0 0.0
        0.0 0.0 0.0
    ]

    verts2 = [
        0.1 0.3 0.0
        0.5 0.2 0.0
        0.6 0.8 0.0
        0.0 0.3 0.2
        0.0 0.2 0.5
        0.0 0.8 0.7
    ]

    verts2 = [
        0.1 0.3 0.0
        0.5 0.2 0.0
        0.0 0.3 0.2
        0.0 0.2 0.5
        0.0 0.8 0.7
    ]

    faces1 = [
        1 2 3
        4 5 6
        7 8 9
        10 11 12
    ]

    faces2 = [
        1 2 3
        3 4 5
    ]

    _normal1 = [
        0.0 0.0 1.0
        0.0 0.0 1.0
        0.0 0.0 1.0
        -1.0 0.0 0.0
        -1.0 0.0 0.0
        -1.0 0.0 0.0
        0.0 1.0 0.0
        0.0 1.0 0.0
        0.0 1.0 0.0
        0.0 0.0 0.0
        0.0 0.0 0.0
        0.0 0.0 0.0
    ]

    _normal2 = [
        -0.2408 -0.9631 -0.1204
        -0.2408 -0.9631 -0.1204
        -0.9389 -0.3414 -0.0427
        -1.0000 0.0000 0.0000
        -1.0000 0.0000 0.0000
    ]

    _fnormal1 = [
        -0.0 0.0 1.0
        -1.0 0.0 0.0
        0.0 1.0 0.0
        0.0 0.0 0.0
    ]

    _fnormal2 = [
        -0.2408 -0.9631 -0.1204
        -1.0000 0.0000 0.0000
    ]

    _farea1 = [0.125, 0.1, 0.02, 0.0]
    _farea2 = [0.0415, 0.1]

    _normal1 = _normal1'
    _normal2 = _normal2'
    _fnormal1 = _fnormal1'
    _fnormal2 = _fnormal2'
    _farea1 = _farea1'
    _farea2 = _farea2'

    # verts_normals, faces_normals, faces_areas
    for T in [Float64, Float32], R in [Int64, UInt32]
        verts = [T.(verts1'), T.(verts2')]
        faces = [R.(faces1'), R.(faces2')]
        m = TriMesh(verts, faces)

        @test compute_verts_normals_packed(m) isa Array{T,2}
        @test compute_verts_normals_padded(m) isa Array{T,3}
        @test compute_verts_normals_list(m) isa Array{Array{T,2},1}
        @test all(
            isapprox.(
                cat(_normal1, _normal2; dims = 2),
                compute_verts_normals_packed(m),
                rtol = 1e-4,
                atol = 1e-4,
            ),
        )
        @test all(
            isapprox.(
                [_normal1, _normal2],
                compute_verts_normals_list(m),
                rtol = 1e-4,
                atol = 1e-4,
            ),
        )
        @test all(
            isapprox.(
                _normal1,
                compute_verts_normals_padded(m)[:, 1:size(_normal1, 2), 1],
                rtol = 1e-4,
                atol = 1e-4,
            ),
        )
        @test all(
            isapprox.(
                _normal2,
                compute_verts_normals_padded(m)[:, 1:size(_normal2, 2), 2],
                rtol = 1e-4,
                atol = 1e-4,
            ),
        )
        @test all(compute_verts_normals_padded(m)[:, size(_normal1, 2)+1:end, 1] .== 0)
        @test all(compute_verts_normals_padded(m)[:, size(_normal2, 2)+1:end, 2] .== 0)

        @test compute_faces_normals_packed(m) isa Array{T,2}
        @test compute_faces_normals_padded(m) isa Array{T,3}
        @test compute_faces_normals_list(m) isa Array{Array{T,2},1}
        @test all(
            isapprox.(
                cat(_fnormal1, _fnormal2; dims = 2),
                compute_faces_normals_packed(m),
                rtol = 1e-4,
                atol = 1e-4,
            ),
        )
        @test all(
            isapprox.(
                [_fnormal1, _fnormal2],
                compute_faces_normals_list(m),
                rtol = 1e-4,
                atol = 1e-4,
            ),
        )
        @test all(
            isapprox.(
                _fnormal1,
                compute_faces_normals_padded(m)[:, 1:size(_fnormal1, 2), 1],
                rtol = 1e-4,
                atol = 1e-4,
            ),
        )
        @test all(
            isapprox.(
                _fnormal2,
                compute_faces_normals_padded(m)[:, 1:size(_fnormal2, 2), 2],
                rtol = 1e-4,
                atol = 1e-4,
            ),
        )
        @test all(compute_faces_normals_padded(m)[:, size(_fnormal1, 2)+1:end, 1] .== 0)
        @test all(compute_faces_normals_padded(m)[:, size(_fnormal2, 2)+1:end, 2] .== 0)

        @test compute_faces_areas_packed(m) isa Array{T,1}
        @test compute_faces_areas_padded(m) isa Array{T,3}
        @test compute_faces_areas_list(m) isa Array{Array{T,2},1}
        @test all(
            isapprox.(
                reshape(cat(_farea1, _farea2; dims = 2), :),
                compute_faces_areas_packed(m),
                rtol = 1e-4,
                atol = 1e-4,
            ),
        )
        @test all(
            isapprox.(
                [_farea1, _farea2],
                compute_faces_areas_list(m),
                rtol = 1e-4,
                atol = 1e-4,
            ),
        )
        @test all(
            isapprox.(
                _farea1,
                compute_faces_areas_padded(m)[:, 1:size(_farea1, 2), 1],
                rtol = 1e-4,
                atol = 1e-4,
            ),
        )
        @test all(
            isapprox.(
                _farea2,
                compute_faces_areas_padded(m)[:, 1:size(_farea2, 2), 2],
                rtol = 1e-4,
                atol = 1e-4,
            ),
        )
        @test all(compute_faces_areas_padded(m)[:, size(_farea1, 2)+1:end, 1] .== 0)
        @test all(compute_faces_areas_padded(m)[:, size(_farea2, 2)+1:end, 2] .== 0)
    end
end

@info "Testing Rep Utils..."
@testset "Rep Utils" begin
    for T in [Float64, Float32, Int64, UInt32]
        a1 = [
            1 2 3
            4 5 6
            7 8 9
            10 11 12
        ]

        a2 = [
            13 14 15
            16 17 18
        ]

        a3 = [
            19 20 21
            22 23 24
            25 26 27
        ]

        a1 = T.(a1')
        a2 = T.(a2')
        a3 = T.(a3')
        _list = [a1, a2, a3]

        _packed = [
            1 2 3
            4 5 6
            7 8 9
            10 11 12
            13 14 15
            16 17 18
            19 20 21
            22 23 24
            25 26 27
        ]
        _packed = T.(_packed')

        _padded = zeros(3, 4, 3)
        _padded[:, 1:size(a1, 2), 1] = a1
        _padded[:, 1:size(a2, 2), 2] = a2
        _padded[:, 1:size(a3, 2), 3] = a3
        _padded = T.(_padded)

        items_len, packed_first_idx, packed_to_list_idx = Flux3D._auxiliary_mesh(_list)

        @test items_len == [4, 2, 3]
        @test packed_first_idx == [1, 5, 7]
        @test all(packed_to_list_idx[1:4] .== 1)
        @test all(packed_to_list_idx[5:6] .== 2)
        @test all(packed_to_list_idx[7:9] .== 3)

        @test Flux3D._list_to_padded(_list, 0) == _padded
        @test Flux3D._list_to_padded(_list, 0) isa Array{T,3}
        @test gradient(x -> 0.5 .* sum(Flux3D._list_to_padded(x, 0) .^ 2), _list)[1] ==
              _list

        @test Flux3D._list_to_packed(_list) == _packed
        @test Flux3D._list_to_packed(_list) isa Array{T,2}
        @test gradient(x -> 0.5 .* sum(Flux3D._list_to_packed(x) .^ 2), _list)[1] ==
              Tuple(_list)

        @test Flux3D._packed_to_padded(_packed, items_len, 0) == _padded
        @test Flux3D._packed_to_padded(_packed, items_len, 0) isa Array{T,3}
        @test gradient(
            x -> 0.5 .* sum(Flux3D._packed_to_padded(x, items_len, 0) .^ 2),
            _packed,
        )[1] == _packed

        @test Flux3D._packed_to_list(_packed, items_len) == _list
        @test Flux3D._packed_to_list(_packed, items_len) isa Array{Array{T,2},1}
        gs = gradient(_packed) do x
            loss = 0
            for x in Flux3D._packed_to_list(x, items_len)
                loss += sum(x .^ 2)
            end
            return 0.5 * loss
        end
        @test gs[1] == _packed

        @test Flux3D._padded_to_list(_padded, items_len) == _list
        @test Flux3D._padded_to_list(_padded, items_len) isa Array{Array{T,2},1}
        gs = gradient(_padded) do x
            loss = 0
            for x in Flux3D._padded_to_list(x, items_len)
                loss += sum(x .^ 2)
            end
            return 0.5 * loss
        end
        @test gs[1] == _padded

        @test Flux3D._padded_to_packed(_padded, items_len) == _packed
        @test Flux3D._padded_to_packed(_padded, items_len) isa Array{T,2}
        @test gradient(
            x -> sum(0.5 .* sum(Flux3D._padded_to_packed(x, items_len) .^ 2)),
            _padded,
        )[1] == _padded
    end
end


module Visualize_Assembly

using Modia3D

# Basic definitions
vmat1 = Modia3D.Material(color="LightBlue" , transparency=0.5)
vmat2 = Modia3D.Material(color="Red")

@assembly Bar(;Lx = 0.1, Ly=Lx/5, Lz=Ly) begin
   frame0 = Modia3D.Object3D(Modia3D.Solid(Modia3D.SolidBeam(Lx,Ly,Lz), nothing, vmat1))
   frame1 = Modia3D.Object3D(frame0; r=[-Lx/2, 0.0, 0.0])
   frame2 = Modia3D.Object3D(frame0; r=[ Lx/2, 0.0, 0.0])

   cyl    = Modia3D.Cylinder(Ly/2,1.2*Ly; material=vmat2)
   cyl1   = Modia3D.Object3D(frame1, cyl)
   cyl2   = Modia3D.Object3D(frame2, cyl)
end

Modia3D.visualizeAssembly!( Bar(Lx = 1.0, sceneOptions=Modia3D.SceneOptions(visualizeFrames=true,
                                                                            defaultFrameLength=0.3,
                                                                            enableContactDetection=false) ) )

println("... success of Visualize_Assembly.jl!")
end


module TestExamples3D

using Test
using Trixi

include("test_trixi.jl")

# pathof(Trixi) returns /path/to/Trixi/src/Trixi.jl, dirname gives the parent directory
EXAMPLES_DIR = joinpath(pathof(Trixi) |> dirname |> dirname, "examples", "3d")

# Start with a clean environment: remove Trixi output directory if it exists
outdir = "out"
isdir(outdir) && rm(outdir, recursive=true)

@testset "3D" begin

# Run basic tests
@testset "Examples 3D" begin
  # Linear scalar advection
  include("test_examples_3d_advection.jl")

  # Hyperbolic diffusion
  include("test_examples_3d_hypdiff.jl")

  # Compressible Euler
  include("test_examples_3d_euler.jl")

  # MHD
  include("test_examples_3d_mhd.jl")

  # Compressible Euler with self-gravity
  include("test_examples_3d_eulergravity.jl")

  # Lattice-Boltzmann
  include("test_examples_3d_lbm.jl")
end

@testset "Displaying components 3D" begin
  @test_nowarn include(joinpath(EXAMPLES_DIR, "elixir_advection_amr.jl"))

  # test both short and long printing formats
  @test_nowarn show(mesh); println()
  @test_nowarn println(mesh)
  @test_nowarn display(mesh)

  @test_nowarn show(equations); println()
  @test_nowarn println(equations)
  @test_nowarn display(equations)

  @test_nowarn show(solver); println()
  @test_nowarn println(solver)
  @test_nowarn display(solver)

  @test_nowarn show(solver.basis); println()
  @test_nowarn println(solver.basis)
  @test_nowarn display(solver.basis)

  @test_nowarn show(solver.mortar); println()
  @test_nowarn println(solver.mortar)
  @test_nowarn display(solver.mortar)

  @test_nowarn show(semi); println()
  @test_nowarn println(semi)
  @test_nowarn display(semi)

  @test_nowarn show(summary_callback); println()
  @test_nowarn println(summary_callback)
  @test_nowarn display(summary_callback)

  @test_nowarn show(amr_controller); println()
  @test_nowarn println(amr_controller)
  @test_nowarn display(amr_controller)

  @test_nowarn show(amr_callback); println()
  @test_nowarn println(amr_callback)
  @test_nowarn display(amr_callback)

  @test_nowarn show(stepsize_callback); println()
  @test_nowarn println(stepsize_callback)
  @test_nowarn display(stepsize_callback)

  @test_nowarn show(save_solution); println()
  @test_nowarn println(save_solution)
  @test_nowarn display(save_solution)

  @test_nowarn show(analysis_callback); println()
  @test_nowarn println(analysis_callback)
  @test_nowarn display(analysis_callback)

  @test_nowarn show(alive_callback); println()
  @test_nowarn println(alive_callback)
  @test_nowarn display(alive_callback)

  @test_nowarn println(callbacks)
end

@testset "Additional tests in 3D" begin
  @testset "compressible Euler" begin
    eqn = CompressibleEulerEquations3D(1.4)

    @test isapprox(energy_total([1.0, 2.0, 3.0, 4.0, 20.0], eqn), 20.0)
    @test isapprox(energy_kinetic([1.0, 2.0, 3.0, 4.0, 20], eqn), 14.5)
    @test isapprox(energy_internal([1.0, 2.0, 3.0, 4.0, 20], eqn), 5.5)
  end

  @testset "hyperbolic diffusion" begin
    @test_nowarn HyperbolicDiffusionEquations3D(nu=1.0)
    eqn = HyperbolicDiffusionEquations3D(nu=1.0)

    @test isapprox(initial_condition_sedov_self_gravity(collect(1:3), 4.5, eqn), zeros(4))
    @test isapprox(boundary_condition_sedov_self_gravity(collect(1:4), 1, 1, collect(11:13), 2.3, flux_central, eqn), [-1.0, -19.739208802178712, 0.0, 0.0])
    @test isapprox(boundary_condition_sedov_self_gravity(collect(1:4), 2, 2, collect(11:13), 4.5, flux_central, eqn), [-1.5, 0.0, -19.739208802178712, 0.0])
  end

  @testset "ideal GLM MHD" begin
    eqn = IdealGlmMhdEquations3D(1.4)
    u = [1.0, 2.0, 3.0, 4.0, 20.0, 0.1, 0.2, 0.3, 1.5]

    @test isapprox(density(u, eqn), 1.0)
    @test isapprox(pressure(u, eqn), 1.7219999999999995)
    @test isapprox(density_pressure(u, eqn), 1.7219999999999995)

    @test isapprox(Trixi.entropy_thermodynamic(u, eqn), 0.5434864060055388)
    @test isapprox(Trixi.entropy_math(u, eqn), -1.3587160150138473)
    @test isapprox(Trixi.entropy(u, eqn), -1.3587160150138473)

    @test isapprox(energy_total(u, eqn), 20.0)
    @test isapprox(energy_kinetic(u, eqn), 14.5)
    @test isapprox(energy_magnetic(u, eqn), 0.07)
    @test isapprox(energy_internal(u, eqn), 4.305)

    @test isapprox(cross_helicity(u, eqn), 2.0)
  end
end

# Clean up afterwards: delete Trixi output directory
@test_nowarn rm(outdir, recursive=true)

end # 3D

end #module