""" The discrete exterior calculus (DEC) for simplicial sets.

This module provides the dual complex associated with a delta set (the primal
complex), which is a discrete incarnation of Hodge duality, as well as the many
operators of the DEC that depend on it, such as the Hodge star, codifferential,
wedge product, interior product, and Lie derivative. The main reference for this
module is Hirani's 2003 PhD thesis.
"""
module DiscreteExteriorCalculus
export DualSimplex, DualV, DualE, DualTri, DualChain, DualForm,
  PrimalVectorField, DualVectorField,
  AbstractDeltaDualComplex1D, DeltaDualComplex1D,
  OrientedDeltaDualComplex1D, EmbeddedDeltaDualComplex1D,
  AbstractDeltaDualComplex2D, DeltaDualComplex2D,
  OrientedDeltaDualComplex2D, EmbeddedDeltaDualComplex2D,
  SimplexCenter, Barycenter, Circumcenter, Incenter, geometric_center,
  subsimplices, primal_vertex, elementary_duals, dual_boundary, dual_derivative,
  ⋆, hodge_star, inv_hodge_star, δ, codifferential, ∇², laplace_beltrami, Δ, laplace_de_rham,
  ♭, flat, ♯, sharp, ∧, wedge_product, interior_product, interior_product_flat,
  ℒ, lie_derivative, lie_derivative_flat,
  vertex_center, edge_center, triangle_center, dual_triangle_vertices,
  dual_point, dual_volume, subdivide_duals!, DiagonalHodge, GeometricHodge

import Base: ndims
using LinearAlgebra: Diagonal, dot, norm, cross
using SparseArrays
using StaticArrays: @SVector, SVector

using Catlab, Catlab.CategoricalAlgebra.CSets
using Catlab.CategoricalAlgebra.FinSets: deleteat
using Catlab.Graphs: HasGraph
import Catlab.Theories: Δ
using ..ArrayUtils, ..SimplicialSets
using ..SimplicialSets: DeltaCategory1D, DeltaCategory2D, CayleyMengerDet,
  operator_nz, ∂_nz, d_nz, cayley_menger, negate
import ..SimplicialSets: ∂, d, volume

abstract type DiscreteFlat end
struct DPPFlat <: DiscreteFlat end

abstract type DiscreteSharp end
struct PPSharp <: DiscreteSharp end

abstract type DiscreteHodge end
struct GeometricHodge <: DiscreteHodge end
struct DiagonalHodge  <: DiscreteHodge end

# 1D dual complex
#################

# Should be expressed using a coproduct of two copies of `DeltaCategory1D`.

@present SchemaDualComplex1D <: DeltaCategory1D begin
  # Dual vertices and edges.
  (DualV, DualE)::Ob
  (D_∂v0, D_∂v1)::Hom(DualE, DualV)

  # Centers of primal simplices are dual vertices.
  vertex_center::Hom(V, DualV)
  edge_center::Hom(E, DualV)

  # Every primal edge is subdivided into two dual edges.
  #
  # (∂v0_dual, ∂v1_dual)::Hom(E,DualE)
  #
  # ∂v0_dual ⋅ D_∂v1 == ∂v0 ⋅ vertex_center
  # ∂v1_dual ⋅ D_∂v1 == ∂v1 ⋅ vertex_center
  # ∂v0_dual ⋅ D_∂v0 == edge_center
  # ∂v1_dual ⋅ D_∂v0 == edge_center
  #
  # We could, and arguably should, track these through dedicated morphisms, as
  # in the commented code above. We don't because it scales badly in dimension:
  # an ``n``-simplex is subdivided into ``n!`` sub-simplices. So we would need 6
  # morphisms for triangles and 24 for tets, and an even larger number of
  # equations to say how everything fits together. Moreover, in practice, the
  # incidence data for the dual complex suffices to construct the dual cells of
  # primal simplices.
end

""" Abstract type for dual complex of a 1D delta set.
"""
@abstract_acset_type AbstractDeltaDualComplex1D <: HasDeltaSet1D

""" Dual complex of a one-dimensional delta set.

The data structure includes both the primal complex and the dual complex, as
well as the mapping between them.
"""
@acset_type DeltaDualComplex1D(SchemaDualComplex1D,
  index=[:∂v0,:∂v1,:D_∂v0,:D_∂v1]) <: AbstractDeltaDualComplex1D

""" Dual vertex corresponding to center of primal vertex.
"""
vertex_center(s::HasDeltaSet, args...) = s[args..., :vertex_center]

""" Dual vertex corresponding to center of primal edge.
"""
edge_center(s::HasDeltaSet1D, args...) = s[args..., :edge_center]

subsimplices(::Type{Val{1}}, s::HasDeltaSet1D, e::Int) =
  SVector{2}(incident(s, edge_center(s, e), :D_∂v0))

primal_vertex(::Type{Val{1}}, s::HasDeltaSet1D, e...) = s[e..., :D_∂v1]

elementary_duals(::Type{Val{0}}, s::AbstractDeltaDualComplex1D, v::Int) =
  incident(s, vertex_center(s,v), :D_∂v1)
elementary_duals(::Type{Val{1}}, s::AbstractDeltaDualComplex1D, e::Int) =
  SVector(edge_center(s,e))

""" Boundary dual vertices of a dual triangle.

This accessor assumes that the simplicial identities for the dual hold.
"""
function dual_triangle_vertices(s::HasDeltaSet1D, t...)
  SVector(s[s[t..., :D_∂e1], :D_∂v1],
          s[s[t..., :D_∂e0], :D_∂v1],
          s[s[t..., :D_∂e0], :D_∂v0])
end

# 1D oriented dual complex
#-------------------------

@present SchemaOrientedDualComplex1D <: SchemaDualComplex1D begin
  Orientation::AttrType
  edge_orientation::Attr(E, Orientation)
  D_edge_orientation::Attr(DualE, Orientation)
end

""" Oriented dual complex of an oriented 1D delta set.
"""
@acset_type OrientedDeltaDualComplex1D(SchemaOrientedDualComplex1D,
  index=[:∂v0,:∂v1,:D_∂v0,:D_∂v1]) <: AbstractDeltaDualComplex1D

dual_boundary_nz(::Type{Val{1}}, s::AbstractDeltaDualComplex1D, x::Int) =
  # Boundary vertices of dual 1-cell ↔
  # Dual vertices for cofaces of (edges incident to) primal vertex.
  d_nz(Val{0}, s, x)

dual_derivative_nz(::Type{Val{0}}, s::AbstractDeltaDualComplex1D, x::Int) =
  negatenz(∂_nz(Val{1}, s, x))

negatenz((I, V)) = (I, negate.(V))

""" Construct 1D dual complex from 1D delta set.
"""
function (::Type{S})(t::AbstractDeltaSet1D) where S <: AbstractDeltaDualComplex1D
  s = S()
  copy_parts!(s, t)
  make_dual_simplices_1d!(s)
  return s
end

""" Make dual vertice and edges for dual complex of dimension ≧ 1.

Although zero-dimensional duality is geometrically trivial (subdividing a vertex
gives back the same vertex), we treat the dual vertices as disjoint from the
primal vertices. Thus, a dual vertex is created for every primal vertex.

If the primal complex is oriented, an orientation is induced on the dual
complex. The dual edges are oriented relative to the primal edges they subdivide
(Hirani 2003, PhD thesis, Ch. 2, last sentence of Remark 2.5.1).
"""
function make_dual_simplices_1d!(s::HasDeltaSet1D)
  # Make dual vertices and edges.
  s[:vertex_center] = vcenters = add_parts!(s, :DualV, nv(s))
  s[:edge_center] = ecenters = add_parts!(s, :DualV, ne(s))
  D_edges = map((0,1)) do i
    add_parts!(s, :DualE, ne(s);
               D_∂v0 = ecenters, D_∂v1 = view(vcenters, ∂(1,i,s)))
  end

  # Orient elementary dual edges.
  if has_subpart(s, :edge_orientation)
    edge_orient = s[:edge_orientation]
    s[D_edges[1], :D_edge_orientation] = negate.(edge_orient)
    s[D_edges[2], :D_edge_orientation] = edge_orient
  end

  D_edges
end

# 1D embedded dual complex
#-------------------------

@present SchemaEmbeddedDualComplex1D <: SchemaOrientedDualComplex1D begin
  (Real, Point)::AttrType
  point::Attr(V, Point)
  length::Attr(E, Real)
  dual_point::Attr(DualV, Point)
  dual_length::Attr(DualE, Real)
end

""" Embedded dual complex of an embedded 1D delta set.

Although they are redundant information, the lengths of the primal and dual
edges are precomputed and stored.
"""
@acset_type EmbeddedDeltaDualComplex1D(SchemaEmbeddedDualComplex1D,
  index=[:∂v0,:∂v1,:D_∂v0,:D_∂v1]) <: AbstractDeltaDualComplex1D

""" Point associated with dual vertex of complex.
"""
dual_point(s::HasDeltaSet, args...) = s[args..., :dual_point]

struct PrecomputedVol end

volume(::Type{Val{n}}, s::EmbeddedDeltaDualComplex1D, x) where n =
  volume(Val{n}, s, x, PrecomputedVol())
dual_volume(::Type{Val{n}}, s::EmbeddedDeltaDualComplex1D, x) where n =
  dual_volume(Val{n}, s, x, PrecomputedVol())

volume(::Type{Val{1}}, s::HasDeltaSet1D, e, ::PrecomputedVol) = s[e, :length]
dual_volume(::Type{Val{1}}, s::HasDeltaSet1D, e, ::PrecomputedVol) =
  s[e, :dual_length]

dual_volume(::Type{Val{1}}, s::HasDeltaSet1D, e::Int, ::CayleyMengerDet) =
  volume(dual_point(s, SVector(s[e,:D_∂v0], s[e,:D_∂v1])))

hodge_diag(::Type{Val{0}}, s::AbstractDeltaDualComplex1D, v::Int) =
  sum(dual_volume(Val{1}, s, elementary_duals(Val{0},s,v)))
hodge_diag(::Type{Val{1}}, s::AbstractDeltaDualComplex1D, e::Int) =
  1 / volume(Val{1},s,e)

""" Compute geometric subdivision for embedded dual complex.

Supports different methods of subdivision through the choice of geometric
center, as defined by [`geometric_center`](@ref). In particular, barycentric
subdivision and circumcentric subdivision are supported.
"""
function subdivide_duals!(s::EmbeddedDeltaDualComplex1D, args...)
  subdivide_duals_1d!(s, args...)
  precompute_volumes_1d!(s)
end

function subdivide_duals_1d!(s::HasDeltaSet1D, alg)
  for v in vertices(s)
    s[vertex_center(s,v), :dual_point] = point(s, v)
  end
  for e in edges(s)
    s[edge_center(s,e), :dual_point] = geometric_center(
      point(s, edge_vertices(s, e)), alg)
  end
end

function precompute_volumes_1d!(s::HasDeltaSet1D)
  for e in edges(s)
    s[e, :length] = volume(1,s,e,CayleyMengerDet())
  end
  for e in parts(s, :DualE)
    s[e, :dual_length] = dual_volume(1,s,e,CayleyMengerDet())
  end
end

# 2D dual complex
#################

# Should be expressed using a coproduct of two copies of `DeltaCategory2D` or
# perhaps a pushout of `SchemaDualComplex2D` and `DeltaCategory1D`.

@present SchemaDualComplex2D <: DeltaCategory2D begin
  # Dual vertices, edges, and triangles.
  (DualV, DualE, DualTri)::Ob
  (D_∂v0, D_∂v1)::Hom(DualE, DualV)
  (D_∂e0, D_∂e1, D_∂e2)::Hom(DualTri, DualE)

  # Simplicial identities for dual simplices.
  D_∂e1 ⋅ D_∂v1 == D_∂e2 ⋅ D_∂v1
  D_∂e0 ⋅ D_∂v1 == D_∂e2 ⋅ D_∂v0
  D_∂e0 ⋅ D_∂v0 == D_∂e1 ⋅ D_∂v0

  # Centers of primal simplices are dual vertices.
  vertex_center::Hom(V, DualV)
  edge_center::Hom(E, DualV)
  tri_center::Hom(Tri, DualV)
end

""" Abstract type for dual complex of a 2D delta set.
"""
@abstract_acset_type AbstractDeltaDualComplex2D <: HasDeltaSet2D

""" Dual complex of a two-dimensional delta set.
"""
@acset_type DeltaDualComplex2D(SchemaDualComplex2D,
  index=[:∂v0,:∂v1,:∂e0,:∂e1,:∂e2,:D_∂v0,:D_∂v1,:D_∂e0,:D_∂e1,:D_∂e2]) <: AbstractDeltaDualComplex2D

""" Dual vertex corresponding to center of primal triangle.
"""
triangle_center(s::HasDeltaSet2D, args...) = s[args..., :tri_center]

subsimplices(::Type{Val{2}}, s::HasDeltaSet2D, t::Int) =
  SVector{6}(incident(s, triangle_center(s,t), @SVector [:D_∂e1, :D_∂v0]))

primal_vertex(::Type{Val{2}}, s::HasDeltaSet2D, t...) =
  primal_vertex(Val{1}, s, s[t..., :D_∂e2])

elementary_duals(::Type{Val{0}}, s::AbstractDeltaDualComplex2D, v::Int) =
  incident(s, vertex_center(s,v), @SVector [:D_∂e1, :D_∂v1])
elementary_duals(::Type{Val{1}}, s::AbstractDeltaDualComplex2D, e::Int) =
  incident(s, edge_center(s,e), :D_∂v1)
elementary_duals(::Type{Val{2}}, s::AbstractDeltaDualComplex2D, t::Int) =
  SVector(triangle_center(s,t))

# 2D oriented dual complex
#-------------------------

@present SchemaOrientedDualComplex2D <: SchemaDualComplex2D begin
  Orientation::AttrType
  edge_orientation::Attr(E, Orientation)
  tri_orientation::Attr(Tri, Orientation)
  D_edge_orientation::Attr(DualE, Orientation)
  D_tri_orientation::Attr(DualTri, Orientation)
end

""" Oriented dual complex of an oriented 2D delta set.
"""
@acset_type OrientedDeltaDualComplex2D(SchemaOrientedDualComplex2D,
  index=[:∂v0,:∂v1,:∂e0,:∂e1,:∂e2,:D_∂v0,:D_∂v1,:D_∂e0,:D_∂e1,:D_∂e2]) <: AbstractDeltaDualComplex2D

dual_boundary_nz(::Type{Val{1}}, s::AbstractDeltaDualComplex2D, x::Int) =
  # Boundary vertices of dual 1-cell ↔
  # Dual vertices for cofaces of (triangles incident to) primal edge.
  negatenz(d_nz(Val{1}, s, x))
dual_boundary_nz(::Type{Val{2}}, s::AbstractDeltaDualComplex2D, x::Int) =
  # Boundary edges of dual 2-cell ↔
  # Dual edges for cofaces of (edges incident to) primal vertex.
  d_nz(Val{0}, s, x)

dual_derivative_nz(::Type{Val{0}}, s::AbstractDeltaDualComplex2D, x::Int) =
  ∂_nz(Val{2}, s, x)
dual_derivative_nz(::Type{Val{1}}, s::AbstractDeltaDualComplex2D, x::Int) =
  negatenz(∂_nz(Val{1}, s, x))

""" Construct 2D dual complex from 2D delta set.
"""
function (::Type{S})(t::AbstractDeltaSet2D) where S <: AbstractDeltaDualComplex2D
  s = S()
  copy_parts!(s, t)
  make_dual_simplices_2d!(s)
  return s
end

""" Make dual simplices for dual complex of dimension ≧ 2.

If the primal complex is oriented, an orientation is induced on the dual
complex. The elementary dual edges are oriented following (Hirani, 2003, Example
2.5.2) or (Desbrun et al, 2005, Table 1) and the dual triangles are oriented
relative to the primal triangles they subdivide.
"""
function make_dual_simplices_2d!(s::HasDeltaSet2D)
  # Make dual vertices and edges.
  D_edges01 = make_dual_simplices_1d!(s)
  s[:tri_center] = tri_centers = add_parts!(s, :DualV, ntriangles(s))
  D_edges12 = map((0,1,2)) do e
    add_parts!(s, :DualE, ntriangles(s);
               D_∂v0=tri_centers, D_∂v1=edge_center(s, ∂(2,e,s)))
  end
  D_edges02 = map(triangle_vertices(s)) do vs
    add_parts!(s, :DualE, ntriangles(s);
               D_∂v0=tri_centers, D_∂v1=vertex_center(s, vs))
  end

  # Make dual triangles.
  # Counterclockwise order in drawing with vertices 0, 1, 2 from left to right.
  D_triangle_schemas = ((0,1,1),(0,2,1),(1,2,0),(1,0,1),(2,0,0),(2,1,0))
  D_triangles = map(D_triangle_schemas) do (v,e,ev)
    add_parts!(s, :DualTri, ntriangles(s);
               D_∂e0=D_edges12[e+1], D_∂e1=D_edges02[v+1],
               D_∂e2=view(D_edges01[ev+1], ∂(2,e,s)))
  end

  if has_subpart(s, :tri_orientation)
    # Orient elementary dual triangles.
    tri_orient = s[:tri_orientation]
    rev_tri_orient = negate.(tri_orient)
    for (i, D_tris) in enumerate(D_triangles)
      s[D_tris, :D_tri_orientation] = isodd(i) ? rev_tri_orient : tri_orient
    end

    # Orient elementary dual edges.
    for e in (0,1,2)
      s[D_edges12[e+1], :D_edge_orientation] = relative_sign.(
        s[∂(2,e,s), :edge_orientation],
        isodd(e) ? rev_tri_orient : tri_orient)
    end
    # Remaining dual edges are oriented arbitrarily.
    s[lazy(vcat, D_edges02...), :D_edge_orientation] = one(eltype(tri_orient))
  end

  D_triangles
end

relative_sign(x, y) = sign(x*y)
relative_sign(x::Bool, y::Bool) = (x && y) || (!x && !y)

# 2D embedded dual complex
#-------------------------

@present SchemaEmbeddedDualComplex2D <: SchemaOrientedDualComplex2D begin
  (Real, Point)::AttrType
  point::Attr(V, Point)
  length::Attr(E, Real)
  area::Attr(Tri, Real)
  dual_point::Attr(DualV, Point)
  dual_length::Attr(DualE, Real)
  dual_area::Attr(DualTri, Real)
end

""" Embedded dual complex of an embedded 12 delta set.

Although they are redundant information, the lengths and areas of the
primal/dual edges and triangles are precomputed and stored.
"""
@acset_type EmbeddedDeltaDualComplex2D(SchemaEmbeddedDualComplex2D,
  index=[:∂v0,:∂v1,:∂e0,:∂e1,:∂e2,:D_∂v0,:D_∂v1,:D_∂e0,:D_∂e1,:D_∂e2]) <: AbstractDeltaDualComplex2D

volume(::Type{Val{n}}, s::EmbeddedDeltaDualComplex2D, x) where n =
  volume(Val{n}, s, x, PrecomputedVol())
dual_volume(::Type{Val{n}}, s::EmbeddedDeltaDualComplex2D, x) where n =
  dual_volume(Val{n}, s, x, PrecomputedVol())

volume(::Type{Val{2}}, s::HasDeltaSet2D, t, ::PrecomputedVol) = s[t, :area]
dual_volume(::Type{Val{2}}, s::HasDeltaSet2D, t, ::PrecomputedVol) =
  s[t, :dual_area]

function dual_volume(::Type{Val{2}}, s::HasDeltaSet2D, t::Int, ::CayleyMengerDet)
  dual_vs = SVector(s[s[t, :D_∂e1], :D_∂v1],
                    s[s[t, :D_∂e2], :D_∂v0],
                    s[s[t, :D_∂e0], :D_∂v0])
  volume(dual_point(s, dual_vs))
end

hodge_diag(::Type{Val{0}}, s::AbstractDeltaDualComplex2D, v::Int) =
  sum(dual_volume(Val{2}, s, elementary_duals(Val{0},s,v)))
hodge_diag(::Type{Val{1}}, s::AbstractDeltaDualComplex2D, e::Int) =
  sum(dual_volume(Val{1}, s, elementary_duals(Val{1},s,e))) / volume(Val{1},s,e)
hodge_diag(::Type{Val{2}}, s::AbstractDeltaDualComplex2D, t::Int) =
  1 / volume(Val{2},s,t)

function ♭(s::AbstractDeltaDualComplex2D, X::AbstractVector, ::DPPFlat)
  # XXX: Creating this lookup table shouldn't be necessary. Of course, we could
  # index `tri_center` but that shouldn't be necessary either. Rather, we should
  # loop over incident triangles instead of the elementary duals, which just
  # happens to be inconvenient.
  tri_map = Dict{Int,Int}(triangle_center(s,t) => t for t in triangles(s))

  map(edges(s)) do e
    e_vec = (point(s, tgt(s,e)) - point(s, src(s,e))) * sign(1,s,e)
    dual_edges = elementary_duals(1,s,e)
    dual_lengths = dual_volume(1, s, dual_edges)
    mapreduce(+, dual_edges, dual_lengths) do dual_e, dual_length
      X_vec = X[tri_map[s[dual_e, :D_∂v0]]]
      dual_length * dot(X_vec, e_vec)
    end / sum(dual_lengths)
  end
end

function ♯(s::AbstractDeltaDualComplex2D, α::AbstractVector, ::PPSharp)
  α♯ = zeros(eltype(s[:dual_point]), nv(s))
  for t in triangles(s)
    area = volume(2,s,t)
    tri_center, tri_edges = triangle_center(s,t), triangle_edges(s,t)
    tri_point = dual_point(s, tri_center)
    for (i, (v₀, e₀)) in enumerate(zip(triangle_vertices(s,t), tri_edges))
      e_vec = point(s, tgt(s, e₀)) - point(s, src(s, e₀))
      e_vec /= norm(e_vec)
      e2_vec = point(s, v₀) - point(s, src(s, e₀))
      out_vec = e2_vec - dot(e2_vec, e_vec)*e_vec
      h = norm(out_vec)
      out_vec /= h^2 # length == 1/h
      for e in deleteat(tri_edges, i)
        v, sgn = src(s,e) == v₀ ? (tgt(s,e), -1) : (src(s,e), +1)
        dual_area = sum(dual_volume(2,s,d) for d in elementary_duals(0,s,v)
                        if s[s[d, :D_∂e0], :D_∂v0] == tri_center)
        α♯[v] += sgn * sign(1,s,e) * α[e] * (dual_area / area) * out_vec
      end
    end
  end
  α♯
end

function ∧(::Type{Tuple{1,1}}, s::HasDeltaSet2D, α, β, x::Int)
  # XXX: This calculation of the volume coefficients is awkward due to the
  # design decision described in `SchemaDualComplex1D`.
  dual_vs = vertex_center(s, triangle_vertices(s, x))
  dual_es = sort(SVector{6}(incident(s, triangle_center(s, x), :D_∂v0)),
                 by=e -> s[e,:D_∂v1] .== dual_vs, rev=true)[1:3]
  coeffs = map(dual_es) do e
    sum(dual_volume(2, s, SVector{2}(incident(s, e, :D_∂e1))))
  end / volume(2, s, x)

  # Wedge product of two primal 1-forms, as in (Hirani 2003, Example 7.1.2).
  # This formula is not the same as (Hirani 2003, Equation 7.1.2) but it is
  # equivalent.
  e0, e1, e2 = ∂(2,0,s,x), ∂(2,1,s,x), ∂(2,2,s,x)
  dot(coeffs, SVector(α[e2] * β[e1] - α[e1] * β[e2],
                      α[e2] * β[e0] - α[e0] * β[e2],
                      α[e1] * β[e0] - α[e0] * β[e1])) / 2
end

function subdivide_duals!(s::EmbeddedDeltaDualComplex2D, args...)
  subdivide_duals_2d!(s, args...)
  precompute_volumes_2d!(s)
end

function subdivide_duals_2d!(s::HasDeltaSet2D, alg)
  subdivide_duals_1d!(s, alg)
  for t in triangles(s)
    s[triangle_center(s,t), :dual_point] = geometric_center(
      point(s, triangle_vertices(s, t)), alg)
  end
end

function precompute_volumes_2d!(s::HasDeltaSet2D)
  precompute_volumes_1d!(s)
  for t in triangles(s)
    s[t, :area] = volume(2,s,t,CayleyMengerDet())
  end
  for t in parts(s, :DualTri)
    s[t, :dual_area] = dual_volume(2,s,t,CayleyMengerDet())
  end
end

# General operators
###################

""" Wrapper for dual simplex or simplices of dimension `D`.

See also: [`DualV`](@ref), [`DualE`](@ref), [`DualTri`](@ref).
"""
@parts_array_struct DualSimplex{D}

""" Vertex in simplicial set: alias for `Simplex{0}`.
"""
const DualV = DualSimplex{0}

""" Edge in simplicial set: alias for `Simplex{1}`.
"""
const DualE = DualSimplex{1}

""" Triangle in simplicial set: alias for `Simplex{2}`.
"""
const DualTri = DualSimplex{2}

""" Wrapper for chain of dual cells of dimension `n`.

In an ``N``-dimensional complex, the elementary dual simplices of each
``n``-simplex together comprise the dual ``(N-n)``-cell of the simplex. Using
this correspondence, a basis for primal ``n``-chains defines the basis for dual
``(N-n)``-chains.

!!! note

    In (Hirani 2003, Definition 3.4.1), the duality operator assigns a certain
    sign to each elementary dual simplex. For us, all of these signs should be
    regarded as positive because we have already incorporated them into the
    orientation of the dual simplices.
"""
@vector_struct DualChain{n}

""" Wrapper for form, aka cochain, on dual cells of dimension `n`.
"""
@vector_struct DualForm{n}

""" Wrapper for vector field on primal vertices.
"""
@vector_struct PrimalVectorField

""" Wrapper for vector field on dual vertices.
"""
@vector_struct DualVectorField

ndims(s::AbstractDeltaDualComplex1D) = 1
ndims(s::AbstractDeltaDualComplex2D) = 2

volume(s::HasDeltaSet, x::DualSimplex{n}, args...) where n =
  dual_volume(Val{n}, s, x.data, args...)
@inline dual_volume(n::Int, s::HasDeltaSet, args...) =
  dual_volume(Val{n}, s, args...)

""" List of dual simplices comprising the subdivision of a primal simplex.

A primal ``n``-simplex is always subdivided into ``n!`` dual ``n``-simplices,
not be confused with the [`elementary_duals`](@ref) which have complementary
dimension.

The returned list is ordered such that subsimplices with the same primal vertex
appear consecutively.
"""
subsimplices(s::HasDeltaSet, x::Simplex{n}) where n =
  DualSimplex{n}(subsimplices(Val{n}, s, x.data))
@inline subsimplices(n::Int, s::HasDeltaSet, args...) =
  subsimplices(Val{n}, s, args...)

""" Primal vertex associated with a dual simplex.
"""
primal_vertex(s::HasDeltaSet, x::DualSimplex{n}) where n =
  V(primal_vertex(Val{n}, s, x.data))
@inline primal_vertex(n::Int, s::HasDeltaSet, args...) =
  primal_vertex(Val{n}, s, args...)

""" List of elementary dual simplices corresponding to primal simplex.

In general, in an ``n``-dimensional complex, the elementary duals of primal
``k``-simplices are dual ``(n-k)``-simplices. Thus, in 1D dual complexes, the
elementary duals of...

- primal vertices are dual edges
- primal edges are (single) dual vertices

In 2D dual complexes, the elementary duals of...

- primal vertices are dual triangles
- primal edges are dual edges
- primal triangles are (single) dual triangles
"""
elementary_duals(s::HasDeltaSet, x::Simplex{n}) where n =
  DualSimplex{ndims(s)-n}(elementary_duals(Val{n}, s, x.data))
@inline elementary_duals(n::Int, s::HasDeltaSet, args...) =
  elementary_duals(Val{n}, s, args...)

""" Boundary of chain of dual cells.

Transpose of [`dual_derivative`](@ref).
"""
@inline dual_boundary(n::Int, s::HasDeltaSet, args...) =
  dual_boundary(Val{n}, s, args...)
∂(s::HasDeltaSet, x::DualChain{n}) where n =
  DualChain{n-1}(dual_boundary(Val{n}, s, x.data))

function dual_boundary(::Type{Val{n}}, s::HasDeltaSet, args...) where n
  operator_nz(Int, nsimplices(ndims(s)-n+1,s),
              nsimplices(ndims(s)-n,s), args...) do x
    dual_boundary_nz(Val{n}, s, x)
  end
end

""" Discrete exterior derivative of dual form.

Transpose of [`dual_boundary`](@ref). For more info, see (Desbrun, Kanso, Tong,
2008: Discrete differential forms for computational modeling, §4.5).
"""
@inline dual_derivative(n::Int, s::HasDeltaSet, args...) =
  dual_derivative(Val{n}, s, args...)
d(s::HasDeltaSet, x::DualForm{n}) where n =
  DualForm{n+1}(dual_derivative(Val{n}, s, x.data))

function dual_derivative(::Type{Val{n}}, s::HasDeltaSet, args...) where n
  operator_nz(Int, nsimplices(ndims(s)-n-1,s),
              nsimplices(ndims(s)-n,s), args...) do x
    dual_derivative_nz(Val{n}, s, x)
  end
end

""" Hodge star operator from primal ``n``-forms to dual ``N-n``-forms.

!!! note

    Some authors, such as (Hirani 2003) and (Desbrun 2005), use the symbol ``⋆``
    for the duality operator on chains and the symbol ``*`` for the Hodge star
    operator on cochains. We do not explicitly define the duality operator and
    we use the symbol ``⋆`` for the Hodge star.
"""
⋆(s::HasDeltaSet, x::SimplexForm{n}; kw...) where n =
  DualForm{ndims(s)-n}(⋆(Val{n}, s, x.data; kw...))
@inline ⋆(n::Int, s::HasDeltaSet, args...; kw...) = ⋆(Val{n}, s, args...; kw...)
@inline ⋆(::Type{Val{n}}, s::HasDeltaSet;
          hodge::DiscreteHodge=GeometricHodge()) where n = ⋆(Val{n}, s, hodge)
@inline ⋆(::Type{Val{n}}, s::HasDeltaSet, form::AbstractVector;
          hodge::DiscreteHodge=GeometricHodge()) where n = ⋆(Val{n}, s, form, hodge)

⋆(::Type{Val{n}}, s::HasDeltaSet, form::AbstractVector, ::DiagonalHodge) where n =
  applydiag(form) do x, a; a * hodge_diag(Val{n},s,x) end
⋆(::Type{Val{n}}, s::HasDeltaSet, ::DiagonalHodge) where n =
  Diagonal([ hodge_diag(Val{n},s,x) for x in simplices(n,s) ])

# Note that this cross product defines the positive direction for flux to
# always be in the positive z direction. This will likely not generalize to
# arbitrary meshes embedded in 3D space, and so will need to be revisited.
# Potentially this orientation can be provided by the simplicial triangle
# orientation?
crossdot(v1, v2) = begin
  v1v2 = cross(v1, v2)
  norm(v1v2) * (last(v1v2) == 0 ? 1.0 : sign(last(v1v2)))
end

""" Hodge star operator from primal 1-forms to dual 1-forms.

This specific hodge star implementation is based on the hodge star presented in
(Ayoub et al 2020), which generalizes the operator presented in (Hirani 2003).
This reproduces the diagonal hodge for a dual mesh generated under
circumcentric subdivision and provides off-diagonal correction factors for
meshes generated under other subdivision schemes (e.g. barycentric).
"""
function ⋆(::Type{Val{1}}, s::AbstractDeltaDualComplex2D, ::GeometricHodge)

  vals = Dict{Tuple{Int64, Int64}, Float64}()
  I = Vector{Int64}()
  J = Vector{Int64}()
  V = Vector{Float64}()

  rel_orient = 0.0
  for t in triangles(s)
    e = reverse(triangle_edges(s, t))
    ev = point(s, tgt(s, e)) .- point(s, src(s,e))

    tc = dual_point(s, triangle_center(s, t))
    dv = map(enumerate(dual_point(s, edge_center(s, e)))) do (i,v)
      (tc - v) * (i == 2 ? -1 : 1)
    end

    diag_dot = map(1:3) do i
      dot(ev[i], dv[i]) / dot(ev[i], ev[i])
    end

    # This relative orientation needs to be redefined for each triangle in the
    # case that the mesh has multiple independent connected components
    rel_orient = 0.0
    for i in 1:3
      diag_cross = sign(Val{2}, s, t) * crossdot(ev[i], dv[i]) /
                      dot(ev[i], ev[i])
      if diag_cross != 0.0
        # Decide the orientation of the mesh relative to z-axis (see crossdot)
        # For optimization, this could be moved out of this loop
        if rel_orient == 0.0
          rel_orient = sign(diag_cross)
        end

        push!(I, e[i])
        push!(J, e[i])
        push!(V, diag_cross * rel_orient)
      end
    end

    for p ∈ ((1,2,3), (1,3,2), (2,1,3),
             (2,3,1), (3,1,2), (3,2,1))
      val = rel_orient * sign(Val{2}, s, t) * diag_dot[p[1]] *
              dot(ev[p[1]], ev[p[3]]) / crossdot(ev[p[2]], ev[p[3]])
      if val != 0.0
        push!(I, e[p[1]])
        push!(J, e[p[2]])
        push!(V, val)
      end
    end
  end
  sparse(I,J,V)
end

⋆(::Type{Val{0}}, s::AbstractDeltaDualComplex2D, ::GeometricHodge) =
  ⋆(Val{0}, s, DiagonalHodge())
⋆(::Type{Val{2}}, s::AbstractDeltaDualComplex2D, ::GeometricHodge) =
  ⋆(Val{2}, s, DiagonalHodge())

⋆(::Type{Val{0}}, s::AbstractDeltaDualComplex2D, form::AbstractVector, ::GeometricHodge) =
  ⋆(Val{0}, s, form, DiagonalHodge())
⋆(::Type{Val{1}}, s::AbstractDeltaDualComplex2D, form::AbstractVector, ::GeometricHodge) =
  ⋆(Val{1}, s, GeometricHodge()) * form
⋆(::Type{Val{2}}, s::AbstractDeltaDualComplex2D, form::AbstractVector, ::GeometricHodge) =
  ⋆(Val{2}, s, form, DiagonalHodge())

⋆(::Type{Val{n}}, s::AbstractDeltaDualComplex1D, ::GeometricHodge) where n =
  ⋆(Val{n}, s, DiagonalHodge())
⋆(::Type{Val{n}}, s::AbstractDeltaDualComplex1D, form::AbstractVector, ::GeometricHodge) where n =
  ⋆(Val{n}, s, form, DiagonalHodge())

""" Alias for the Hodge star operator [`⋆`](@ref).
"""
const hodge_star = ⋆

""" Inverse Hodge star operator from dual ``N-n``-forms to primal ``n``-forms.

Confusingly, this is *not* the operator inverse of the Hodge star [`⋆`](@ref)
because it carries an extra global sign, in analogy to the smooth case
(Gillette, 2009, Notes on the DEC, Definition 2.27).
"""
@inline inv_hodge_star(n::Int, s::HasDeltaSet, args...; kw...) =
  inv_hodge_star(Val{n}, s, args...; kw...)
@inline inv_hodge_star(::Type{Val{n}}, s::HasDeltaSet;
                       hodge::DiscreteHodge=GeometricHodge()) where n =
  inv_hodge_star(Val{n}, s, hodge)
@inline inv_hodge_star(::Type{Val{n}}, s::HasDeltaSet, form::AbstractVector;
                       hodge::DiscreteHodge=GeometricHodge()) where n =
  inv_hodge_star(Val{n}, s, form, hodge)

function inv_hodge_star(::Type{Val{n}}, s::HasDeltaSet,
                        form::AbstractVector, ::DiagonalHodge) where n
  if iseven(n*(ndims(s)-n))
    applydiag(form) do x, a; a / hodge_diag(Val{n},s,x) end
  else
    applydiag(form) do x, a; -a / hodge_diag(Val{n},s,x) end
  end
end

function inv_hodge_star(::Type{Val{n}}, s::HasDeltaSet, ::DiagonalHodge) where n
  if iseven(n*(ndims(s)-n))
    Diagonal([ 1 / hodge_diag(Val{n},s,x) for x in simplices(n,s) ])
  else
    Diagonal([ -1 / hodge_diag(Val{n},s,x) for x in simplices(n,s) ])
  end
end

function inv_hodge_star(::Type{Val{1}}, s::AbstractDeltaDualComplex2D,
                        ::GeometricHodge)
  -1 * inv(Matrix(⋆(Val{1}, s, GeometricHodge())))
end
function inv_hodge_star(::Type{Val{1}}, s::AbstractDeltaDualComplex2D,
                        form::AbstractVector, ::GeometricHodge)
  -1 * (Matrix(⋆(Val{1}, s, GeometricHodge())) \ form)
end

inv_hodge_star(::Type{Val{0}}, s::AbstractDeltaDualComplex2D, ::GeometricHodge) =
  inv_hodge_star(Val{0}, s, DiagonalHodge())
inv_hodge_star(::Type{Val{2}}, s::AbstractDeltaDualComplex2D, ::GeometricHodge) =
  inv_hodge_star(Val{2}, s, DiagonalHodge())

inv_hodge_star(::Type{Val{0}}, s::AbstractDeltaDualComplex2D,
               form::AbstractVector, ::GeometricHodge) =
  inv_hodge_star(Val{0}, s, form, DiagonalHodge())
inv_hodge_star(::Type{Val{2}}, s::AbstractDeltaDualComplex2D,
               form::AbstractVector, ::GeometricHodge) =
  inv_hodge_star(Val{2}, s, form, DiagonalHodge())

inv_hodge_star(::Type{Val{n}}, s::AbstractDeltaDualComplex1D,
               ::GeometricHodge) where n =
  inv_hodge_star(Val{n}, s, DiagonalHodge())
inv_hodge_star(::Type{Val{n}}, s::AbstractDeltaDualComplex1D,
               form::AbstractVector, ::GeometricHodge) where n =
  inv_hodge_star(Val{n}, s, form, DiagonalHodge())

""" Codifferential operator from primal ``n`` forms to primal ``n-1``-forms.
"""
δ(s::HasDeltaSet, x::SimplexForm{n}; kw...) where n =
  SimplexForm{n-1}(δ(Val{n}, s, GeometricHodge(), x.data; kw...))
@inline δ(n::Int, s::HasDeltaSet, args...; kw...) =
  δ(Val{n}, s, args...; kw...)
@inline δ(::Type{Val{n}}, s::HasDeltaSet; hodge::DiscreteHodge=GeometricHodge(),
          matrix_type::Type=SparseMatrixCSC{Float64}) where n =
  δ(Val{n}, s, hodge, matrix_type)
@inline δ(::Type{Val{n}}, s::HasDeltaSet, form::AbstractVector;
          hodge::DiscreteHodge=GeometricHodge()) where n =
  δ(Val{n}, s, hodge, form)

function δ(::Type{Val{n}}, s::HasDeltaSet, ::DiagonalHodge, args...) where n
  # The sign of δ in Gillette's notes (see test file) is simply a product of
  # the signs for the inverse hodge and dual derivative involved.
  sgn = iseven((n-1)*(ndims(s)*(n-1) + 1)) ? +1 : -1
  operator_nz(Float64, nsimplices(n-1,s), nsimplices(n,s), args...) do x
    c = hodge_diag(Val{n}, s, x)
    I, V = dual_derivative_nz(Val{ndims(s)-n}, s, x)
    V = map(I, V) do i, a
      sgn * c * a / hodge_diag(Val{n-1}, s, i)
    end
    (I, V)
  end
end

function δ(::Type{Val{n}}, s::HasDeltaSet, ::GeometricHodge, matrix_type) where n
  inv_hodge_star(n-1, s) * dual_derivative(ndims(s)-n, s) * ⋆(n, s)
end

function δ(::Type{Val{n}}, s::HasDeltaSet, ::GeometricHodge, form::AbstractVector) where n
  Vector(inv_hodge_star(n - 1, s, dual_derivative(ndims(s)-n, s, ⋆(n, s, form))))
end

""" Alias for the codifferential operator [`δ`](@ref).
"""
const codifferential = δ

""" Laplace-Beltrami operator on discrete forms.

This linear operator on primal ``n``-forms defined by ``∇² α := -δ d α``, where
[`δ`](@ref) is the codifferential and [`d`](@ref) is the exterior derivative.

!!! note

    For following texts such as Abraham-Marsden-Ratiu, we take the sign
    convention that makes the Laplace-Beltrami operator consistent with the
    Euclidean Laplace operator (the divergence of the gradient). Other authors,
    such as (Hirani 2003), take the opposite convention, which has the advantage
    of being consistent with the Laplace-de Rham operator [`Δ`](@ref).
"""
∇²(s::HasDeltaSet, x::SimplexForm{n}; kw...) where n =
  SimplexForm{n}(∇²(Val{n}, s, x.data; kw...))
@inline ∇²(n::Int, s::HasDeltaSet, args...; kw...) = ∇²(Val{n}, s, args...; kw...)

∇²(::Type{Val{n}}, s::HasDeltaSet, form::AbstractVector; kw...) where n =
  -δ(n+1, s, d(Val{n}, s, form); kw...)
∇²(::Type{Val{n}}, s::HasDeltaSet; matrix_type::Type=SparseMatrixCSC{Float64}, kw...) where n =
  -δ(n+1, s; matrix_type=matrix_type, kw...) * d(Val{n}, s, matrix_type)

""" Alias for the Laplace-Beltrami operator [`∇²`](@ref).
"""
const laplace_beltrami = ∇²

""" Laplace-de Rham operator on discrete forms.

This linear operator on primal ``n``-forms is defined by ``Δ := δ d + d δ``.
Restricted to 0-forms, it reduces to the negative of the Laplace-Beltrami
operator [`∇²`](@ref): ``Δ f = -∇² f``.
"""
Δ(s::HasDeltaSet, x::SimplexForm{n}; kw...) where n =
  SimplexForm{n}(Δ(Val{n}, s, x.data; kw...))
@inline Δ(n::Int, s::HasDeltaSet, args...; kw...) = Δ(Val{n}, s, args...; kw...)

Δ(::Type{Val{0}}, s::HasDeltaSet, form::AbstractVector; kw...) =
  δ(1, s, d(Val{0}, s, form); kw...)
Δ(::Type{Val{0}}, s::HasDeltaSet; matrix_type::Type=SparseMatrixCSC{Float64}, kw...) =
  δ(1,s; matrix_type=matrix_type, kw...) * d(Val{0},s,matrix_type)

Δ(::Type{Val{n}}, s::HasDeltaSet, form::AbstractVector; kw...) where n =
  δ(n+1, s, d(Val{n}, s, form); kw...) + d(Val{n-1}, s, δ(n, s, form; kw...))
Δ(::Type{Val{n}}, s::HasDeltaSet; matrix_type::Type=SparseMatrixCSC{Float64}, kw...) where n =
  δ(n+1,s; matrix_type=matrix_type, kw...) * d(Val{n},s,matrix_type) +
		d(Val{n-1},s,matrix_type) * δ(n,s; matrix_type=matrix_type, kw...)

Δ(::Type{Val{1}}, s::AbstractDeltaDualComplex1D, form::AbstractVector; kw...) =
  d(Val{0}, s, δ(1, s, form; kw...))
Δ(::Type{Val{1}}, s::AbstractDeltaDualComplex1D; matrix_type::Type=SparseMatrixCSC{Float64}, kw...) =
  d(Val{0},s,matrix_type) * δ(1,s; matrix_type=matrix_type, kw...)

Δ(::Type{Val{2}}, s::AbstractDeltaDualComplex2D, form::AbstractVector; kw...) =
  d(Val{1}, s, δ(2, s, form; kw...))
Δ(::Type{Val{2}}, s::AbstractDeltaDualComplex2D; matrix_type::Type=SparseMatrixCSC{Float64}, kw...) =
  d(Val{1},s,matrix_type) * δ(2,s; matrix_type=matrix_type, kw...)
""" Alias for the Laplace-de Rham operator [`Δ`](@ref).
"""
const laplace_de_rham = Δ

""" Flat operator converting vector fields to 1-forms.

A generic function for discrete flat operators. Currently only the DPP-flat from
(Hirani 2003, Definition 5.5.2) and (Desbrun et al 2005, Definition 7.3) is
implemented.

See also: the sharp operator [`♯`](@ref).
"""
♭(s::HasDeltaSet, X::DualVectorField) = EForm(♭(s, X.data, DPPFlat()))

""" Alias for the flat operator [`♭`](@ref).
"""
const flat = ♭

""" Sharp operator for converting 1-forms to vector fields.

A generic function for discrete sharp operators. Currently only the
primal-primal flat from (Hirani 2003, Definition 5.8.1 and Remark 2.7.2) is
implemented.

!!! note

    A PP-flat is also defined in (Desbrun et al 2005, Definition 7.4) but
    differs in two ways: Desbrun et al's notation suggests a *unit* normal
    vector, whereas the gradient of Hirani's primal-primal interpolation
    function is not necessarily a unit vector. More importantly, Hirani's vector
    is a normal to a different face than Desbrun et al's, with further confusion
    created by the fact that Hirani's Figure 5.7 agrees with Desbrun et al's
    description rather than his own. That being said, to the best of our
    knowledge, our implementation is the correct one and agrees with Hirani's
    description, if not his figure.

See also: the flat operator [`♭`](@ref).
"""
♯(s::HasDeltaSet, α::EForm) = PrimalVectorField(♯(s, α.data, PPSharp()))

""" Alias for the sharp operator [`♯`](@ref).
"""
const sharp = ♯

""" Wedge product of discrete forms.

The wedge product of a ``k``-form and an ``l``-form is a ``(k+l)``-form.

The DEC and related systems have several flavors of wedge product. This one is
the discrete primal-primal wedge product introduced in (Hirani, 2003, Chapter 7)
and (Desbrun et al 2005, Section 8). It depends on the geometric embedding and
requires the dual complex.
"""
∧(s::HasDeltaSet, α::SimplexForm{k}, β::SimplexForm{l}) where {k,l} =
  SimplexForm{k+l}(∧(Tuple{k,l}, s, α.data, β.data))
@inline ∧(k::Int, l::Int, s::HasDeltaSet, args...) = ∧(Tuple{k,l}, s, args...)

function ∧(::Type{Tuple{k,l}}, s::HasDeltaSet, α, β) where {k,l}
  map(simplices(k+l, s)) do x
    ∧(Tuple{k,l}, s, α, β, x)
  end
end

∧(::Type{Tuple{0,0}}, s::HasDeltaSet, f, g, x::Int) = f[x]*g[x]
∧(::Type{Tuple{k,0}}, s::HasDeltaSet, α, g, x::Int) where k =
  wedge_product_zero(Val{k}, s, g, α, x)
∧(::Type{Tuple{0,k}}, s::HasDeltaSet, f, β, x::Int) where k =
  wedge_product_zero(Val{k}, s, f, β, x)

""" Wedge product of a 0-form and a ``k``-form.
"""
function wedge_product_zero(::Type{Val{k}}, s::HasDeltaSet,
                            f, α, x::Int) where k
  subs = subsimplices(k, s, x)
  vs = primal_vertex(k, s, subs)
  coeffs = map(x′ -> dual_volume(k,s,x′), subs) / volume(k,s,x)
  dot(coeffs, f[vs]) * α[x] / factorial(k)
end

""" Alias for the wedge product operator [`∧`](@ref).
"""
const wedge_product = ∧

""" Interior product of a vector field (or 1-form) and a ``n``-form.

Specifically, this operation is the primal-dual interior product defined in
(Hirani 2003, Section 8.2) and (Desbrun et al 2005, Section 10). Thus it takes a
primal vector field (or primal 1-form) and a dual ``n``-forms and then returns a
dual ``(n-1)``-form.
"""
interior_product(s::HasDeltaSet, X♭::EForm, α::DualForm{n}; kw...) where n =
  DualForm{n-1}(interior_product_flat(Val{n}, s, X♭.data, α.data); kw...)

""" Interior product of a 1-form and a ``n``-form, yielding an ``(n-1)``-form.

Usually, the interior product is defined for vector fields; this function
assumes that the flat operator [`♭`](@ref) (not yet implemented for primal
vector fields) has already been applied to yield a 1-form.
"""
@inline interior_product_flat(n::Int, s::HasDeltaSet, args...; kw...) =
  interior_product_flat(Val{n}, s, args...; kw...)

function interior_product_flat(::Type{Val{n}}, s::HasDeltaSet,
                               X♭::AbstractVector, α::AbstractVector;
                               kw...) where n
  # TODO: Global sign `iseven(n*n′) ? +1 : -1`
  n′ = ndims(s) - n
  hodge_star(n′+1,s, wedge_product(n′,1,s, inv_hodge_star(n′,s, α; kw...), X♭); kw...)
end

""" Lie derivative of ``n``-form with respect to a vector field (or 1-form).

Specifically, this is the primal-dual Lie derivative defined in (Hirani 2003,
Section 8.4) and (Desbrun et al 2005, Section 10).
"""
ℒ(s::HasDeltaSet, X♭::EForm, α::DualForm{n}; kw...) where n =
  DualForm{n}(lie_derivative_flat(Val{n}, s, X♭, α.data; kw...))

""" Alias for Lie derivative operator [`ℒ`](@ref).
"""
const lie_derivative = ℒ

""" Lie derivative of ``n``-form with respect to a 1-form.

Assumes that the flat operator [`♭`](@ref) has already been applied to the
vector field.
"""
@inline lie_derivative_flat(n::Int, s::HasDeltaSet, args...; kw...) =
  lie_derivative_flat(Val{n}, s, args...; kw...)

function lie_derivative_flat(::Type{Val{0}}, s::HasDeltaSet,
                             X♭::AbstractVector, α::AbstractVector; kw...)
  interior_product_flat(1, s, X♭, dual_derivative(0, s, α); kw...)
end

function lie_derivative_flat(::Type{Val{1}}, s::HasDeltaSet,
                             X♭::AbstractVector, α::AbstractVector; kw...)
  interior_product_flat(2, s, X♭, dual_derivative(1, s, α); kw...) +
    dual_derivative(0, s, interior_product_flat(1, s, X♭, α; kw...))
end

function lie_derivative_flat(::Type{Val{2}}, s::HasDeltaSet,
                             X♭::AbstractVector, α::AbstractVector; kw...)
  dual_derivative(1, s, interior_product_flat(2, s, X♭, α; kw...))
end

# Euclidean geometry
####################

""" A notion of "geometric center" of a simplex.

See also: [`geometric_center`](@ref).
"""
abstract type SimplexCenter end

""" Calculate the center of simplex spanned by given points.

The first argument is a list of points and the second specifies the notion of
"center", via an instance of [`SimplexCenter`](@ref).
"""
function geometric_center end

""" Barycenter, aka centroid, of a simplex.
"""
struct Barycenter <: SimplexCenter end

function geometric_center(points, ::Barycenter)
  sum(points) / length(points)
end

""" Circumcenter, or center of circumscribed circle, of a simplex.

The circumcenter is calculated by inverting the Cayley-Menger matrix, as
explained by
[Westdendorp](https://westy31.home.xs4all.nl/Circumsphere/ncircumsphere.htm).
This method of calculation is also used in the package
[AlphaShapes.jl](https://github.com/harveydevereux/AlphaShapes.jl).
"""
struct Circumcenter <: SimplexCenter end

function geometric_center(points, ::Circumcenter)
  CM = cayley_menger(points...)
  barycentric_coords = inv(CM)[1,2:end]
  mapreduce(*, +, barycentric_coords, points)
end

""" Incenter, or center of inscribed circle, of a simplex.
"""
struct Incenter <: SimplexCenter end

function geometric_center(points, ::Incenter)
  length(points) > 2 || return geometric_center(points, Barycenter())
  face_volumes = map(i -> volume(deleteat(points, i)), eachindex(points))
  barycentric_coords = face_volumes / sum(face_volumes)
  mapreduce(*, +, barycentric_coords, points)
end

end


function readinput(filename)
    map(value -> parse(Int, value), readlines(filename))
end

function getjoltdifferences(jolts)
    basejolt = 0
    joltdifferences = Dict(1 => 0, 2 => 0, 3 => 0)
    joltdifferences[3] += 1
    while length(jolts) > 0
        jolt = popfirst!(jolts)
        joltdifferences[jolt - basejolt] += 1
        basejolt = jolt
    end
    return joltdifferences
end

function getjoltcombinations(jolts)
    pushfirst!(jolts, 0)
    n = length(jolts)
    joltcombinations = zeros(Int, n)
    joltcombinations[1] = 1
    for i in 1:n-1
        for k in i+1:n
            if jolts[k] - jolts[i] <= 3
                joltcombinations[k] += joltcombinations[i]
            end
        end
    end
    return joltcombinations
end

jolts = sort!(readinput("d10.in"))
joltdifferences = getjoltdifferences(deepcopy(jolts))
joltcombinations = getjoltcombinations(deepcopy(jolts))
println("P1: ", joltdifferences[1] * joltdifferences[3])
println("P2: ", joltcombinations[end])


export Airy

"""
	Airy(param;mesh,label)

Define an object of type `AbstractModel` in view of solving the initial-value problem for
the linear ([Airy](https://en.wikipedia.org/wiki/Airy_wave_theory)) water waves equations.

# Arguments
- `param::NamedTuple` must contain
    - the shallowness parameter `μ` (set the infinite-layer case if `μ=Inf`);
    - optionally, `ν` the shallow/deep water scaling factor. By default, `ν=1` if `μ≦1` and `ν=1/√μ` otherwise;
    - numerical parameters to construct the mesh of collocation points, if `mesh` is not provided.
- `mesh  :: Mesh`: the mesh of collocation points. By default, `mesh = Mesh(param)`.
- `label :: String`: a label for future references (default is `"linear (Airy)"`).

# Return values
Generate necessary ingredients for solving an initial-value problem via `solve!`:
1. a function `Airy.f!` to be called in explicit time-integration solvers;
2. a function `Airy.mapto` which from `(η,v)` of type `InitialData` provides the raw data matrix on which computations are to be executed;
3. a function `Airy.mapfro` which from such data matrix returns the Tuple of real vectors `(η,v,x)`, where
    - `η` is the values of surface deformation at collocation points `x`;
    - `v` is the derivative of the trace of the velocity potential at `x`.

"""
mutable struct Airy <: AbstractModel

	label   :: String
	f!		:: Function
	mapto	:: Function
	mapfro	:: Function
	info 	:: String

    function Airy(param::NamedTuple; # param is a NamedTuple containing all necessary parameters
		mesh = Mesh(param),
		IL	    = false,
		label = "linear (Airy)"  # using a keyword argument allows the user to supersede the default label.
		 )

		# Set up
		μ = param.μ
		if !in(:ν,keys(param))
			if μ > 1
				ν = 1/sqrt(μ)
				nu = "1/√μ (deep water case)"
			else
				ν = 1
				nu = "1 (shallow water case)"
			end
		else
			ν = param.ν
			nu = "$ν"
		end
		if μ == Inf || ν==0 || IL == true # infinite layer case
			IL = true;  # IL (=Infinite layer) is a flag to be used thereafter
			μ = 1; ν = 1; # Then we should set μ=ν=1 in subsequent formula.
		end

		# Print information
		info = "$label model.\n"
		if IL == true
			info *= "└─Infinite depth case.\n"
		else
			info *= "└─Shallowness parameter μ=$μ, \
					scaling parameter ν=$nu.\n"
		end
		info *= "\nDiscretized with $(mesh.N) collocation points on [$(mesh.xmin), $(mesh.xmax)]."

		# Collocation points and Fourier modes
		x, k = mesh.x, mesh.k
		# Fourier multipliers
    	∂ₓ	 = 1im * k            # Differentiation
		if IL == true
			∂ₓF₁ = 1im * sign.(k)
		else
			∂ₓF₁ = 1im * tanh.(√μ*k)/(√μ*ν)
		end
		# Pre-allocation
		fftη = zeros(Complex{Float64}, mesh.N)
        fftv = zeros(Complex{Float64}, mesh.N)

		# Evolution equations are ∂t U = f(U)
		function f!(U)

			fftη .= U[:,1]
			fftv .= U[:,2]

		   	U[:,1] .= -∂ₓF₁.*fftv
		   	U[:,2] .= -∂ₓ.*fftη

		end

		# Build raw data from physical data (discrete Fourier transform)
		function mapto(data::InitialData)
			U = [fft(data.η(x)) fft(data.v(x))]
		end

		# Reconstruct physical variables from raw data
		# Return `(η,v,x)`, where
		# - `η` is the surface deformation;
		# - `v` is the derivative of the trace of the velocity potential;
		# - `x` is the vector of collocation points
		function mapfro(U)
			real(ifft(U[:,1])),real(ifft(U[:,2])),mesh.x
		end

        new(label, f!, mapto, mapfro, info )

    end
end


export Differential, expand_derivatives, @derivatives

struct Differential <: Function
    x::Expression
end

Base.show(io::IO, D::Differential) = print(io, "(D'~", D.x, ")")
Base.convert(::Type{Expr}, D::Differential) = D

(D::Differential)(x::Operation) = Operation(D, Expression[x])
function (D::Differential)(x::Variable)
    D.x === x             && return Constant(1)
    has_dependent(x, D.x) || return Constant(0)
    return Operation(D, Expression[x])
end
(::Differential)(::Any) = Constant(0)
Base.:(==)(D1::Differential, D2::Differential) = isequal(D1.x, D2.x)

function expand_derivatives(O::Operation)
    @. O.args = expand_derivatives(O.args)

    if O.op isa Differential
        D = O.op
        o = O.args[1]
        isa(o, Operation) || return O
        return simplify_constants(sum(i->derivative(o,i)*expand_derivatives(D(o.args[i])),1:length(o.args)))
    end

    return O
end
expand_derivatives(x) = x

# Don't specialize on the function here
derivative(O::Operation, idx) = derivative(O.op, (O.args...,), Val(idx))

# Pre-defined derivatives
import DiffRules, SpecialFunctions, NaNMath
for (modu, fun, arity) ∈ DiffRules.diffrules()
    for i ∈ 1:arity
        @eval function derivative(::typeof($modu.$fun), args::NTuple{$arity,Any}, ::Val{$i})
            M, f = $(modu, fun)
            partials = DiffRules.diffrule(M, f, args...)
            dx = @static $arity == 1 ? partials : partials[$i]
            convert(Expression, dx)
        end
    end
end

function count_order(x)
    @assert !(x isa Symbol) "The variable $x must have an order of differentiation that is greater or equal to 1!"
    n = 1
    while !(x.args[1] isa Symbol)
        n = n+1
        x = x.args[1]
    end
    n, x.args[1]
end

_repeat_apply(f, n) = n == 1 ? f : f ∘ _repeat_apply(f, n-1)
function _differential_macro(x)
    ex = Expr(:block)
    lhss = Symbol[]
    x = flatten_expr!(x)
    for di in x
        @assert di isa Expr && di.args[1] == :~ "@derivatives expects a form that looks like `@derivatives D''~t E'~t`"
        lhs = di.args[2]
        rhs = di.args[3]
        order, lhs = count_order(lhs)
        push!(lhss, lhs)
        expr = :($lhs = $_repeat_apply(Differential($rhs), $order))
        push!(ex.args,  expr)
    end
    push!(ex.args, Expr(:tuple, lhss...))
    ex
end

macro derivatives(x...)
    esc(_differential_macro(x))
end

function calculate_jacobian(eqs,vars)
    Expression[Differential(vars[j])(eqs[i]) for i in 1:length(eqs), j in 1:length(vars)]
end


using Revise
	using SparseArrays, LinearAlgebra, DiffEqOperators, Setfield, Parameters
	using BifurcationKit
	using Plots
	const BK = BifurcationKit
################################################################################
# case of the SH equation
Nx = 200; Lx = 6.;
X = -Lx .+ 2Lx/Nx*(0:Nx-1) |> collect
hx = X[2]-X[1]

norminf(x) = norm(x, Inf64)
const _weight = rand(Nx)
normweighted(x) = norm(_weight .* x)

# Q = Neumann0BC(hx)
Q = Dirichlet0BC(hx |> typeof)
Dxx = sparse(CenteredDifference(2, 2, hx, Nx) * Q)[1]
Lsh = -(I + Dxx)^2

function R_SH(u, par)
	@unpack l, ν, L1 = par
	out = similar(u)
	out .= L1 * u .+ l .* u .+ ν .* u.^3 - u.^5
end

Jac_sp(u, par) = par.L1 + spdiagm(0 => par.l .+ 3 .* par.ν .* u.^2 .- 5 .* u.^4)
d2R(u,p,dx1,dx2) = @. p.ν * 6u*dx1*dx2 - 5*4u^3*dx1*dx2
d3R(u,p,dx1,dx2,dx3) = @. p.ν * 6dx3*dx1*dx2 - 5*4*3u^2*dx1*dx2*dx3
jet = (R_SH, Jac_sp, d2R, d3R)

parSH = (l = -0.1, ν = 2., L1 = Lsh)
####################################################################################################
sol0 = 1.1cos.(X) .* exp.(-0X.^2/(2*5^2))
	optnew = NewtonPar(verbose = false, tol = 1e-12)
	# allocations 357, 0.8ms
	sol1, = @time newton(
	R_SH, Jac_sp,
	sol0, (@set parSH.l = -.1), optnew)
	Plots.plot(X, sol1)

opts = ContinuationPar(dsmin = 0.0001, dsmax = 0.01, ds = 0.01,
		newtonOptions = setproperties(optnew; maxIter = 30, tol = 1e-8), pMax = 1.,
		maxSteps = 300, plotEveryStep = 40, detectBifurcation = 3, nInversion = 4, tolBisectionEigenvalue = 1e-17, dsminBisection = 1e-7)

function cb(state; kwargs...)
	fromNewton = get(kwargs, :fromNewton, false)
	_x = get(kwargs, :z0, nothing)
	if ~fromNewton
		return norm(_x.u - state.x) < 20.5 && abs(_x.p - state.p)<0.05
	end
	true
end

kwargsC = (verbosity = 3,
	plot = true,
	# tangentAlgo = BorderedPred(),
	linearAlgo  = MatrixBLS(),
	recordFromSolution = (x, p) -> (n2 = norm(x), nw = normweighted(x), s = sum(x), s2 = x[end ÷ 2], s4 = x[end ÷ 4], s5 = x[end ÷ 5]),
	plotSolution = (x, p;kwargs...)->(plot!(X, x; ylabel="solution", label="", kwargs...)),
	callbackN = cb
	)

brflat, = @time continuation(
	R_SH, Jac_sp, sol1, (@set parSH.l = -0.1), (@lens _.l), opts;
	kwargsC...)

plot(brflat, putspecialptlegend = false)
####################################################################################################
# branch switching
function optrec(x, p, l; opt = opts)
	level =  l
	if level <= 2
		return setproperties(opt; maxSteps = 300, detectBifurcation = 3, nev = Nx, detectLoop = false)
	else
		return setproperties(opt; maxSteps = 250, detectBifurcation = 3, nev = Nx, detectLoop = true)
	end
end

diagram = @time bifurcationdiagram(jet..., sol1, (@set parSH.l = -0.1), (@lens _.l), 2, optrec; kwargsC..., halfbranch = true, verbosity = 0, usedeflation = true)

code = ()
	vars = (:param, :n2)
	plot(diagram; code = code, plotfold = false,  markersize = 2, putspecialptlegend = false, vars = vars)
	plot!(brflat, putspecialptlegend = false, vars = vars)
	title!("#branches = $(size(diagram, code))")

diagram2 = bifurcationdiagram!(jet..., BK.getBranch(diagram, (2,)), 3, optrec; kwargsC..., halfbranch = true)

####################################################################################################
deflationOp = DeflationOperator(2, 1.0, [sol1])

br, = @time continuation(
	R_SH, Jac_sp,
	(@set parSH.l = -1.), (@lens _.l),
	setproperties(opts; ds = 0.001, maxSteps = 20000, pMax = 0.25, pMin = -1., newtonOptions = setproperties(optnew; tol = 1e-9, maxIter = 15, verbose = false), saveSolEveryStep = 0, detectBifurcation = 0),
	deflationOp;
	verbosity = 1,
	maxBranches = 150,
	perturbSolution = (sol, p, id) -> sol .+ 0.02 .* rand(length(sol)),
	normN = x -> norm(x, Inf64),
	recordFromSolution = (x, p) -> (s5 = x[end ÷ 5], n2 = norm(x), nw = normweighted(x), s = sum(x), s2 = x[end ÷ 2], s4 = x[end ÷ 4],),
	# tangentAlgo = SecantPred(),
	)

plot(br..., legend=false, linewidth=1, vars = (:param, :n2))


"""
    filter_exp(N, Nc, s, V, invV)

Construct exponential filter for nodal solution

- @arg N: number of coefficients
- @arg Nc: cutoff location
- @arg s: order of filter (must be even)
- @arg V: Vandermonde matrix
"""
function filter_exp(N, s, V, Nc = 0, invV = inv(V))
    nv = size(V, 1)
    if nv == N + 1
        filterdiag = KitBase.filter_exp1d(N, s, Nc)
    elseif nv == (N + 1) * (N + 2) ÷ 2
        filterdiag = filter_exp2d(N, s, Nc)
    end
    F = V * diagm(filterdiag) * invV

    return F
end

"""
    filter_exp2d(N, Nc, s)

Construct exponential filter for modal solution

- @arg N: degree of polynomials
- @arg s: order of filter (must be even)
- @arg Nc: cutoff location
"""
function filter_exp2d(Norder, sp, Nc = 0)
    alpha = -log(eps())

    filterdiag = ones((Norder + 1) * (Norder + 2) ÷ 2)
    sk = 1
    for i = 0:Norder
        for j = 0:Norder-i
            if i + j >= Nc
                filterdiag[sk] = exp(-alpha * ((i + j - Nc) / (Norder - Nc))^sp)
            end
            sk += 1
        end
    end

    return filterdiag
end

"""
    basis_norm(deg)

Calculate norm of polynomial basis
"""
function basis_norm(deg)
    NxHat = 100
    xHat = range(-1, stop = 1, length = NxHat) |> collect
    dxHat = xHat[2] - xHat[1]

    nLocal = deg + 1
    PhiL1 = zeros(nLocal)
    for i = 1:nLocal
        PhiL1[i] = dxHat * sum(abs.(JacobiP(xHat, 0, 0, i - 1)))
    end

    return PhiL1
end


# This code calculates the lyapunov spectrum of the KS equation
# by transforming it in spectral space.
# It uses very efficient handling of Fourier transforms.
# It saves exponents to be used by the figure generating scripts.
using DrWatson
@quickactivate "NonlinearDynamicsTextbook"
include(srcdir("style.jl"))
using OrdinaryDiffEq
import FFTW, LinearAlgebra

# In the following `u` is the field in real space, while `y` is the Fourier
# transform of the field, which lives in inverse space.
# `w` is a matrix that has the first column the state `y` and the remaining the
# deviation vectors `δy`.

function kse_spectral_and_tangent!(dw, w, p, t)
    @unpack  ks, forward_plan, inverse_plan, yd, ud, ud2, ud3, yd2, ik2, k²_k⁴ = p
    y = view(w, :, 1)
    dy = view(dw, :, 1)

    # KS equation in spectral space
    LinearAlgebra.mul!(ud, inverse_plan, y) # create current u in x-space
    @. ud3 = ud*ud
    y² = LinearAlgebra.mul!(yd, forward_plan, ud3) # transform to k-space
    @. dy = - y²*ik2 + k²_k⁴*y

    # Equations for tangent space in spectral space (loop over each deviation vector)
    for i in 2:size(w, 2)
        δy = view(w, :, i)
        dδy = view(dw, :, i)
        ud2 .= ud # keeps track for tangent
        LinearAlgebra.mul!(ud2, inverse_plan, δy)
        ud2 .*= ud
        spectral_tan = LinearAlgebra.mul!(yd2, forward_plan, ud2)
        @. dδy = k²_k⁴*δy + im*ks*spectral_tan
    end
    return nothing
end

function orthonorm_spectral(D, M = D) # Orthonormal vectors converted to spectral space
    M > D && throw(ArgumentError("M must be ≤ D"))
    Δ = Matrix(LinearAlgebra.qr(rand(D, M)).Q)
    FFTW.rfft(Δ)
end

function tannorm(u::AbstractMatrix, t)
    s = size(u)[1]
    x = 0.0
    for i in 1:s; @inbounds x += abs2(u[i, 1]); end
    return sqrt(x/length(x))
end
tannorm(u, t) = abs(u)

# %% Lyapunov code
# This function uses some convenience methods from DynamicalSystems.jl
# based on the `tangent_integrator` format (state is a matrix. first column
# is actual system state, all other columns are deviation vectors)
import ProgressMeter
function kse_lyapunovs_spectral(integ, N, Δt::Real)
    progress = ProgressMeter.Progress(N; desc = "KSE Lyapunov Spectrum: ", dt = 1.0)
    M = size(integ.u, 2) - 1 # number of Ls
    λ = zeros(M)
    forward_plan = integ.p.forward_plan
    inverse_plan = integ.p.inverse_plan
    W = zeros(length(inverse_plan*integ.u[:, 1]), M) # for use in buffer
    t0 = integ.t
    ud = zeros(size(W, 1))
    yd = integ.u[:, 1]

    for n in 1:N
        step!(integ, Δt)
        # Get deviations in real space
        for i in 1:M
            LinearAlgebra.mul!(ud, inverse_plan, view(integ.u, :, i+1))
            W[:, i] .= ud
        end
        # Perform a (buffered) QR decomposition
        Q, R = LinearAlgebra.qr!(W)
        # Keep track of LEs
        for j in 1:M
            @inbounds λ[j] += log(abs(R[j,j]))
        end
        # Set the new deviations (also convert back to spectral)
        for i in 1:M
            ud .= Q[:, i]
            LinearAlgebra.mul!(yd, forward_plan, ud)
            integ.u[:, i+1] .= yd
        end
        u_modified!(integ, true) # Ensure that DiffEq knows we did something!
        ProgressMeter.update!(progress, n)
    end
    λ ./= (integ.t - t0)
    return λ
end

# %% Actually compute the exponents
function produce_kse_lyaps(config)
    @unpack b, dx, N, Δt, kport = config
    xs = range(0, b; step = dx) # space
    u0 = @. cos(xs) + 0.1*sin(xs/8) + 0.01*cos((2π/b)*xs)
    ks = Vector(FFTW.rfftfreq(length(u0))/dx) # conjugate space (wavenumbers)

    forward_plan = FFTW.plan_rfft(u0)
    y0 = forward_plan * u0
    inverse_plan = FFTW.plan_irfft(y0, length(u0))
    ik2 = -im .* ks ./ 2
    k²_k⁴ = @. ks^2 - ks^4

    ud = copy(u0)
    ud2 = copy(u0)
    ud3 = copy(u0)
    yd = copy(y0)
    yd2 = copy(y0)
    ksparams = @ntuple forward_plan inverse_plan ks ud ud2 ud3 yd yd2 k²_k⁴ ik2

    D = length(u0)
    M = round(Int, kport*D)
    δy0 = orthonorm_spectral(D, M)
    w0 = hcat(y0, δy0)
    prob = ODEProblem(kse_spectral_and_tangent!, w0, (0.0, 100.0), ksparams)
    integ = init(prob, Tsit5(); save_everystep = false, internalnorm = tannorm)

    @time λs = kse_lyapunovs_spectral(integ, N, Δt)
    return @strdict λs b dx N Δt kport
end

b = 20      # length of spatial domain
N = 4000    # steps to run the QR normalization
Δt = 1.0    # timestep between normalizations
dx = 0.2    # spatial discretization
kport = 0.5

for b in 20:20:60
    if b == 20
        kport = 0.5
    else
        kport = 0.3
    end
    @show b
    config = @strdict N dx Δt b kport
    file, path = produce_or_load(
        datadir("ksiva"), config, produce_kse_lyaps;
        prefix = "ksiva_spectrum", ignores = ["kport"], storepatch = false,
    )
end

b = 20

for dx in (0.2, 0.15, 0.1)
    kport = 0.5
    @show dx
    config = @strdict N dx Δt b kport
    file, path = produce_or_load(
        datadir("ksiva"), config, produce_kse_lyaps;
        prefix = "ksiva_spectrum", ignores = ["kport"], storepatch = false,
    )
end


module TestMax
    using StreamStats
    using Distributions
    using Base.Test

    # Max of uniform draws
    for n in rand(1:1_000_000, 100)
        xs = rand(n)
        stat = StreamStats.Max()
        for x in xs
            update!(stat, x)
        end
        online_m = maximum(stat)
        online_ms = state(stat)
        online_n = nobs(stat)
        batch_m = maximum(xs)
        @test online_m == batch_m
        @test online_n == n
    end
end


x = 20

println(2x+2)
println(2(x)+2)
println(2(x+1))
println((2)x+2)

println(2+3x)
println(2*3x)
println(2^3x)
println(2(div(x,2)))


using Test, Flux , StochasticDiffEq , LinearAlgebra
println("Optimal Stopping Time Test")
using NeuralPDE

using Random
Random.seed!(100)

d = 1
r = 0.04f0
beta = 0.2f0
T = 1
u0 = fill(80.00 , d , 1)
sdealg = EM()
ensemblealg = EnsembleThreads()
f(du,u,p,t) = (du .= r*u)
sigma(du,u,p,t)  = (du .= Diagonal(beta*u))
tspan = (0.0 , 1.0)
N = 50
dt = tspan[2]/49
K = 100.00
function g(t , x)
  return exp(-r*t)*(max(K -  maximum(x)  , 0))
end

prob  = SDEProblem(f , sigma , u0 , tspan ; g = g)
opt = Flux.ADAM(0.1)
m = Chain(Dense(d , 5, tanh), Dense(5, 16 , tanh)  , Dense(16 , N ), softmax)
sol = solve(prob, NeuralPDE.NNStopping( m, opt , sdealg , ensemblealg), verbose = true, dt = dt,
            abstol=1e-6, maxiters = 20 , trajectories = 200)

##Analytical Binomial Tree approach for American Options
function BinomialTreeAM1D(S0 , N , r , beta)
    V = zeros(N+1)
    dT = T/N
    u = exp(beta*sqrt(dT))
    d = 1/u
    S_T = [S0*(u^j)* (d^(N-j)) for j in 0:N]
    a = exp(r*dT)
    p = (a - d)/(u - d)
    q = 1.0 - p
    V = [max(K - x , 0) for x in S_T]
    for i in N-1:-1:0
      V[1:end-1] = exp(-r*dT).*(p*V[2:end] + q*V[1:end-1])
      S_T = S_T*u
      V = [max(K - S_T[i] , V[i]) for i in 1:size(S_T)[1]]
    end
    return V[1]
end
real_sol = BinomialTreeAM1D(u0[1] , N , r , beta)
error = abs(sol - real_sol)
@test error < 0.5


## test_spectral.jl : test spectral method for GRF generation

@testset "spectral decomposition   " begin

## 1d Mat\'ern ##
cov = CovarianceFunction(1,Matern(0.3,1))
pts = 0:0.01:1
grf = GaussianRandomField(cov,Spectral(),pts)
@test isa(grf,GaussianRandomField)
@test isa(grf.cov,CovarianceFunction)
@test isa(grf.cov.cov,Matern)
@test ndims(grf.cov) == 1
@test isa(grf,GaussianRandomField{C,Spectral} where {C})
@test length(grf.pts[1]) == length(pts)
@test length(sample(grf)) == length(pts)

## 2d Exponential ##
cov = CovarianceFunction(2,Exponential(1))
pts1 = 0:0.05:1
pts2 = 0:0.05:1
grf = GaussianRandomField(cov,Spectral(),pts1,pts2)
@test isa(grf,GaussianRandomField)
@test isa(grf.cov,CovarianceFunction)
@test isa(grf.cov.cov,Exponential)
@test ndims(grf.cov) == 2
@test isa(grf,GaussianRandomField{C,Spectral} where {C})
@test length(grf.pts) == 2
@test length(grf.pts[1]) == length(pts1)
@test length(grf.pts[2]) == length(pts2)
@test size(sample(grf),1) == length(pts1)
@test size(sample(grf),2) == length(pts2)

## 3d Gaussian ##
cov = CovarianceFunction(3,Matern(1,2.5))
pts1 = 0:0.1:1
pts2 = 0:0.1:1
pts3 = 0:0.1:1
grf = GaussianRandomField(cov,Spectral(),pts1,pts2,pts3)
@test isa(grf,GaussianRandomField)
@test isa(grf.cov,CovarianceFunction)
@test isa(grf.cov.cov,Matern)
@test ndims(grf.cov) == 3
@test isa(grf,GaussianRandomField{C,Spectral} where {C})
@test length(grf.pts) == 3
@test length(grf.pts[1]) == length(pts1)
@test length(grf.pts[2]) == length(pts2)
@test length(grf.pts[3]) == length(pts3)
@test size(sample(grf),1) == length(pts1)
@test size(sample(grf),2) == length(pts2)
@test size(sample(grf),3) == length(pts3)

## non-SPD covariance matrix ##
cov = CovarianceFunction(2,SquaredExponential(0.5))
pts1 = 0:0.05:1
pts2 = 0:0.05:1
@suppress grf = GaussianRandomField(cov,Spectral(),pts1,pts2)
@test isa(grf,GaussianRandomField)
@test isa(grf.cov,CovarianceFunction)
@test isa(grf.cov.cov,SquaredExponential)
@test ndims(grf.cov) == 2
@test isa(grf,GaussianRandomField{C,Spectral} where {C})
@test length(grf.pts) == 2
@test length(grf.pts[1]) == length(pts1)
@test length(grf.pts[2]) == length(pts2)
@test size(sample(grf),1) == length(pts1)
@test size(sample(grf),2) == length(pts2)

## test non-equidistant structured grid  ##
cov = CovarianceFunction(2,Matern(1.,2.5,σ=1.,p=2))
pts1 = sin.(-pi/2:0.2:pi/2)
pts2 = sin.(-pi/2:0.1:pi/2)
grf = GaussianRandomField(cov,Spectral(),pts1,pts2)
@test isa(grf,GaussianRandomField)
@test isa(grf.cov,CovarianceFunction)
@test isa(grf.cov.cov,Matern)
@test ndims(grf.cov) == 2
@test isa(grf,GaussianRandomField{C,Spectral} where {C})
@test length(grf.pts) == 2
@test length(grf.pts[1]) == length(pts1)
@test length(grf.pts[2]) == length(pts2)
@test size(sample(grf),1) == length(pts1)
@test size(sample(grf),2) == length(pts2)

## test anisotropic
cov = CovarianceFunction(2,AnisotropicExponential([1000 0; 0 1000]))
pts1 = range(0,stop = 10,length = 64)
pts2 = range(0,stop = 10,length = 64)
grf = GaussianRandomField(cov,Spectral(),pts1,pts2)
@test isa(grf,GaussianRandomField)
@test isa(grf.cov,CovarianceFunction)
@test isa(grf.cov.cov,AnisotropicExponential)
@test ndims(grf.cov) == 2
@test isa(grf,GaussianRandomField{C,Spectral} where {C})
@test length(grf.pts) == 2
@test length(grf.pts[1]) == length(pts1)
@test length(grf.pts[2]) == length(pts2)
@test size(sample(grf),1) == length(pts1)
@test size(sample(grf),2) == length(pts2)

end


"""
    point_grid(bbox::Vector{T}, cellSide::Real, mask::Union{Polygon, Nothing}=nothing, units::String="kilometers") where {T <: Real}

Create a Point grid from a bounding box

# Examples
```jldoctest
julia> bbox = [-1, 2, 1, 3]
4-element Array{Int64,1}:
 -1
  2
  1
  3

julia> point_grid(bbox, 100)
FeatureCollection{Feature}(Feature[Feature(Point([-0.899869, 2.05034]), Dict{String,Any}()), Feature(Point([-0.899869, 2.94966]), Dict{String,Any}()), Feature(Point([0.0, 2.05034]), Dict{String,Any}()), Feature(Point([0.0, 2.94966]), Dict{String,Any}()), Feature(Point([0.899869, 2.05034]), Dict{String,Any}()), Feature(Point([0.899869, 2.94966]), Dict{String,Any}())], nothing, nothing)
```
"""
function point_grid(bbox::Vector{T}, cellSide::Real, mask::Union{Polygon, Nothing}=nothing, units::String="kilometers") where {T <: Real}
    results = []

    west = bbox[1]
    south = bbox[2]
    east = bbox[3]
    north = bbox[4]

    xfrac = cellSide / (distance(float([west, south]), float([east, south]), units))
    cellWidth = xfrac * (east - west)
    yfrac = cellSide / (distance(float([west, south]), float([west, north]), units))
    cellHeight = yfrac * (north - south)

    bbWidth = east - west
    bbHeight = north - south

    cols = floor(bbWidth / cellWidth)
    rows = floor(bbHeight / cellHeight)

    Δx  = (bbWidth - cols * cellWidth) / 2
    Δy = (bbHeight - rows * cellHeight) / 2

    curx = west + Δx

    while curx <= east
        cury = south + Δy

        while cury <= north
            pt = Point([curx, cury])
            if mask != nothing
                # if within...
                push!(results, pt)
            else
                push!(results, pt)
            end

            cury += cellHeight
        end

        curx += cellWidth
    end

    return FeatureCollection([Feature(x) for x in results])
end

"""
    rectangle_grid(bbox::Vector{T}, width::Real, height::Real, mask::Union{Polygon, Nothing}=nothing, units::String="kilometers") where {T <: Real}

Create a grid of rectangles from a bounding box.

# Examples
```jldoctest
julia> bbox = [-1, 2, 1, 3]
4-element Array{Int64,1}:
 -1
  2
  1
  3

julia> rectangle_grid(bbox, 150, 100)
FeatureCollection{Feature}(Feature[Feature(Polygon(Array{Array{Float64,1},1}[[[-0.674901, 2.05034], [-0.674901, 2.94966], [0.674901, 2.94966], [0.674901, 2.05034], [-0.674901, 2.05034]]]), Dict{String,Any}())], nothing, nothing)
```
"""
function rectangle_grid(bbox::Vector{T}, width::Real, height::Real, mask::Union{Polygon, Nothing}=nothing, units::String="kilometers") where {T <: Real}
    results = []

    west = bbox[1]
    south = bbox[2]
    east = bbox[3]
    north = bbox[4]

    xfrac = width / (distance(float([west, south]), float([east, south]), units))
    cellWidth = xfrac * (east - west)
    yfrac = height / (distance(float([west, south]), float([west, north]), units))
    cellHeight = yfrac * (north - south)

    bbWidth = east - west
    bbHeight = north - south

    cols = floor(bbWidth / cellWidth)
    rows = floor(bbHeight / cellHeight)

    Δx  = (bbWidth - cols * cellWidth) / 2
    Δy = (bbHeight - rows * cellHeight) / 2

    curx = west + Δx

    for i in 1:cols
        cury = south + Δy

        for j in 1:rows
            poly = Polygon([[
                [curx, cury], [curx, cury + cellHeight], [curx + cellWidth, cury + cellHeight],
                [curx + cellWidth, cury], [curx, cury]]])

            if mask != nothing
                # intersects...
                push!(results, poly)
            else
                push!(results, poly)
            end
            cury += cellHeight
        end
        curx += cellWidth
    end

    return FeatureCollection([Feature(x) for x in results])
end

function hexagon(center::Point, x::Real, y::Real, cosines::Vector, sines::Vector)
    vertices = []
    coords = center.coordinates

    for i in 1:6
      x = coords[1] + x * cosines[i]
      y = coords[2] + y * sines[i]
      push!(vertices, [x, y])
    end
    push!(vertices, vertices[1])

    return Polygon([vertices])
end

function hexTriangles(center::Point, x::Real, y::Real, cosines::Vector, sines::Vector)
    triangles = []
    coords = center.coordinates

    for i in 1:6
        vertices = []
        j = (i + 1) % 6 == 0 ? 1 : (i + 1) % 6
        push!(vertices, coords)
        push!(vertices, [coords[1] + x * cosines[i], coords[2] + y * sines[i]])
        push!(vertices, [coords[1] + x * cosines[j], coords[2] + y * sines[j]])
        push!(vertices, coords)

        push!(triangles, Polygon([vertices]))
    end

    return triangles
end

"""
     hexgrid(bbox::Vector{T}, cellSide::Real, mask::Union{Polygon, Nothing}=nothing, triangles::Bool=false, units::String="kilometers") where {T <: Real}

Take a bounding box and the diameter of the cell and return a FeatureCollection of flat-topped
hexagons or triangles Polygon aligned in an "odd-q" vertical grid as
described in [Hexagonal Grids](http://www.redblobgames.com/grids/hexagons/).
"""
function hexgrid(bbox::Vector{T}, cellSide::Real, mask::Union{Polygon, Nothing}=nothing, triangles::Bool=false, units::String="kilometers") where {T <: Real}
    west, south, east, north = bbox

    centerY = (south + north) / 2.
    centerX = (west + east) / 2.

    xfrac = cellSide * 2. / (distance(float([west, centerY]), float([east, centerY]), units))
    cellWidth = xfrac * (east - west)
    yfrac = cellSide * 2. / (distance(float([centerX, south]), float([centerX, north]), units))
    cellHeight = yfrac * (north - south)
    radius = cellWidth / 2.

    hex_width = radius * 2.
    hex_height = sqrt(3) / 2. * cellHeight

    bWidth = east - west
    bHeight = north - south

    intx = 3. / 4. * hex_width
    inty = hex_height

    spanx = (bWidth - hex_width) / (hex_width - radius / 2.)
    countx = floor(spanx)

    adjustx = ((countx * intx - radius / 2.) - bWidth) / 2. - radius / 2. + intx / 2.

    county = floor((bHeight - hex_height) / hex_height)
    adjusty = (bHeight - county * hex_height) / 2.

    hasOffsetY = (county * hex_height - bHeight) > hex_height / 2.

    hasOffsetY && (adjusty -= hex_height / 4.)

    cosines = []
    sines = []

    for i in 1:6
        angle = 2. * pi / 6. * (i - 1)
        push!(cosines, cos(angle))
        push!(sines, sin(angle))
    end

    results = []

    for i in 1:(countx+1)
        for j in 1:(county+1)
            odd = (i-1) % 2 === 1

            ((j-1) === 0 && odd) && continue
            ((j-1) === 0 && hasOffsetY) && continue

            centerx = (i-1) * intx + west - adjustx
            centery = (j-1) * inty + south + adjusty

            odd && (centery -= hex_height / 2.)

            if triangles
                t = hexTriangles(Point([centerx, centery]),
                    cellWidth / 2., cellHeight / 2., cosines, sines)

                for triangle in t
                    if mask != nothing
                        # intersects...
                        push!(results, triangle)
                    else
                        push!(results, triangle)
                    end
                end
            else
                hex = hexagon(Point([centerx, centery]), cellWidth / 2., cellHeight / 2., cosines, sines)
                if mask != nothing
                    # intersects...
                    push!(results, hex)
                else
                    push!(results, hex)
                end
            end
        end
    end

    return FeatureCollection([Feature(x) for x in results])
end

"""
    square_grid(bbox::Vector{T}, cell_side::Real, mask::Union{Polygon, Nothing}=nothing, units::String="kilometers") where {T <: Real}

Create a square grid from a bounding box.

# Examples
```jldoctest
julia> bbox = [-1, 2, 1, 3]
4-element Array{Int64,1}:
 -1
  2
  1
  3

julia> square_grid(bbox, 100)
FeatureCollection{Feature}(Feature[Feature(Polygon(Array{Array{Float64,1},1}[[[-0.899869, 2.05034], [-0.899869, 2.94966], [0.0, 2.94966], [0.0, 2.05034], [-0.899869, 2.05034]]]), Dict{String,Any}()), Feature(Polygon(Array{Array{Float64,1},1}[[[0.0, 2.05034], [0.0, 2.94966], [0.899869, 2.94966], [0.899869, 2.05034], [0.0, 2.05034]]]), Dict{String,Any}())], nothing, nothing)
```
"""
function square_grid(bbox::Vector{T}, cell_side::Real, mask::Union{Polygon, Nothing}=nothing, units::String="kilometers") where {T <: Real}
    return rectangle_grid(bbox, cell_side, cell_side, mask, units)
end

"""
    triangle_grid(bbox::Vector{T}, cell_side::Real, mask::Union{Polygon, Nothing}=nothing, units::String="kilometers") where {T <: Real}

Take a bounding box and a cell depth and returns a set of triangular Polygons in a grid.

# Examples
```jldoctest
julia> bbox = [-1, 2, 1, 3]
4-element Array{Int64,1}:
 -1
  2
  1
  3

julia> triangle_grid(bbox, 200)
FeatureCollection{Feature}(Feature[Feature(Polygon(Array{Array{Float64,1},1}[[[-1.0, 2.0], [-1.0, 3.79864], [0.799737, 2.0], [-1.0, 2.0]]]), Dict{String,Any}()), Feature(Polygon(Array{Array{Float64,1},1}[[[-1.0, 3.79864], [0.799737, 3.79864], [0.799737, 2.0], [-1.0, 3.79864]]]), Dict{String,Any}()), Feature(Polygon(Array{Array{Float64,1},1}[[[0.799737, 2.0], [0.799737, 3.79864], [2.59947, 3.79864], [0.799737, 2.0]]]), Dict{String,Any}()), Feature(Polygon(Array{Array{Float64,1},1}[[[0.799737, 2.0], [2.59947, 3.79864], [2.59947, 2.0], [0.799737, 2.0]]]), Dict{String,Any}())], nothing, nothing)
```
"""
function triangle_grid(bbox::Vector{T}, cell_side::Real, mask::Union{Polygon, Nothing}=nothing, units::String="kilometers") where {T <: Real}
    results = []

    x_frac = cell_side / (distance(float([bbox[1], bbox[2]]), float([bbox[3], bbox[2]]), units))
    cell_width = x_frac * (bbox[3] - bbox[1])
    y_frac = cell_side / (distance(float([bbox[1], bbox[2]]), float([bbox[1], bbox[4]]), units))
    cell_height = y_frac * (bbox[4] - bbox[2])

    xi = 0
    curr_x = bbox[1]

    while curr_x <= bbox[3]
        yi = 0
        curr_y = bbox[2]

        while curr_y <= bbox[4]
            cell1 = nothing
            cell2 = nothing

            if xi % 2 == 0 && yi % 2 == 0
                cell1 = Polygon([[[curr_x, curr_y], [curr_x, curr_y + cell_height],
                    [curr_x + cell_width, curr_y], [curr_x, curr_y]]])

                cell2 = Polygon([[[curr_x, curr_y + cell_height], [curr_x + cell_width, curr_y + cell_height],
                    [curr_x + cell_width, curr_y], [curr_x, curr_y + cell_height]]])
            elseif xi % 2 == 0 && yi % 2 == 1
                cell1 = Polygon([[[curr_x, curr_y], [curr_x + cell_width, curr_y + cell_height],
                    [curr_x + cell_width, curr_y], [curr_x, curr_y]]])

                cell2 = Polygon([[[curr_x, curr_y], [curr_x, curr_y + cell_height],
                    [curr_x + cell_width, curr_y + cell_height], [curr_x, curr_y]]])

            elseif yi % 2 == 0 && xi % 2 == 1
                cell1 = Polygon([[[curr_x, curr_y], [curr_x, curr_y + cell_height],
                    [curr_x + cell_width, curr_y + cell_height], [curr_x, curr_y]]])

                cell2 = Polygon([[[curr_x, curr_y], [curr_x + cell_width, curr_y + cell_height],
                    [curr_x + cell_width, curr_y], [curr_x, curr_y]]])
            elseif yi % 2 == 1 && xi % 2 == 1
                cell1 = Polygon([[[curr_x, curr_y], [curr_x, curr_y + cell_height],
                    [curr_x + cell_width, curr_y], [curr_x, curr_y]]])

                cell2 = Polygon([[[curr_x, curr_y + cell_height], [curr_x + cell_width, curr_y + cell_height],
                    [curr_x + cell_width, curr_y], [curr_x, curr_y + cell_height]]])
            end

            if mask != nothing
                # intersects...
                push!(results, cell1, cell2)
            else
                push!(results, cell1, cell2)
            end
            curr_y += cell_height
            yi += 1
        end
        xi += 1
        curr_x += cell_width
    end
    return FeatureCollection([Feature(x) for x in results])
end


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

module KernelEstimator
using Distributions
using Optim
using StatsBase
using HCubature
import StatsBase: RealVector, RealMatrix

using Distributions: invsqrt2π, log2π, sqrt2, invsqrt2
using SpecialFunctions

export  kde, kerneldensity, gaussiankernel, ekernel, gammakernel, betakernel,
        localconstant, locallinear, npr, lp0, lp1,
        bwkd, bwnormal, bwlscv, bwlcv, bwlocalconstant, bwlocallinear, bwreg,
#         Sind,
        bootstrapCB
#         BootstrapGoodness

include("kernel.jl")
include("regression.jl")
include("density.jl")
include("bandwidth.jl")
# include("semiparametric.jl")
include("confidenceband.jl")
# include("goodnessoffit.jl")

end


using DataFrames, InteractiveFixedEffectModels,Test, CSV
df = df = CSV.read(joinpath(dirname(pathof(InteractiveFixedEffectModels)), "../dataset/Cigar.csv"))

for method in [:gauss_seidel,  :dogleg, :levenberg_marquardt]
	println(method)
	model = @formula Sales ~ 0 + ife(State, Year, 1)
	result = regife(df, model, method = method, maxiter = 10_000)
	@test abs(result.augmentdf[1, :factors1]) ≈ 0.18662770198472406 atol = 1e-1
	@test abs(result.augmentdf[1, :loadings1]) ≈ 587.2272 atol = 1e-1
	@test result.augmentdf[1, :residuals] ≈ - 15.6928 atol = 1e-1

	model = @formula Sales ~ 0 + ife(State, Year, 2)
	result = regife(df, model, method = method, maxiter = 10_000)
	@test abs(result.augmentdf[1, :factors1])  ≈ 0.18662770198472406 atol = 1e-1
	@test abs(result.augmentdf[1, :loadings1])  ≈ 587.227 atol = 1e-1
	@test result.augmentdf[1, :residuals] ≈ 2.16611 atol = 1e-1

	model = @formula Sales ~ ife(State, Year, 1) + fe(State)
	result = regife(df, model, method = method, maxiter = 10_000)
	@test abs(result.augmentdf[1, :factors1])   ≈ 0.17636 atol = 1e-1
	@test abs(result.augmentdf[1, :loadings1])  ≈ 20.176432452716522 atol = 1e-1
	@test result.augmentdf[1, :residuals]  ≈ - 10.0181 atol = 1e-1
	@test result.augmentdf[1, :fe_State]  ≈ 107.4766 atol = 1e-1

	model = @formula Sales ~ ife(State, Year, 2) + fe(State)
	result = regife(df, model, method = method, maxiter = 10_000)
	@test abs(result.augmentdf[1, :factors2]) ≈ 0.244 atol = 1e-1
	@test abs(result.augmentdf[1, :loadings2]) ≈ 49.7943 atol = 1e-1
	@test result.augmentdf[1, :residuals]  ≈ 2.165319 atol = 1e-1
	@test result.augmentdf[1, :fe_State]  ≈ 107.47666 atol = 1e-1

	model = @formula Sales ~ ife(State, Year, 2) + fe(State)
	result = regife(df, model, subset = df.State .<= 30, method = method, maxiter = 10_000)
	@test size(result.augmentdf, 1) == size(df, 1)
	@test abs(result.augmentdf[1, :factors1])  ≈ 0.20215 atol = 1e-1
	@test abs(result.augmentdf[1, :loadings1]) ≈ 29.546 atol = 1e-1
	@test abs(result.augmentdf[1, :factors2])  ≈ 0.250768  atol = 1e-1
	@test abs(result.augmentdf[1, :loadings2])   ≈ 43.578 atol = 1e-1
	@test result.augmentdf[1, :residuals]  ≈ 2.9448  atol = 1e-1
	@test result.augmentdf[1, :fe_State] ≈ 107.4766 atol = 1e-1
end


##
#
# This is a simple example for travel time calculation in 3D using the Fast Marching approach for the factored eikonal equation on a regular grid.
# See more details in:
# Eran Treister and Eldad Haber, A fast marching algorithm for the factored eikonal equation, Journal of Computational Physics, 324, 210-225, 2016.
#
# Please cite this paper if you use our package.
#
##
using jInv.Mesh
using FactoredEikonalFastMarching

plotting = false;
if plotting
	using PyPlot;
	using jInvVis;
end

#### SETTING THE PARAMETERS OF THE PROBLEM

dim = 3;
n_nodes = 0;
if dim == 2
	n_nodes = [33;33];
	domain = [0.0,1.0,0.0,1.0]; # Domain is in KMs
	src = div(n_nodes,2);
	src[2] = 1;
else
	n_nodes = [33;33;33];
	domain = [0.0,1.0,0.0,1.0,0.0,1.0]; # Domain is in KMs
	src = div(n_nodes,2);
	src[3] = 1;
end
n_tup = tuple(n_nodes...);
mNodal = ones(Float64,n_tup); # m is 1/v^2 whre v is the velocity in km/sec

#### SETTING THE PARAMETERS OF THE FAST MARCHING SOLVER

M = getRegularMesh(domain,n_nodes-1);
mem = getEikonalTempMemory(n_nodes); # here we allocate temporary memory. Reuse this memory for multiple sources on the same mesh.
eikParam = getEikonalParam(M,mNodal,src,true);
solveFastMarchingUpwindGrad(eikParam, mem);
mem = 0; # deallocate memory.
T = copy(eikParam.T1); # here we get the factor tau1.

## Below we extract the true travel time (multiply it wit tau0)
# This function works on the solution vector itself.
# Use getAnalytic2DeikonalSolutionAll and getAnalytic3DeikonalSolutionAll to get the analytical solution itself for a constant velocity model.
if M.dim==2
	selfMultiplyWithAnalyticSolution2D(n_nodes,M.h,src,T);
else
	selfMultiplyWithAnalyticSolution3D(n_nodes,M.h,src,T)
end
# Here, T is the solution of the eikonal equation, the answer is in seconds.

#### PLOTTING THE RESULTS

if plotting
	plotModel(T);
end


using DifferentialEquations
using PyCall
using PyPlot

function aginglaw(v, theta, dc)
    return 1 - theta * v / dc
end

function sliplaw(v, theta, dc)
    return -v * theta / dc * log(v * θ / dc)
end

function calcdvθclassic!(du, u, p, t)
    dc, η, σn, a, b, μ, Vp, L, ρ, statelaw = p
    θ = u[1]
    v = u[2]
    du[1] = statelaw(v, θ, dc)
    du[2] = 1 / (η / σn + a / v) * (μ * (Vp - v) / (L * σn) - b * du[1] / θ)
    return nothing
end

function plottimeseries(sol, siay, titlelabel)
    t1 = [x / siay for x in sol.t]
    θ1 = [x[1] for x in sol.u]
    v1 = [x[2] for x in sol.u]

    figure(figsize = (12, 6))
    subplot(2, 2, 1)
    plot(t1, θ1, "-b", linewidth = 0.5)
    yscale("log")
    ylabel(L"\theta")

    subplot(2, 2, 2)
    plot(1:1:length(t1), θ1, "-b", linewidth = 0.5)
    yscale("log")
    ylabel(L"\theta")

    subplot(2, 2, 3)
    plot(t1, v1, "-b", linewidth = 0.5)
    yscale("log")
    xlabel("time (years)")
    ylabel("v (m/s)")

    subplot(2, 2, 4)
    plot(1:1:length(t1), v1, "-b", linewidth = 0.5)
    yscale("log")
    xlabel("time (step)")
    ylabel("v (m/s)")
    suptitle(titlelabel)

    return nothing
end

function sliding()
    # Model parameters
    siay = 365.25 * 24 * 60 * 60
    tspan = (0.0, siay * 1000.0) # This will run
    # tspan = (0.0, siay * 302.0) # This will run but fail for the a, b case
    μ = 3e10
    ν = 0.25
    ρ = 2700.0
    η = μ / (2.0 * sqrt(μ / ρ))
    L = 60 * 1e3
    a = 0.015
    b = 0.02
    vp = 1e-9
    σn = 30e6
    dc = 0.1
    abstol = 1e-4
    reltol = 1e-4

    # Parameters need for d(a, b σn)/dt
    fstar = 0.6 # ???
    vstar = vp # ???
    θstar = 1e9 # ???

    # Show Tushar scalings for a and b
    # tusharabmodels()

    # Time integrate - classic
    icsclassic = [1e8; vp / 1000]
    pclassic = (dc, η, σn, a, b, μ, vp, L, ρ, aginglaw)
    probclassic = ODEProblem(calcdvθclassic!, icsclassic, tspan, pclassic)
    solclassic = solve(probclassic, RK4(), abstol = abstol, reltol = reltol)
    plottimeseries(solclassic, siay, "RSF (classic)")

    # Time integrate - a, b can evolve
    # icsab = [1e8 ; vp / 1000 ; a ; b]
    # pab = (dc, η, σn, μ, vp, L, ρ, aginglaw, fstar, vstar, θstar)
    # probab = ODEProblem(calcdvθab!, icsab, tspan, pab)
    # solab = solve(probab, RK4(), abstol = abstol, reltol = reltol)
    # plottimeseries(solab, siay, "RSF (ab)")

    return nothing
end
sliding()


import NNlib.σ

# define some functions
loss(x,y) = x - y

function σ(x)
    x = 1+exp.(-x)
    for i = 1:length(x)
        x[i] = 1/x[i]
    end
    return x
end

function σ_derivative(x)
    res = 1-x
    for i = 1:length(x)
        res[i] *= x[i]
    end
    return res
end

function vecprod(a,b)
    if length(a) == length(b)
        res = reshape(zeros(length(a)),1,length(a))
        for i = 1:length(a)
            res[i] = a[i]*b[i]
        end
        return res
    end
end

function print_status(err, label, pred)
    println("Error: ", err)
    println("True: ", label)
    println("Pred: ", pred)
end

# input variables
bin_length = 8
max_num = 2^bin_length

α = 0.1                     # learning rate
input_dim = 2               # two input numbers
hidden_dim = 9              # hidden neurons
output_dim = 1              # the output is sum of the two numbers
set_size = 15000            # size of the training set

# training data
num_x = reshape([convert(Int16,rand(0:max_num/2)) for i = 1:set_size*2], 2, set_size)
num_y = reshape([num_x[1,i] + num_x[2,i] for i = 1:set_size], 1, set_size)

# convert to bin
x, y = [], []
for i = 1:set_size
    a = bin(num_x[1,i],bin_length)
    b = bin(num_x[2,i],bin_length)
    c = bin(num_y[i],bin_length)

    in_bin = zeros(2,bin_length)
    out_bin = zeros(1,bin_length)
    for j = 1:bin_length
        in_bin[1,j] = convert(Float64, a[j])-48
        in_bin[2,j] = convert(Float64, b[j])-48
        out_bin[j] = convert(Float64, c[j])-48
    end
    push!(x,in_bin)
    push!(y,out_bin)
end

# weight matrices
syn_0 = 2*randn(input_dim,hidden_dim)
syn_1 = 2*randn(hidden_dim,output_dim)
syn_r = 2*randn(hidden_dim,hidden_dim)

syn_0_update = zeros(size(syn_0))
syn_1_update = zeros(size(syn_1))
syn_r_update = zeros(size(syn_r))

# training
for i = 1:set_size

    prediction = reshape(zeros(bin_length),1,bin_length)          # result of the net
    overallError = 0

    layer_2_Δs = []
    layer_1_values = []
    push!(layer_1_values, reshape(zeros(hidden_dim),1,hidden_dim))

    # calculate output and error
    for pos = bin_length:-1:1
        # MODEL -->
        # hidden layer (input ~+ prev_hidden)
        layer_1 = σ(x[i][1:end, pos]'*syn_0 + layer_1_values[end]*syn_r)

        # output layer (new binary representation)
        layer_2 = σ(layer_1*syn_1)

        layer_2_error = loss(y[i][pos],layer_2)
        push!(layer_2_Δs, layer_2_error*σ_derivative(layer_2))     # save derivatives for each timestep
        overallError += abs(layer_2_error[1])

        prediction[pos] = round(layer_2[1,1])

        push!(layer_1_values, deepcopy(layer_1))                   # store hidden layer for next timestep
        # MODEL --<
    end

    future_layer_1_Δ = reshape(zeros(hidden_dim),1,hidden_dim)

    # backpropagation
    for pos = 1:bin_length
        layer_1 = layer_1_values[end-pos+1]        # selecting the current hidden layer
        prev_layer_1 = layer_1_values[end-pos]     # selecting the previous hidden layer

        # error at output layer
        layer_2_Δ = layer_2_Δs[end-pos+1]
        # error at hidden layer (given the error at the future hidden layer and the current output error)
        layer_1_Δ = vecprod((future_layer_1_Δ*syn_r' + layer_2_Δ*syn_1'),σ_derivative(layer_1))

        # let's update all our weights so we can try again
        syn_1_update += layer_1'*layer_2_Δ
        syn_r_update += prev_layer_1'*layer_1_Δ
        syn_0_update += x[i][1:end, pos]*layer_1_Δ

        future_layer_1_Δ = layer_1_Δ
    end

    # backpropagation is calculated, now update the weights
    syn_0 += syn_0_update * α
    syn_1 += syn_1_update * α
    syn_r += syn_r_update * α

    syn_0_update *= 0
    syn_1_update *= 0
    syn_r_update *= 0

    if i % (0.1*set_size) == 0
        print_status(overallError, y[i], prediction)
    end
end


import CGinJulia.widemult, CGinJulia.det2, CGinJulia.det3, CGinJulia.det4, CGinJulia.det4ck

## I don't want Float64 to widen to BigFloat, but to Double64
@testset "wide multiplication and DoubleFloats" begin
	f = 1f0; g = 2f0; h = 2f0^22-1
	df = 1.0; dg = 2.0; dh = 2.0^52-1
	@test(typeof(h) == Float32)
	@test(typeof(widemult(f,g))==Float64)
	@test(typeof(widemult(df,dg))==Double64)
	@test(widemult(g,h)+2 == 2.0^23)
	@test(widemult(dg,dh)+2 == 2.0^53)
end
##

@testset "determinants" begin
	e1 = (1,0,0,0); e2 = (0,1,0,0); e3 = (0,0,1,0); e4 = (0,0,0,1);
	r = (1,2,3,5); s = (7,11,13,17); t = (1, 2, 4, 8); u = (1, 10, 100, 1000)
	@test(det2(e1,e2, 1,2) == 1)
	@test(det3(e1,e2,e3, 1,2,3) == 1)
	@test(det4(e1,e2,e3,e4, 1,2,3,4) == 1)
	@test(det2(r,s, 1,2) == -3)
	@test(det3(r,s,t, 1,2,3) == -3)
	@test(det4(r,s,t,u, 1,2,3,4) == -2160)
	@test(det4(r,s,t,u, 1,2,3,4) == det4ck(r,s,t,u, 1,2,3,4))
	@test(det4(r,s,t,u, 1,2,3,4) == det4dbl(r,s,t,u, 1,2,3,4))
end


@testset ExtendedTestSet "squared_euclidean" begin
    @testset ExtendedTestSet "squared_euclidean 1D" begin
        x = boolean_indicator([0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0])
        tfm = SquaredEuclidean(x)
        answer = [1, 0, 1, 4, 1, 0, 0, 0, 0, 0, 1]
        @test transform(x, tfm) == answer
    end

    @testset ExtendedTestSet "squared_euclidean 2D" begin
        x = boolean_indicator(
            [
                0 1 1 1 0
                1 1 1 1 1
                1 0 0 0 1
                1 0 0 0 1
                1 0 0 0 1
                1 1 1 1 1
                0 1 1 1 0
            ]
        )
        tfm = SquaredEuclidean(x)
        answer = [
            1.0 0.0 0.0 0.0 1.0
            0.0 0.0 0.0 0.0 0.0
            0.0 1.0 1.0 1.0 0.0
            0.0 4.0 4.0 4.0 0.0
            0.0 1.0 1.0 1.0 0.0
            0.0 0.0 0.0 0.0 0.0
            1.0 0.0 0.0 0.0 1.0
        ]
        @test transform(x, tfm) == answer
    end

    @testset ExtendedTestSet "squared_euclidean 3D" begin
        x1 = [
                0 1 1 1 0
                1 1 1 1 1
                1 0 0 0 1
                1 0 0 0 1
                1 0 0 0 1
                1 1 1 1 1
                0 1 1 1 0
                ]

        x = cat(x1, x1, dims=3)
        x = boolean_indicator(x)

        tfm = SquaredEuclidean(x)
        a1 = [
            1.0 0.0 0.0 0.0 1.0
            0.0 0.0 0.0 0.0 0.0
            0.0 1.0 1.0 1.0 0.0
            0.0 4.0 4.0 4.0 0.0
            0.0 1.0 1.0 1.0 0.0
            0.0 0.0 0.0 0.0 0.0
            1.0 0.0 0.0 0.0 1.0
        ]
        answer = cat(a1, a1, dims=3)
        @test transform(x, tfm) == answer
    end

    @testset ExtendedTestSet "squared_euclidean threaded" begin
        x = boolean_indicator(
            [
                0 1 1 1 0
                1 1 1 1 1
                1 0 0 0 1
                1 0 0 0 1
                1 0 0 0 1
                1 1 1 1 1
                0 1 1 1 0
            ]
        )
        tfm = SquaredEuclidean(x)
        answer = [
            1.0 0.0 0.0 0.0 1.0
            0.0 0.0 0.0 0.0 0.0
            0.0 1.0 1.0 1.0 0.0
            0.0 4.0 4.0 4.0 0.0
            0.0 1.0 1.0 1.0 0.0
            0.0 0.0 0.0 0.0 0.0
            1.0 0.0 0.0 0.0 1.0
        ]
        @test transform!(x, tfm, Threads.nthreads()) == answer
    end
end


import Statistics
using FastGaussQuadrature

struct TIEstimate
    burn_in::Int
    integration_nodes::Int
    num_samples::Int
end

name(x::TIEstimate) = "TI"

struct ThermodynamicIntegrationResult{V} <: SimulationResult
    integration_weights::Vector{Float64}
    inv_temps::Vector{Float64}
    energies::Array{V,2}
    acceptance::Array{Bool,2}
end

function blocks(result::ThermodynamicIntegrationResult, block_size=2^10)
    block_averages(array) = map(mean, Iterators.partition(array, block_size))
    mapreduce(block_averages, hcat, eachcol(result.energies))
end

# perform the quadrature integral
log_marginal(result::ThermodynamicIntegrationResult{<:Real}) = dot(result.integration_weights, vec(mean(result.energies, dims=1)))
log_marginal(result::ThermodynamicIntegrationResult{<:Vector}) = sum(result.integration_weights .* vec(mean(result.energies, dims=1)))

function Statistics.var(result::ThermodynamicIntegrationResult, block_size=2^10)
    b = blocks(result, block_size)
    σ² = var(b, dims=1) ./ size(b, 1)
    dot(result.integration_weights.^2, σ²)
end

function summary(results::ThermodynamicIntegrationResult...)
    block_size = 2^10
    energy_blocks = [blocks(r, block_size) for r in results]
    acceptance = [mean(r.acceptance, dims=1) for r in results]
    inv_temps = [r.inv_temps for r in results]
    integration_weights = [r.integration_weights for r in results]

    DataFrame(EnergyBlocks=energy_blocks, Acceptance=acceptance, Theta=inv_temps, IntegrationWeights=integration_weights)
end

# Monte-Carlo computation of the marginal probability for the given configuration
function simulate(algorithm::TIEstimate, initial, system; kwargs...)
    # Generate the array of θ values for which we want to simulate the system.
    # We use Gauss-Legendre quadrature which predetermines the choice of θ.
    nodes, weights = gausslegendre(algorithm.integration_nodes)
    θrange = 0.5 .* nodes .+ 0.5
    # The factor 0.5 comes from rescaling the integration limits from [-1,1] to [0,1].
    weights = 0.5 .* weights

    initial_energy = energy_difference(initial, system)
    energies = fill(initial_energy, algorithm.num_samples, length(θrange))
    accept = Array{Bool}(undef, algorithm.num_samples, length(θrange))
    for i in eachindex(θrange)
        chn = chain(system; θ=θrange[i], kwargs...)
        sampler = MetropolisSampler(algorithm.burn_in, 0, energy(initial, chn), copy(initial), chn)
        for (j, was_accepted) in Iterators.enumerate(Iterators.take(sampler, algorithm.num_samples))
            energies[j, i] = -energy_difference(sampler.state, system)
            accept[j, i] = was_accepted != 0
        end
        initial = sampler.state
    end

    ThermodynamicIntegrationResult(weights, θrange, energies, accept)
end


# need to check the arities
# units handling??
applymap = Dict{String,Function}(
    # eq sometimes needs to be ~ and sometimes needs to be =, not sure what the soln is
    "eq" => x -> Symbolics.:~(x...), # arity 2,
    "times" => Base.prod, # arity 2, but prod fine
    # "prod" => Base.prod,
    "divide" => x -> Base.:/(x...),
    "power" => x -> Base.:^(x...),
    "root" => custom_root,
    "plus" => x -> Base.:+(x...),
    "minus" => x -> Base.:-(x...),

    # comparison functions implemented using the Heaviside function
    "lt" => x -> H(x[2] - x[1] - ϵ),
    "leq" => x -> H(x[2] - x[1]),
    "geq" => x -> H(x[1] - x[2]),
    "gt" => x -> H(x[1] - x[2] - ϵ),

    # equal nodes are not part of the MathML specification and
    # are generated by disambiguate_equality!
    "equal" => x -> H(x[1] - x[2]) * H(x[2] - x[1]),
    "neq" => x -> 1 - H(x[1] - x[2]) * H(x[2] - x[1]),

    # "lt" => x -> Base.foldl(Base.:<, x),
    # "leq" => x -> Base.foldl(Base.:≤, x),
    # "geq" => x -> Base.foldl(Base.:≥, x),
    # "gt" => x -> Base.foldl(Base.:>, x),

    # "quotient" => x->Base.:div(x...), # broken, RoundingMode
    "factorial" => x -> Base.factorial(x...),
    "max" => x -> Base.max(x...),
    "min" => x -> Base.min(x...),
    "rem" => x -> Base.:rem(x...),
    "gcd" => x -> Base.:gcd(x...),

    "and" => x -> Base.:*(x...),
    "or" => heaviside_or,
    "xor" => x -> x[1]*(one(x[2])-x[2]) + (one(x[1])-x[1])*x[2],
    "not" => x -> one(x[1]) - x[1],

    # "and" => x -> Base.:&(x...),
    # "or" => Base.:|,
    # "xor" => Base.:⊻,
    # "not" => Base.:!,

    "abs" => x -> Base.abs(x...),
    "conjugate" => Base.conj,
    "arg" => Base.angle,
    "real" => Base.real,
    "imaginary" => Base.imag,
    "lcm" =>  x -> Base.lcm(x...),

    "floor" => x -> x[1] - frac(x[1]),
    "ceiling" => x -> x[1] - frac(x[1]) + one(x[1]),
    "round" => x -> x[1] + 0.5 - frac(x[1] + 0.5),

    # "floor" => x -> Base.floor(x...),
    # "ceiling" => x -> Base.ceil(x...),
    # "round" => x -> Base.round(x...),

    "inverse" => Base.inv,
    "compose" => x -> Base.:∘(x...),
    "ident" => Base.identity,
    "approx" => x -> Base.:≈(x...),

    "sin" => x -> Base.sin(x...),
    "cos" => x -> Base.cos(x...),
    "tan" => x -> Base.tan(x...),
    "sec" => x -> Base.sec(x...),
    "csc" => x -> Base.csc(x...),
    "cot" => x -> Base.cot(x...),
    "arcsin" => x -> Base.asin(x...),
    "arccos" => x -> Base.acos(x...),
    "arctan" => x -> Base.atan(x...),
    "arcsec" => x -> Base.asec(x...),
    "arccsc" => x -> Base.acsc(x...),
    "arccot" => x -> Base.acot(x...),
    "sinh" => x -> Base.sinh(x...),
    "cosh" => x -> Base.cosh(x...),
    "tanh" => x -> Base.tanh(x...),
    "sech" => x -> Base.sech(x...),
    "csch" => x -> Base.csch(x...),
    "coth" => x -> Base.coth(x...),
    "arcsinh" => x -> Base.asinh(x...),
    "arccosh" => x -> Base.acosh(x...),
    "arctanh" => x -> Base.atanh(x...),
    "arcsech" => x -> Base.asech(x...),
    "arccsch" => x -> Base.acsch(x...),
    "arccoth" => x -> Base.acoth(x...),

    "exp" => x -> Base.exp(x...),
    "log" => x -> Base.log10(x...), #  todo handle <logbase>
    "ln" => x -> Base.log(x...),

    "mean" => Statistics.mean,
    "sdev" => Statistics.std,
    "variance" => Statistics.var,
    "median" => Statistics.median,
    # "mode" => Statistics.mode, # crazy mode isn't in Base

    "vector" => Base.identity,
    "diff" => parse_diff,
    "cn" => parse_diff,
    # "apply" => x -> parse_apply(x) # this wont work because we pass the name which is string
)

tagmap = Dict{String,Function}(
    "cn" => parse_cn,
    "ci" => parse_ci,

    "degree" => x -> parse_node(x.firstelement), # won't work for all cases
    "bvar" => parse_bvar, # won't work for all cases

    "piecewise" => parse_piecewise,

    "apply" => parse_apply,
    "math" => x -> map(parse_node, elements(x)),
    "vector" => x -> map(parse_node, elements(x)),
    "lambda" => parse_lambda,

    # MathML defined constants
    "pi" => x -> π,
    "exponentiale" => x -> ℯ,
    "notanumber" => x -> NaN,
    "infinity" => x -> Inf,
    "true" => x -> true,
    "false" => x -> false,
)