# MIT license
# Copyright (c) Microsoft Corporation. All rights reserved.
# See LICENSE in the project root for full license information.

"""
    AcceleratedParametricSurface{T,N,S} <: ParametricSurface{T,N}

Wrapper class for [`ParametricSurface`](@ref)s where analytical intersection isn't feasible (e.g. [`ZernikeSurface`](@ref), [`ChebyshevSurface`](@ref)).
The surface is instead triangulated and an iterative (newton raphson) process carried out to determine precise ray intersection points.
`S` is the type of the ParametricSurface being wrapped.

```julia
AcceleratedParametricSurface(surf::ParametricSurface{T,N}, numsamples::Int = 17; interface::NullOrFresnel{T} = nullinterface(T))
```
"""
struct AcceleratedParametricSurface{T,N,S<:ParametricSurface{T,N}} <: ParametricSurface{T,N}
    surface::S
    triangles::Vector{Triangle{T}}
    sidelengths::Vector{T}
    triangles_bbox::BoundingBox{T}
    interface::NullOrFresnel{T}

    function AcceleratedParametricSurface(surf::S, numsamples::Int = 17; interface::NullOrFresnel{T} = NullInterface(T)) where {T<:Real,N,S<:ParametricSurface{T,N}}
        a = AcceleratedParametricSurface(surf, triangulate(surf, numsamples, true, true), interface = interface)
        emptytrianglepool!(T)
        return a
    end

    function AcceleratedParametricSurface(surf::S, triangles::Vector{Triangle{T}}; interface::NullOrFresnel{T} = NullInterface(T)) where {T<:Real,N,S<:ParametricSurface{T,N}}
        sidelengths = Vector{T}(undef, length(triangles))
        @inbounds @simd for i in 1:length(triangles)
            t = triangles[i]
            sidelengths[i] = max(norm(t.BA), norm(t.CA))
        end
        return new{T,N,S}(surf, copy(triangles), sidelengths, BoundingBox(triangles), interface)
    end
end
export AcceleratedParametricSurface

uvrange(::Type{AcceleratedParametricSurface{T,N,S}}) where {T<:Real,N,S<:ParametricSurface{T,N}} = uvrange(S)
uvrange(surf::AcceleratedParametricSurface{T,N,S}) where {T<:Real,N,S<:ParametricSurface{T,N}} = uvrange(surf.surface)
point(surf::AcceleratedParametricSurface{T,N,S}, u::T, v::T) where {T<:Real,N,S<:ParametricSurface{T,N}} = point(surf.surface, u, v)
partials(surf::AcceleratedParametricSurface{T,N,S}, u::T, v::T) where {T<:Real,N,S<:ParametricSurface{T,N}} = partials(surf.surface, u, v)
normal(surf::AcceleratedParametricSurface{T,N,S}, u::T, v::T) where {T<:Real,N,S<:ParametricSurface{T,N}} = normal(surf.surface, u, v)
inside(surf::AcceleratedParametricSurface{T,N,S}, x::T, y::T, z::T) where {T<:Real,N,S<:ParametricSurface{T,N}} = inside(surf.surface, x, y, z)
onsurface(surf::AcceleratedParametricSurface{T,N,S}, x::T, y::T, z::T) where {T<:Real,N,S<:ParametricSurface{T,N}} = onsurface(surf.surface, x, y, z)
"""
    interface(surf::Surface{T}) -> OpticalInterface{T}

Return the [`OpticalInterface`](@ref) associated with `surf`.
"""
interface(a::AcceleratedParametricSurface{T}) where {T<:Real} = a.interface
uv(surf::AcceleratedParametricSurface{T,3}, x::T, y::T, z::T) where {T<:Real} = uv(surf.surface, x, y, z)

function BoundingBox(surf::AcceleratedParametricSurface{T,3}) where {T<:Real}
    # the stored bbox is a tight one around the triangulated surface only, in reality the half-space created is an infinite prism capped by the triangulated surface
    # this bounding box is not accurate to the actual surface, but it is accurate to the triangulated surface
    # if a ray hits the accurate bounding box but not the triangulated bounding box then it would miss the triangulated surface
    # anyway (erroneously) and not return an intersection - so we don't lose anything by making this approximation here
    tbox = surf.triangles_bbox
    return BoundingBox(tbox.xmin, tbox.xmax, tbox.ymin, tbox.ymax, typemin(T), tbox.zmax)
end

# function makemesh(surface::AcceleratedParametricSurface{S,N,T}, ::Int) where {S<:Real,N,T<:ParametricSurface{S,N}}
#     # If we've already triangulated the surface then use this directly as it will be the most
#     # accurate representation of the surface being intersected with
#     return TriangleMesh(surface.triangles)
# end

# "computes all the intersections of the ray with the surface. Each intersection is represented as an Interval that encloses the interior of the object. If the ray is entering the surface, dot(ray,normal) < 0, then the interval is (intersection,infinity) where α is the parametric value of the line surface intersection. If the ray is leaving the surface, dot(ray,normal) > 0, then the interval is (rayorigin, α). If there are two intersections with the surface then returns an Interval(intersection,intersection). If there are multiple segments enclosing the object then will return a list that will contain some combination of the above types of Interval."
# fallback method for any accelerated surface which doesn't have an overridden method

"""
    surfaceintersection(surf::Surface{T}, r::AbstractRay{T}) where {T}

Calculates the intersection of `r` with a surface of any type, `surf`.
Note that some surfaces cannot be intersected analytically so must be wrapped in an [`AcceleratedParametricSurface`](@ref) in order to be intersected.

Returns an [`EmptyInterval`](@ref) if there is no [`Intersection`](@ref), an [`Interval`](@ref) if there is one or two intersections and a [`DisjointUnion`](@ref) if there are more than two intersections.
"""
function surfaceintersection(surf::AcceleratedParametricSurface{T,N}, r::AbstractRay{T,N}) where {T,N}
    if doesintersect(surf.triangles_bbox, r)
        i = triangulatedintersection(surf, r)
        if i isa EmptyInterval{T}
            if inside(surf, origin(r))
                return rayorigininterval(Infinity(T))
            else
                return EmptyInterval(T)
            end
        else
            return i
        end
    else
        if inside(surf, origin(r))
            return rayorigininterval(Infinity(T))
        else
            return EmptyInterval(T)
        end
    end
end

"""
    triangulatedintersection(surf::AcceleratedParametricSurface{T,N,S}, r::AbstractRay{T,N})

Intersection of a ray, `r`, with a triangulated surface, `surf`, no concept of inside so never returns a [`RayOrigin`](@ref) [`Interval`](@ref).
"""
function triangulatedintersection(surf::AcceleratedParametricSurface{T,N,S}, r::AbstractRay{T,N}) where {T<:Real,N,S}
    result = newinintervalpool!(T)
    offset_ray = Ray(origin(r) - ACCEL_SURF_RAY_OFFSET * direction(r), direction(r))
    for (i, tri) in enumerate(surf.triangles)
        # test if the ray is within the maximum side length from one of the points, if not then skip it
        w = vertex(tri, 1) - origin(r)
        dist = norm(w - dot(w, direction(r)) * direction(r))
        if dist > surf.sidelengths[i]
            continue
        end
        # find the triangle intersection - it's quite possible that for rays starting near the surface we will we miss
        # an intersection because of imprecise triangulation, so introduce an offset
        intv = surfaceintersection(tri, offset_ray)
        if !(intv isa EmptyInterval{T})
            ints = halfspaceintersection(intv)
            if α(ints) <= ACCEL_SURF_RAY_OFFSET
                # skip any intersection behind the true start point (with some wiggle room)
                continue
            end
            let dup = false
                # check if a point with the same alpha had already been included, if so ignore this one
                # doing this check now avoids executing newton many times unnecessarily
                for eintv in result
                    eints = halfspaceintersection(eintv)
                    if samepoint(α(eints), α(ints) + ACCEL_SURF_RAY_OFFSET)
                        dup = true
                        break
                    end
                end
                if !dup
                    # evaluate the precise point of the intersection with the surface
                    intuv = newton(surf.surface, r, uv(ints))
                    # check that the evaluated uv point lies on the (correct side of the) ray
                    if intuv !== nothing
                        u, v = intuv
                        surfpt = point(surf, u, v)
                        t = α(r, surfpt) # this is the 'true' α again (i.e. not offset)
                        if t > zero(T)
                            # check again that this point hasn't already been included (α might be slightly different, i.e. more accurate, now)
                            for eintv in result
                                eints = halfspaceintersection(eintv)
                                if samepoint(α(eints), t)
                                    dup = true
                                    break
                                end
                            end
                            if !dup && samepoint(point(r, t), surfpt)
                                # intersection is good so calc the normal and add to list
                                surfnormal = normal(surf, u, v)
                                intsct = Intersection(t, surfpt, surfnormal, u, v, interface(surf))
                                if dot(direction(r), surfnormal) < zero(T)
                                    intvl = positivehalfspace(intsct)
                                else
                                    intvl = rayorigininterval(intsct)
                                end
                                push!(result, intvl)
                            end
                        end
                    end
                end
            end
        end
    end

    nr = length(result)
    if nr === 0
        return EmptyInterval(T)
    elseif nr === 1
        return result[1]
    end

    # sort!(result, lt = (a, b) -> α(halfspaceintersection(a)) < α(halfspaceintersection(b))) #this will sort the intervals by α
    # alloc free sort of result
    @inbounds for i in 2:nr
        value = result[i]
        j = i - 1
        while j > 0 && α(halfspaceintersection(result[j])) > α(halfspaceintersection(value))
            result[j + 1] = result[j]
            j = j - 1
        end
        result[j + 1] = value
    end

    let start = 1
        temp = newinintervalpool!(T)

        if lower(result[1]) isa RayOrigin{T}
            # Starting "inside" the surface, i.e., n̂⋅d̂, > 0.
            # Can't truly define inside for Bezier patches because they don't define a half-space.
            start = 2
            push!(temp, result[1])
        end

        @inbounds for i in start:2:(nr - 1)
            intvlintsct = intervalintersection(result[i], result[i + 1])
            if !(intvlintsct isa EmptyInterval{T})
                push!(temp, intvlintsct)
            else
                # this can happen in some edge cases, seems like the best thing to do is juse ignore it...
                @warn "Funny behavior in triangulated intersection" maxlog = 1
                # throw(ErrorException("Triangulated intersection error - this should never happen."))
            end
        end

        if mod(nr - (start - 1), 2) === 1 #one unpaired interval
            push!(temp, last(result))
        end

        return DisjointUnion(temp)
    end
end

"""
    jacobian(surf::ParametricSurface{T,N}, u::T, v::T, P1::SVector{M,T}, P2::SVector{M,T})

Computes Jacobian of `f(t,u,v) = ( dot(P1,[surf(u,v),1],P2,[surf(u,v),1]) )`.
`P1`, `P2` are orthogonal planes that pass through the ray.
`J = [ ∂f1/∂u ∂f1/∂v ; ∂f2/∂u ∂f2/∂v]`
"""
function jacobian(surf::ParametricSurface{T,N}, u::T, v::T, P1::SVector{M,T}, P2::SVector{M,T}) where {T<:Real,N,M}
    (du, dv) = partials(surf, u, v)
    return SMatrix{2,2,T}(dot(view(P1, 1:(M - 1)), du), dot(view(P2, 1:(M - 1)), du), dot(view(P1, 1:(M - 1)), dv), dot(view(P2, 1:(M - 1)), dv))
end

"""
    newton(surf::ParametricSurface{T,N}, r::AbstractRay{T,N}, startingpoint::SVector{2,T})

Newton iteration to find the precise intersection of a parametric surface with a ray given a starting point (in uv space) on the surface.
"""
function newton(surf::ParametricSurface{T,N}, r::AbstractRay{T,N}, startingpoint::SVector{2,T}) where {T,N}
    tolerance = 1e-12
    maxiterations = 6

    urange, vrange = uvrange(surf)

    dx, dy, dz = direction(r)
    if abs(dx) > abs(dy) && abs(dx) > abs(dz)
        N1 = SVector{3,T}(dy, -dx, zero(T))
    else
        N1 = SVector{3,T}(zero(T), dz, -dy)
    end

    N2 = cross(N1, direction(r))
    P1 = SVector{4,T}(N1[1], N1[2], N1[3], -dot(N1, origin(r)))
    P2 = SVector{4,T}(N2[1], N2[2], N2[3], -dot(N2, origin(r)))

    @inline function f(uv::SVector{2,T})::SVector{2,T}
        psurf = point(surf, uv[1], uv[2])
        res1 = zero(T)
        res2 = zero(T)
        @inbounds for i in 1:3
            res1 += P1[i] * psurf[i]
            res2 += P2[i] * psurf[i]
        end
        res1 += P1[4]
        res2 += P2[4]
        # return [dot(P1, vcat(psurf, 1)), dot(P2, vcat(psurf, 1))]
        return SVector{2,T}(res1, res2)
    end

    xilast = startingpoint
    xi = zeros(SVector{2,T})
    error = typemax(T)
    i = 0
    finalpass = false

    while i < maxiterations
        if any(isnan.(xilast))
            # if something is NaN then just return the starting point and hope that the surface point matches
            xilast = startingpoint
            error = zero(T)
            break
        end

        j = jacobian(surf, xilast[1], xilast[2], P1, P2) # xi has values u,v

        if det(j) == zero(T)
            # if the jacobian is singular then break at the current iteration and return whatever we have
            error = zero(T)
            break
        end

        lstf = f(xilast)
        xi = xilast - (j \ lstf)
        error = sum(abs.(xi - xilast))

        if !finalpass && error < tolerance
            i = maxiterations - 3 # iterate a few more times after the xi and xi-1 are the same value, at least to 64 bit precision. Seems to give more accurate solutions. Perhaps because of higher precision for intermediate results?
            finalpass = true
        end

        i += 1

        xilast = xi
    end

    if error > tolerance
        return nothing
    else
        if xi[1] < urange[1] || xi[1] > urange[2] || xi[2] < vrange[1] || xi[2] > vrange[2]
            return nothing
        else
            return xilast
        end
    end
end

# Only show the underlying Bezier surface control points, not the approximating triangle mesh which is generally huge
Base.show(io::IO, a::AcceleratedParametricSurface{S,N,T}) where {S<:Real,N,T<:ParametricSurface{S,N}} = println(io, "Accelerated$(string(a.surface))")


# This file is a part of BAT.jl, licensed under the MIT License (MIT).

"""
    abstract type AbstractVariateTransform <: Function

Abstract type for change-of-variables transformations.

Subtypes (e.g. `SomeTrafo <: AbstractVariateTransform`) must support (with
`trafo::SomeTrafo`):

```julia
    (trafo)(v_prev::SomeVariate) == v_new
    (trafo)(v_prev::SomeVariate, ladj_prev::Real)) == (v = v_new, ladj = ladj_new)
    (trafo)(s_prev::DensitySample)::DensitySample
    ((trafo2 ∘ trafo1)(v)::AbstractVariateTransform)(v) == trafo2(trafo1(v))
    inv(trafo)(trafo(v)) == v
    inv(inv(trafo)) == trafo

    ValueShapes.varshape(trafo)::ValueShapes.AbstractValueShape
```

with `varshape(v_prev) == varshape(trafo)`.

`ladj` must be `logabsdet(jacobian(trafo, v))`.
"""
abstract type AbstractVariateTransform end
export AbstractVariateTransform

"""
    ladjof(r::NamedTuple{(...,:ladj,...)})::Real

Extract the `log(abs(det(jacobian)))` value that is part of a result `r`.

Examples:

```julia
ladjof((..., ladj = some_ladj, ...)) == some_ladj
ladjof(trafo)(v) = trafo(v, )
```
"""
function ladjof end
export ladjof

ladjof(x::NamedTuple) = x.ladj

struct LADJOfVarTrafo{T<:AbstractVariateTransform} <: Function
    trafo::T
end

(ladjof_trafo::LADJOfVarTrafo)(v::Any) = ladjof(trafo(v, 1))
(ladjof_trafo::LADJOfVarTrafo)(v::Any, prev_ladj::Real) = ladjof(trafo(v, prev_ladj))

"""
    ladjof(trafo::AbstractVariateTransform)::Function

Returns a function that computes the `log(abs(det(jacobian)))` of `trafo` for
a given variate `v`:

```julia
    ladjof(trafo)(v) == ladjof(trafo(v, 1))
    ladjof(trafo)(v, prev_ladj) == ladjof(trafo(v, prev_ladj))
```
"""
ladjof(trafo::AbstractVariateTransform) = LADJOfVarTrafo(trafo)

"""
    abstract type VariateSpace <: Function

*BAT-internal, not part of stable public API.*

Abstract type for variate spaces.
"""
abstract type VariateSpace end

struct UnitSpace <: VariateSpace end
struct InfiniteSpace <: VariateSpace end
struct MixedSpace <: VariateSpace end

product_varspace(s::VariateSpace) = s

product_varspace(::UnitSpace, ::UnitSpace) = MixedSpace
product_varspace(::InfiniteSpace, ::InfiniteSpace) = MixedSpace
product_varspace(::VariateSpace, ::VariateSpace) = MixedSpace

function product_varspace(s1::VariateSpace, s2::VariateSpace, spcs::VariateSpace...)
    product_varspace(product_varspace(s1, s2), spcs...)
end

"""
    abstract type VariateTransform{VF:<VariateForm,ST<:VariateSpace,SF<:VariateSpace}

*BAT-internal, not part of stable public API.*

Abstract parameterized type for change-of-variables transformations.

Subtypes (e.g. `SomeTrafo <: VariateTransform`) must implement:

* `BAT.target_space(trafo::SomeTrafo, v)`
* `BAT.source_space(trafo::SomeTrafo, v)`
* `BAT.apply_vartrafo_impl(trafo::SomeTrafo, v)`
* `BAT.apply_vartrafo_impl(inv_trafo::InverseVT{SomeTrafo}, v)`
* `ValueShapes.varshape(trafo::SomeTrafo)`

for real values and/or real-valued vectors `v`.
"""
abstract type VariateTransform{
    VF<:VariateForm,ST<:VariateSpace,SF<:VariateSpace
} <: AbstractVariateTransform end

function target_space end

function source_space end

function apply_vartrafo end

function apply_vartrafo_impl end

apply_vartrafo(trafo::VariateTransform{Univariate}, v::Real, prev_ladj::Real) =
    apply_vartrafo_impl(trafo, v, prev_ladj)

function apply_vartrafo(trafo::VariateTransform{Univariate}, v::AbstractArray{<:Real,0}, prev_ladj::Real)
    r = apply_vartrafo_impl(trafo, v[], prev_ladj)
    (v = fill(r.v), ladj = r.ladj)
end

apply_vartrafo(trafo::VariateTransform{Multivariate}, v::AbstractVector{<:Real}, prev_ladj::Real) =
    apply_vartrafo_impl(trafo, v, prev_ladj)

apply_vartrafo(trafo::VariateTransform{Matrixvariate}, v::AbstractMatrix{<:Real}, prev_ladj::Real) =
    apply_vartrafo_impl(trafo, v, prev_ladj)

apply_vartrafo(trafo::VariateTransform{ValueShapes.NamedTupleVariate{names}}, v::NamedTuple{names}, prev_ladj::Real) where names =
    apply_vartrafo_impl(trafo, v, prev_ladj)

apply_vartrafo(trafo::VariateTransform{ValueShapes.NamedTupleVariate{names}}, v::ShapedAsNT{<:NamedTuple{names}}, prev_ladj::Real) where names =
    apply_vartrafo_impl(trafo, v, prev_ladj)

(trafo::VariateTransform)(v::Any) = apply_vartrafo(trafo, v, Float32(NaN)).v
(trafo::VariateTransform)(v::Any, prev_ladj::Real) = apply_vartrafo(trafo, v, prev_ladj)

# ToDo: Move to ValueShapes.jl:
_variate_form(shape::ScalarShape{<:Real}) = Univariate
_variate_form(shape::ArrayShape{<:Real,1}) = Multivariate
_variate_form(shape::ArrayShape{<:Real,2}) = Matrixvariate
_variate_form(shape::NamedTupleShape{names}) where names = ValueShapes.NamedTupleVariate{names}

function var_trafo_result(trg_v::Real, src_v::Real, trafo_ladj::Real, prev_ladj::Real)
    R = float(typeof(src_v))
    ladj_sum = convert(R, trafo_ladj + prev_ladj)
    trg_ladj = if !isnan(ladj_sum)
        ladj_sum
    else
        # Should be safe to assume that target dist goes to zero at infinity, should win out over infinite prev_ladj:
        ladjs_should_cancel = (trafo_ladj == R(-Inf) && prev_ladj == R(+Inf) && isinf(trg_v))
        ladjs_should_cancel ? zero(R) : ladj_sum
    end
    (v = convert(R, trg_v), ladj = trg_ladj)
end

function var_trafo_result(trg_v::Real, src_v::Real)
    R = float(typeof(src_v))
    (v = convert(R, trg_v), ladj = convert(R, NaN))
end

function var_trafo_result(trg_v::AbstractVector{<:Real}, src_v::AbstractVector{<:Real}, trafo_ladj::Real, prev_ladj::Real)
    R = float(eltype(src_v))
    ladj_sum = convert(R, trafo_ladj + prev_ladj)
    trg_ladj = if !isnan(ladj_sum)
        ladj_sum
    else
        # Should be safe to assume that target dist goes to zero at infinity, should win out over infinite prev_ladj:
        ladjs_should_cancel = (trafo_ladj == R(-Inf) && prev_ladj == R(+Inf) && any(isinf, trg_v))
        ladjs_should_cancel ? zero(R) : ladj_sum
    end
    (v = convert_eltype(R, trg_v), ladj = trg_ladj)
end

function var_trafo_result(trg_v::AbstractVector{<:Real}, src_v::AbstractVector{<:Real})
    R = float(eltype(src_v))
    (v = convert_eltype(R, trg_v), ladj = convert(R, NaN))
end

function (trafo::VariateTransform)(s::DensitySample)
    r = trafo(s.v, zero(Float32))
    v = stripscalar(r.v)  # ToDo: Do we want to use stripscalar here?
    logd = s.logd - r.ladj
    DensitySample(v, logd, s.weight, s.info, s.aux)
end

# Custom broadcast(::VariateTransform, DensitySampleVector), multithreaded:
function Base.copy(
    instance::Base.Broadcast.Broadcasted{
        <:Base.Broadcast.AbstractArrayStyle{1},
        <:Any,
        <:VariateTransform,
        <:Tuple{DensitySampleVector}
    }
)
    trafo = instance.f
    s_src = instance.args[1]
    vs_trg = valshape(trafo(first(s_src.v)))
    s_src_us = unshaped.(s_src)

    n = length(eachindex(s_src_us))
    s_trg_unshaped = DensitySampleVector((
        nestedview(similar(flatview(s_src_us.v), totalndof(vs_trg), n)),
        zero(s_src_us.logd),
        deepcopy(s_src_us.weight),
        deepcopy(s_src_us.info),
        deepcopy(s_src_us.aux),
    ))
    @assert axes(s_trg_unshaped) == axes(s_src)
    @assert s_trg_unshaped.v isa ArrayOfSimilarArrays
    @threads for i in eachindex(s_trg_unshaped, s_src)
        r = trafo(s_src.v[i], zero(Float32))
        s_trg_unshaped.v[i] .= unshaped(r.v)
        s_trg_unshaped.logd[i] -= r.ladj
    end
    vs_trg.(s_trg_unshaped)
end

struct InvVT{
    VF <: VariateForm,
    ST <: VariateSpace,
    SF <: VariateSpace,
    FT <: VariateTransform{VF,ST,SF}
} <: VariateTransform{VF,SF,ST}
    orig::FT
end

Base.inv(trafo::VariateTransform) = InvVT(trafo)
Base.inv(trafo::InvVT) = trafo.orig

ValueShapes.varshape(trafo::InvVT) = varshape(trafo.orig)

target_space(trafo::InvVT) = source_space(trafo.orig)

source_space(trafo::InvVT) = target_space(trafo.orig)

const InverseVT{FT} = InvVT{VF,ST,SF,FT} where {VF,ST,SF}

struct IdentityVT{
    VF <: VariateForm,
    S <: VariateSpace,
    VS <: AbstractValueShape
} <: VariateTransform{VF,S,S}
    space::S
    varshape::VS
end

function IdentityVT(space::VariateSpace, shape::AbstractValueShape)
    VF = _variate_form(shape)
    S = typeof(space)
    VS = typeof(shape)
    IdentityVT{VF,S,VS}(space, shape)
end

Base.inv(trafo::IdentityVT) = trafo

ValueShapes.varshape(trafo::IdentityVT) = trafo.varshape

function apply_vartrafo_impl(trafo::Union{IdentityVT,InvVT{<:IdentityVT}}, v::Any, prev_ladj::Real)
    (v = v, ladj = prev_ladj)
end

(trafo::Union{IdentityVT,InvVT{<:IdentityVT}})(s::DensitySample) = s

# Custom broadcast(::IdentityVT, DensitySampleVector), multithreaded:
function Base.copy(
    instance::Base.Broadcast.Broadcasted{
        <:Base.Broadcast.AbstractArrayStyle{1},
        <:Any,
        <:Union{IdentityVT,InvVT{<:IdentityVT}},
        <:Tuple{DensitySampleVector}
    }
)
    instance.args[1]
end


#=

Proposal functions for joint
Biogeographic competition model

Ignacio Quintero Mächler

t(-_-t)

May 16 2017

=#

#=
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
`Y` IID proposal functions
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
=#

"""
    upnode!(λ1     ::Float64,
            λ0     ::Float64,
            triad  ::Array{Int64,1},
            Y      ::Array{Int64,3},
            stemevs::Array{Array{Float64,1},1},
            bridx_a::Array{Array{UnitRange{Int64},1},1},
            brδt   ::Vector{Vector{Float64}},
            brl    ::Vector{Float64},
            brs    ::Array{Int64,3},
            narea  ::Int64,
            nedge  ::Int64)

Update node and incident branches using discrete
Data Augmentation for all areas using a non-competitive
mutual-independence Markov model.
"""
function upnode!(λ1     ::Float64,
                 λ0     ::Float64,
                 triad  ::Array{Int64,1},
                 Y      ::Array{Int64,3},
                 stemevs::Array{Array{Float64,1},1},
                 bridx_a::Array{Array{UnitRange{Int64},1},1},
                 brδt   ::Vector{Vector{Float64}},
                 brl    ::Vector{Float64},
                 brs    ::Array{Int64,3},
                 narea  ::Int64,
                 nedge  ::Int64)

  @inbounds begin

    # define branch triad
    pr, d1, d2 = triad

    # sample
    samplenode!(λ1, λ0, pr, d1, d2, brs, brl, narea)

    # save extinct
    ntries = 1
    while iszero(sum(view(brs,pr,2,:)))
      samplenode!(λ1, λ0, pr, d1, d2, brs, brl, narea)

      ntries += 1
      if ntries == 500
        return false
      end
    end

    # set new node in Y
    @simd for k in Base.OneTo(narea)
      Y[bridx_a[k][d1][1]] = Y[bridx_a[k][d2][1]] = brs[pr,2,k]
    end

    # sample a consistent history
    createhists!(λ1, λ0, Y, pr, d1, d2, brs, brδt, bridx_a, narea, nedge,
                 stemevs, brl[nedge])

    ntries = 1
    while ifextY(Y, stemevs, triad, brs, brl[nedge], narea, bridx_a, nedge)
      createhists!(λ1, λ0, Y, pr, d1, d2, brs, brδt, bridx_a, narea, nedge,
                   stemevs, brl[nedge])

      ntries += 1
      if ntries == 500
        return false
      end
    end

  end

  return true
end

"""
    samplenode!(λ1   ::Float64,
                λ0   ::Float64,
                pr   ::Int64,
                d1   ::Int64,
                d2   ::Int64,
                brs  ::Array{Int64,3},
                brl  ::Array{Float64,1},
                narea::Int64)

Sample one internal node according to
mutual-independence model transition probabilities.
"""
function samplenode!(λ1   ::Float64,
                     λ0   ::Float64,
                     pr   ::Int64,
                     d1   ::Int64,
                     d2   ::Int64,
                     brs  ::Array{Int64,3},
                     brl  ::Array{Float64,1},
                     narea::Int64)
  @inbounds begin

    # estimate transition probabilities
    pr0_1, pr0_2 = Ptrfast_start(λ1, λ0, brl[pr], Val{0})
    pr1_1, pr1_2 = Ptrfast_start(λ1, λ0, brl[pr], Val{1})
    d10_1, d10_2 = Ptrfast_end(  λ1, λ0, brl[d1], Val{0})
    d11_1, d11_2 = Ptrfast_end(  λ1, λ0, brl[d1], Val{1})
    d20_1, d20_2 = Ptrfast_end(  λ1, λ0, brl[d2], Val{0})
    d21_1, d21_2 = Ptrfast_end(  λ1, λ0, brl[d2], Val{1})

    for k = Base.OneTo(narea)

      if iszero(brs[pr,1,k])
        ppr_1, ppr_2 = pr0_1, pr0_2
      else
        ppr_1, ppr_2 = pr1_1, pr1_2
      end

      if iszero(brs[d1,2,k])
        pd1_1, pd1_2 = d10_1, d10_2
      else
        pd1_1, pd1_2 = d11_1, d11_2
      end

      if iszero(brs[d2,2,k])
        pd2_1, pd2_2 = d20_1, d20_2
      else
        pd2_1, pd2_2 = d21_1, d21_2
      end

      tp = normlize(*(ppr_1, pd1_1, pd2_1),
                    *(ppr_2, pd1_2, pd2_2))::Float64

      # sample the node's character
      brs[pr,2,k] = brs[d1,1,k] = brs[d2,1,k] = coinsamp(tp)::Int64
    end
  end

  return nothing
end

"""
    createhists!(λ::Array{Float64,1}, Y::Array{Int64,3}, pr::Int64, d1::Int64, d2::Int64, brs::Array{Int64,3}, brδt::Array{Array{Float64,1},1}, bridx_a::Array{Array{Array{Int64,1},1},1}, narea::Int64)

Create bit histories for all areas for the branch trio.
"""
function createhists!(λ1     ::Float64,
                      λ0     ::Float64,
                      Y      ::Array{Int64,3},
                      pr     ::Int64,
                      d1     ::Int64,
                      d2     ::Int64,
                      brs    ::Array{Int64,3},
                      brδt   ::Array{Array{Float64,1},1},
                      bridx_a::Array{Array{UnitRange{Int64},1},1},
                      narea  ::Int64,
                      nedge  ::Int64,
                      stemevs::Array{Array{Float64,1},1},
                      stbrl  ::Float64)

  @inbounds begin

    if pr == nedge
      # if stem branch do continuous DA
      mult_rejsam!(stemevs, brs, λ1, λ0, stbrl, narea, nedge)
      for j = Base.OneTo(narea), idx = (d1,d2)
        bit_rejsam!(Y, bridx_a[j][idx], brs[idx,2,j],
                    λ1, λ0, brδt[idx])
      end
    else
      for j = Base.OneTo(narea), idx = (pr,d1,d2)
        bit_rejsam!(Y, bridx_a[j][idx], brs[idx,2,j],
                    λ1, λ0, brδt[idx])
      end
    end
  end

  return nothing
end

"""
    mult_rejsam!(evs  ::Array{Array{Float64,1},1},
                 brs  ::Array{Int64,3},
                 λ1   ::Float64,
                 λ0   ::Float64,
                 t    ::Float64,
                 narea::Int64,
                 nedge::Int64)

  Multi-area branch rejection independent model sampling.
"""
function mult_rejsam!(evs  ::Array{Array{Float64,1},1},
                      brs  ::Array{Int64,3},
                      λ1   ::Float64,
                      λ0   ::Float64,
                      t    ::Float64,
                      narea::Int64,
                      nedge::Int64)

  @simd for k = Base.OneTo(narea)
    rejsam!(evs[k], brs[nedge,1,k], brs[nedge,2,k], λ1, λ0, t)
  end

  return nothing
end

"""
    ifextY(Y      ::Array{Int64,3},
           triad  ::Array{Int64,1},
           narea  ::Int64,
           bridx_a::Array{Array{UnitRange{Int64},1},1})

Return `true` if at some point the species
goes extinct and/or more than one change is
observed after some **δt**, otherwise returns `false`.
"""
function ifextY(Y      ::Array{Int64,3},
                stemevs::Array{Array{Float64,1},1},
                triad  ::Array{Int64,1},
                brs    ::Array{Int64,3},
                stbrl  ::Float64,
                narea  ::Int64,
                bridx_a::Array{Array{UnitRange{Int64},1},1},
                nedge  ::Int64)

  @inbounds begin

    if triad[1] == nedge
      ifext_cont(stemevs, brs, stbrl, narea, nedge) && return true::Bool

      for k in (triad[2],triad[3])
        ifext_disc(Y, k, narea, bridx_a) && return true::Bool
      end
    else

      for k in triad
        ifext_disc(Y, k, narea, bridx_a) && return true::Bool
      end
    end
  end

  return false::Bool
end

"""
    ifext_cont(t_hist::Array{Array{Float64,1},1},
               brs   ::Array{Int64,3},
               t     ::Float64,
               narea ::Int64,
               nedge ::Int64)

Return true if lineage goes extinct.
"""
function ifext_cont(t_hist::Array{Array{Float64,1},1},
                    brs   ::Array{Int64,3},
                    t     ::Float64,
                    narea ::Int64,
                    nedge ::Int64)

  @inbounds begin

    ioc = 0
    # initial occupancy time
    for k in Base.OneTo(narea)
      if brs[nedge,1,k] == 1
        ioc = k
        break
      end
    end

    ioct = t_hist[ioc][1]

    ntries = 0
    while !isapprox(ioct, t, atol = 1.0e-12)

      if ioc == narea
        ioc = 1
      else
        ioc += 1
      end

      tc = 0.0
      cs = brs[nedge,1,ioc]
      for ts in t_hist[ioc]
        tc += ts
        if ioct < tc
          if cs == 1
            ioct  = tc
            ntries = 0
            break
          else
            ntries += 1
            if ntries > narea
              return true
            end
            break
          end
        end
        cs = 1 - cs
      end

    end
  end

  return false::Bool
end

"""
    ifext_disc(Y      ::Array{Int64,3},
               br     ::Int64,
               narea  ::Int64,
               bridx_a::Array{Array{UnitRange{Int64},1},1})

Return `true` if at some point the species
goes extinct and/or more than one change is
observed after some **δt**, otherwise returns `false`.
This specific method is for single branch updates.
"""
function ifext_disc(Y      ::Array{Int64,3},
                    br     ::Int64,
                    narea  ::Int64,
                    bridx_a::Array{Array{UnitRange{Int64},1},1})

  @inbounds begin

    for i = Base.OneTo(length(bridx_a[1][br]::UnitRange{Int64})-1)
      s_e::Int64 = 0            # count current areas
      s_c::Int64 = 0            # count area changes

      for k = Base.OneTo(narea)
        s_e += Y[bridx_a[k][br][i]]::Int64
        if Y[bridx_a[k][br][i]]::Int64 != Y[bridx_a[k][br][i+1]]::Int64
          s_c += 1
        end
      end

      if s_e == 0 || s_c > 1
        return true::Bool
      end
    end

  end

  return false::Bool
end

#=
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Y stem node proposal function (continuous DA)
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
=#

"""
    upstemnode!(λ1   ::Float64,
                λ0   ::Float64,
                nedge::Int64,
                stemevs::Array{Array{Float64,1},1},
                brs  ::Array{Int64,3},
                brl  ::Array{Float64,1},
                narea::Int64)

Update stem node.
"""
function upstemnode!(λ1     ::Float64,
                     λ0     ::Float64,
                     nedge  ::Int64,
                     stemevs::Array{Array{Float64,1},1},
                     brs    ::Array{Int64,3},
                     stbrl  ::Float64,
                     narea  ::Int64)

  @inbounds begin

    # sample
    samplestem!(λ1, λ0, nedge, brs, stbrl, narea)

    # save extinct
    ntries = 1
    while sum(view(brs,nedge,1,:)) < 1
      samplestem!(λ1, λ0, nedge, brs, stbrl, narea)
      ntries += 1
      if ntries == 500
        return false::Bool
      end
    end

    # sample a congruent history
    mult_rejsam!(stemevs, brs, λ1, λ0, stbrl, narea, nedge)

    ntries = 1
    # check if extinct

    while ifext_cont(stemevs, brs, stbrl, narea, nedge)
      mult_rejsam!(stemevs, brs, λ1, λ0, stbrl, narea, nedge)

      ntries += 1
      if ntries == 500
        return false::Bool
      end
    end
  end

  return true::Bool
end

"""
    samplestem!(λ1   ::Float64,
                λ0   ::Float64,
                nedge::Int64,
                brs  ::Array{Int64,3},
                brl  ::Array{Float64,1},
                narea::Int64)

Sample stem node.
"""
function samplestem!(λ1   ::Float64,
                     λ0   ::Float64,
                     nedge::Int64,
                     brs  ::Array{Int64,3},
                     stbrl::Float64,
                     narea::Int64)

 @inbounds begin

    # estimate transition probabilities
    p0 = normlize(Ptrfast_end(λ1, λ0, stbrl, Val{0}))::Float64
    p1 = normlize(Ptrfast_end(λ1, λ0, stbrl, Val{1}))::Float64

    for k = Base.OneTo(narea)
      # sample the node's character
      if iszero(brs[nedge,2,k])
        brs[nedge,1,k] = coinsamp(p0)::Int64
      else
        brs[nedge,1,k] = coinsamp(p1)::Int64
      end
    end
  end

  return nothing
end

#=
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Y branch proposal functions
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
=#

"""
upbranchY!(λ1     ::Float64,
           λ0     ::Float64,
           ω1     ::Float64,
           ω0     ::Float64,
           avg_Δx ::Array{Float64,2},
           br     ::Int64,
           Y      ::Array{Int64,3},
           stemevc::Array{Array{Float64,1},1},
           wareas ::Array{Int64,1},
           bridx_a::Array{Array{UnitRange{Int64},1},1},
           brδt   ::Vector{Vector{Float64}},
           brl    ::Vector{Float64},
           brs    ::Array{Int64,3},
           narea  ::Int64,
           nedge  ::Int64)

Update one branch using discrete Data Augmentation
for all areas with independent
proposals taking into account `Δx` and `ω1` & `ω0`.
"""
function upbranchY!(λ1     ::Float64,
                    λ0     ::Float64,
                    br     ::Int64,
                    Y      ::Array{Int64,3},
                    stemevs::Array{Array{Float64,1},1},
                    bridx_a::Array{Array{UnitRange{Int64},1},1},
                    brδt   ::Vector{Vector{Float64}},
                    stbrl  ::Float64,
                    brs    ::Array{Int64,3},
                    narea  ::Int64,
                    nedge  ::Int64)

  ntries = 1

  # if stem branch
  if br == nedge
    mult_rejsam!(stemevs, brs, λ1, λ0, stbrl, narea, nedge)

    # check if extinct
    while ifext_cont(stemevs, brs, stbrl, narea, nedge)
      mult_rejsam!(stemevs, brs, λ1, λ0, stbrl, narea, nedge)

      ntries += 1
      if ntries == 500
        return false
      end
    end

  else
    createhists!(λ1, λ0, Y, br, brs, brδt, bridx_a, narea)

    # check if extinct
    while ifext_disc(Y, br, narea, bridx_a)
      createhists!(λ1, λ0, Y, br, brs, brδt, bridx_a, narea)

      ntries += 1
      if ntries == 500
        return false
      end
    end
  end

  return true
end

"""
    createhists!(λ1     ::Float64,
                 λ0     ::Float64,
                 ω1     ::Float64,
                 ω0     ::Float64,
                 avg_Δx ::Array{Float64,2},
                 Y      ::Array{Int64,3},
                 br     ::Int64,
                 brs    ::Array{Int64,3},
                 brδt   ::Array{Array{Float64,1},1},
                 bridx_a::Array{Array{UnitRange{Int64},1},1},
                 narea  ::Int64)

Create bit histories for all areas for one single branch
taking into account `Δx` and `ω1` & `ω0` for all areas.
"""
function createhists!(λ1     ::Float64,
                      λ0     ::Float64,
                      Y      ::Array{Int64,3},
                      br     ::Int64,
                      brs    ::Array{Int64,3},
                      brδt   ::Array{Array{Float64,1},1},
                      bridx_a::Array{Array{UnitRange{Int64},1},1},
                      narea  ::Int64)
  @inbounds begin
    for j = Base.OneTo(narea)
      bit_rejsam!(Y, bridx_a[j][br], brs[br,2,j], λ1, λ0, brδt[br])
    end
  end

  return nothing
end

#=
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
X proposal functions
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
=#

"""
    uptrioX!(pr   ::Int64,
             d1   ::Int64,
             d2   ::Int64,
             X    ::Array{Float64,2},
             bridx::Array{UnitRange{Int64},1},
             brδt ::Array{Array{Float64,1},1},
             σ²c  ::Float64)

Update the node and adjoining branhces of `trio` using Brownian bridges.
"""
function uptrioX!(pr   ::Int64,
                  d1   ::Int64,
                  d2   ::Int64,
                  X    ::Array{Float64,2},
                  bridx::Array{UnitRange{Int64},1},
                  brδt ::Array{Array{Float64,1},1},
                  brl  ::Array{Float64,1},
                  σ²ϕ  ::Float64,
                  nedge::Int64)
  @inbounds begin

    # if not root
    if pr != nedge

      ipr = bridx[pr]
      id1 = bridx[d1]
      id2 = bridx[d2]

      # update node
      X[id1[1]] =
      X[id2[1]] = trioupd(X[ipr[1]],
                          X[id1[end]],
                          X[id2[end]],
                          brl[pr], brl[d1], brl[d2], σ²ϕ)

      #update branches
      bbX!(X, ipr, brδt[pr], σ²ϕ)
      bbX!(X, id1, brδt[d1], σ²ϕ)
      bbX!(X, id2, brδt[d2], σ²ϕ)

    else
      id1 = bridx[d1]
      id2 = bridx[d2]

      # update node
      X[id1[1]] =
      X[id2[1]] = duoupd(X[id1[end]],
                         X[id2[end]],
                         brl[d1], brl[d2], σ²ϕ)

      # update branches
      bbX!(X, id1, brδt[d1], σ²ϕ)
      bbX!(X, id2, brδt[d2], σ²ϕ)
    end

  end

  return nothing
end

"""
    upbranchX!(j    ::Int64,
               X    ::Array{Float64,2},
               bridx::Array{UnitRange{Int64},1},
               brδt ::Array{Array{Float64,1},1},
               σ²c  ::Float64)

Update a branch j in X using a Brownian bridge.
"""
function upbranchX!(j    ::Int64,
                    X    ::Array{Float64,2},
                    bridx::Array{UnitRange{Int64},1},
                    brδt ::Array{Array{Float64,1},1},
                    σ²ϕ  ::Float64)

  @inbounds bbX!(X, bridx[j], brδt[j], σ²ϕ)

  return nothing
end

"""
    bbX!(X::Array{Float64,2}, idx::UnitRange, t::Array{Float64,1}, σ::Float64)

Brownian bridge simulation function for updating a branch in X in place.
"""
function bbX!(X  ::Array{Float64,2},
              idx::UnitRange,
              t  ::Array{Float64,1},
              σ²ϕ::Float64)

  @inbounds begin

    xf::Float64 = X[idx[end]]

    for i = Base.OneTo(lastindex(t)-1)
      X[idx[i+1]] = (X[idx[i]] + randn()*sqrt((t[i+1] - t[i])*σ²ϕ))::Float64
    end

    invte::Float64 = 1.0/t[end]
    xdif ::Float64 = (X[idx[end]] - xf)

    @simd for i = Base.OneTo(lastindex(t))
      X[idx[i]] = (X[idx[i]] - t[i] * invte * xdif)::Float64
    end
  end

  return nothing
end


"""
`module  JAC.MultiPhotonDeExcitation`
    ... a submodel of JAC that contains all methods for computing multi-photon excitation and decay rates.
"""
module MultiPhotonDeExcitation

    using Printf, QuadGK, ..AngularMomentum, ..AtomicState, ..Basics, ..Defaults, ..ManyElectron, ..Nuclear,
                          ..PhotoEmission, ..Radial, ..TableStrings

    """
    `abstract type MultiPhotonDeExcitation.AbstractMultiPhotonProperty`
        ... defines an abstract type to distinguish different multi-photon properties; however, by far not all of these properties
            are usually relevant for all the process::MultiPhotonDeExcitation.AbstractMultiPhotonProcess; cf. the JAC User Guide:

        + struct EnergyDiffCs
                    ... Energy differential cross section for initially unpolarized atoms (TwoPhotonEmission, ...)
        + struct TotalCsLinear
                    ... Total cross section for linerarly-polarized incident radiation (TwoPhotonAbsorptionMonochromatic, ...)
        + struct TotalCsRightCircular
                    ... Total cross section for right-circularly polarized incident radiation (TwoPhotonAbsorptionMonochromatic, ...)
        + struct TotalCsUnpolarized
                    ... Total cross section for unpolarized incident radiation (TwoPhotonAbsorptionMonochromatic, ...)
        + struct TotalCsDensityMatrix
                    ... Total cross section for incident radiation with user-defined density matrix (not yet).
                        (TwoPhotonAbsorptionMonochromatic, ...)
    """
    abstract type  AbstractMultiPhotonProperty       end

    """
    `struct  MultiPhotonDeExcitation.EnergyDiffCs  <:  MultiPhotonDeExcitation.AbstractMultiPhotonProperty`
        ... a struct to request for the calculation of energy-diffrential cross sections in a two-photon emission process.
    """
    struct   EnergyDiffCs  <:  MultiPhotonDeExcitation.AbstractMultiPhotonProperty      end

    function Base.show(property::EnergyDiffCs)
        sa = "energy-differential cross sections for multi-photon emission";  print(io, sa, "\n")
    end

    """
    `struct  MultiPhotonDeExcitation.TotalCsLinear  <:  MultiPhotonDeExcitation.AbstractMultiPhotonProperty`
        ... a struct to request for the calculation of the total (absorption) cross sections for linearly-polarized
            incident radiation.
    """
    struct   TotalCsLinear  <:  MultiPhotonDeExcitation.AbstractMultiPhotonProperty      end

    function Base.show(property::TotalCsLinear)
        sa = "total (absorption) cross sections for linearly-polarized radiation";  print(io, sa, "\n")
    end

    """
    `struct  MultiPhotonDeExcitation.TotalCsRightCircular  <:  MultiPhotonDeExcitation.AbstractMultiPhotonProperty`
        ... a struct to request for the calculation of the total (absorption) cross sections for right-circularly polarized
            incident radiation.
    """
    struct   TotalCsRightCircular  <:  MultiPhotonDeExcitation.AbstractMultiPhotonProperty      end

    function Base.show(property::TotalCsRightCircular)
        sa = "total (absorption) cross sections for right-circularly polarized radiation";  print(io, sa, "\n")
    end

    """
    `struct  MultiPhotonDeExcitation.TotalCsUnpolarized  <:  MultiPhotonDeExcitation.AbstractMultiPhotonProperty`
        ... a struct to request for the calculation of the total (absorption) cross sections for unpolarized
            incident radiation.
    """
    struct   TotalCsUnpolarized  <:  MultiPhotonDeExcitation.AbstractMultiPhotonProperty      end

    function Base.show(property::TotalCsUnpolarized)
        sa = "total (absorption) cross sections for unpolarized radiation";  print(io, sa, "\n")
    end

    """
    `abstract type MultiPhotonDeExcitation.AbstractMultiPhotonProcess`
        ... defines an abstract type to distinguish different multi-photon processes; see also:

        + struct TwoPhotonAbsorptionMonochromatic
                    ... to specify a two-photon absorption process with monochromatic and equally-polarized photons,
                        usually from the same beam.
        + struct TwoPhotonAbsorptionBichromatic
                    ... to specify a two-photon absorption process by photons with well-defined frequency, propagation
                        direction and helicity, usually from different (oriented) beams.
        + struct TwoPhotonEmission
                    ... to specify a two-photon emission process, often for initially-unpolarized atoms.
    """
    abstract type  AbstractMultiPhotonProcess       end

    """
    `struct  MultiPhotonDeExcitation.TwoPhotonAbsorptionMonochromatic  <:  MultiPhotonDeExcitation.AbstractMultiPhotonProcess`
        ... a struct to specify a two-photon absorption process with monochromatic and equally-polarized photons,
            usually from the same beam.

        + properties        ::Array{MultiPhotonDeExcitation.AbstractMultiPhotonProperty,1}
            ... List of the multi-photon processes that are to be calculated for this absorption process.
    """
    struct   TwoPhotonAbsorptionMonochromatic  <:  MultiPhotonDeExcitation.AbstractMultiPhotonProcess
        properties          ::Array{MultiPhotonDeExcitation.AbstractMultiPhotonProperty,1}
    end

    """
    `MultiPhotonDeExcitation.TwoPhotonAbsorptionMonochromatic()`
        ... constructor for an 'default' instance of a MultiPhotonDeExcitation.TwoPhotonAbsorptionMonochromatic() process.
    """
    function TwoPhotonAbsorptionMonochromatic()
        TwoPhotonAbsorptionMonochromatic( [TotalCsLinear(), TotalCsUnpolarized()] )
    end

    function Base.string(process::MultiPhotonDeExcitation.TwoPhotonAbsorptionMonochromatic)
        sa = "Two-photon absorption process with mono-chromatic photons (from the same beam):"
        return( sa )
    end

    function Base.show(io::IO, process::MultiPhotonDeExcitation.TwoPhotonAbsorptionMonochromatic)
        sa = Base.string(process);               print(io, sa, "\n")
        println(io, "properties:                 $(process.properties)  ")
    end

    """
    `struct  MultiPhotonDeExcitation.TwoPhotonAbsorptionBichromatic  <:  MultiPhotonDeExcitation.AbstractMultiPhotonProcess`
        ... a struct to specify a two-photon absorption process by photons with well-defined frequency, propagation direction
            and helicity, usually from different (oriented) beams.

        + properties        ::Array{MultiPhotonDeExcitation.AbstractMultiPhotonProperty,1}
            ... List of the multi-photon processes that are to be calculated for this absorption process.
        + omegaLess         ::Float64
            ... energy of the photon with smaller frequency; the larger frequency is derived from energy conservation.
    """
    struct   TwoPhotonAbsorptionBichromatic  <:  MultiPhotonDeExcitation.AbstractMultiPhotonProcess
        properties          ::Array{MultiPhotonDeExcitation.AbstractMultiPhotonProperty,1}
        omegaLess           ::Float64
    end

    """
    `MultiPhotonDeExcitation.TwoPhotonAbsorptionBichromatic()`
        ... constructor for an 'default' instance of a MultiPhotonDeExcitation.TwoPhotonAbsorptionBichromatic() process.
    """
    function TwoPhotonAbsorptionBichromatic()
        TwoPhotonAbsorptionBichromatic( [TotalCsUnpolarized()], 0.05 )
    end

    function Base.string(process::MultiPhotonDeExcitation.TwoPhotonAbsorptionBichromatic)
        sa = "Two-photon absorption process with bi-chromatic photons (from different beams):"
        return( sa )
    end

    function Base.show(io::IO, process::MultiPhotonDeExcitation.TwoPhotonAbsorptionBichromatic)
        sa = Base.string(process);               print(io, sa, "\n")
        println(io, "properties:                 $(process.properties)  ")
        println(io, "omegaLess:                  $(process.omegaLess)  ")
    end

    """
    `struct  MultiPhotonDeExcitation.TwoPhotonEmission  <:  MultiPhotonDeExcitation.AbstractMultiPhotonProcess`
        ... a struct to specify a two-photon emission process.

        + properties        ::Array{MultiPhotonDeExcitation.AbstractMultiPhotonProperty,1}
            ... List of the multi-photon processes that are to be calculated for this emission process.
        + noSharings  ::Int64   ... number of energy sharings for atomic transition, using the zeros of a Gauß-Legendre integration
    """
    struct   TwoPhotonEmission  <:  MultiPhotonDeExcitation.AbstractMultiPhotonProcess
        properties          ::Array{MultiPhotonDeExcitation.AbstractMultiPhotonProperty,1}
        noSharings          ::Int64
    end

    """
    `MultiPhotonDeExcitation.TwoPhotonEmission()`
        ... constructor for an 'default' instance of a MultiPhotonDeExcitation.TwoPhotonEmission() process.
    """
    function TwoPhotonEmission()
        TwoPhotonEmission( [EnergyDiffCs()], 4 )
    end

    function Base.string(process::MultiPhotonDeExcitation.TwoPhotonEmission)
        sa = "Two-photon emission process:"
        return( sa )
    end

    function Base.show(io::IO, process::MultiPhotonDeExcitation.TwoPhotonEmission)
        sa = Base.string(process);               print(io, sa, "\n")
        println(io, "properties:                 $(process.properties)  ")
    end

    """
    `struct  MultiPhotonDeExcitation.Settings  <:  AbstractProcessSettings`  ... defines a type for the settings in estimating multi-photon excitation and decay rates.

        + process           ::MultiPhotonDeExcitation.AbstractMultiPhotonProcess      ... considered multi-photon process.
        + multipoles        ::Array{EmMultipole}           ... Specifies the multipoles of the radiation field that are to be included.
        + gauges            ::Array{UseGauge}              ... Specifies the gauges to be included into the computations.
        + green             ::Array{AtomicState.GreenChannel,1}
                              ... Green function channels for these multi-photon calculations which need to be generated independently.
        + NoEnergySharings  ::Int64                        ... Number of energy sharings that are used for all emission lines in the
                                                               computations for each line.
        + printBefore       ::Bool                         ... True, if all energies and lines are printed before their evaluation.
        + lineSelection     ::LineSelection                ... Specifies the selected levels, if any.

    """
    struct Settings  <:  AbstractProcessSettings
        process             ::MultiPhotonDeExcitation.AbstractMultiPhotonProcess
        multipoles          ::Array{EmMultipole}
        gauges              ::Array{UseGauge}
        green               ::Array{AtomicState.GreenChannel,1}
        NoEnergySharings    ::Int64
        printBefore         ::Bool
        lineSelection       ::LineSelection
    end

    """
    `MultiPhotonDeExcitation.Settings()`  ... constructor for the default values of multi-photon excitation and decay rates.
    """
    function Settings()
        Settings( TwoPhotonEmission(), EmMultipole[], UseGauge[], AtomicState.GreenExpansion(), false, LineSelection() )
    end

    # `Base.show(io::IO, settings::MultiPhotonDeExcitation.Settings)`
    #		... prepares a proper printout of the variable settings::MultiPhotonDeExcitation.Settings.
     function Base.show(io::IO, settings::MultiPhotonDeExcitation.Settings)
        println(io, "process:                  $(settings.process)  ")
        println(io, "multipoles:               $(settings.multipoles)  ")
        println(io, "gauges:                   $(settings.gauges)  ")
        print(io,   "green:                     (settings.green)  ... with symmetries:   ")
        for channel in settings.green  print(io, "   $(channel.symmetry)")  end;   println(io, " ")
        println(io, "NoEnergySharings:         $(settings.NoEnergySharings)  ")
        println(io, "printBefore:              $(settings.printBefore)  ")
        println(io, "lineSelection:            $(settings.lineSelection)  ")
    end

    include("module-MultiPhotonDeExc-inc-2pAbsMono.jl")
    include("module-MultiPhotonDeExc-inc-2pEmission.jl")

end # module


using Printf

abstract type PerformanceCurve end

xmetric(::Type{<: PerformanceCurve}) = error("unimplemented")
xmetric(::T) where {T <: PerformanceCurve} = xmetric(T)

ymetric(::Type{<: PerformanceCurve}) = error("unimplemented")
ymetric(::T) where {T <: PerformanceCurve} = ymetric(T)

xlabel(::Type{<: PerformanceCurve}) = error("unimplemented")
xlabel(::T) where {T <: PerformanceCurve} = xlabel(T)

ylabel(::Type{<: PerformanceCurve}) = error("unimplemented")
ylabel(::T) where {T <: PerformanceCurve} = ylabel(T)

xvalues(::PerformanceCurve) = error("unimplemented")
yvalues(::PerformanceCurve) = error("unimplemented")

startpoint(::Type{<: PerformanceCurve}, xs, ys) = error("unimplemented")
startpoint(::T, args...) where {T <: PerformanceCurve} = startpoint(T, args...)

endpoint(::Type{<: PerformanceCurve}, xs, ys) = error("unimplemented")
endpoint(::T, args...) where {T <: PerformanceCurve} = endpoint(T, args...)

sortage!(::Type{<: PerformanceCurve}, xs, ys) = error("unimplemented")

function downsample(xs::AbstractVector{Float64}, ys::AbstractVector{Float64})
    keep = [1]
    for i in 2:length(xs)
        if !(xs[i] ≈ xs[i-1] && ys[i] ≈ ys[i-1])
            push!(keep, i)
        end
    end
    keep
end

function prepare!(::Type{T},
                 xs::AbstractVector{Float64},
                 ys::AbstractVector{Float64};
                 down=true) where {T <: PerformanceCurve}

    if size(xs) != size(ys)
        throw(DimensionMismatch("$(xlabel(T)) and $(ylabel(T)) vectors have different sizes"))
    elseif any(<(0.0), xs)
        throw(ArgumentError("$(xlabel(T)) less than 0.0 provided"))
    elseif any(<(0.0), ys)
        throw(ArgumentError("$(ylabel(T)) less than 0.0 provided"))
    elseif any(>(1.0), xs)
        throw(ArgumentError("$(xlabel(T)) greater than 1.0 provided"))
    elseif any(>(1.0), ys)
        throw(ArgumentError("$(ylabel(T)) greater than 1.0 provided"))
    end

    sortage!(T, xs, ys)

    if down
        d = downsample(xs, ys)
        xs = xs[d]
        ys = ys[d]
    end

    s = startpoint(T, xs, ys)
    if !all((xs[begin], ys[begin]) .≈ s)
        pushfirst!(xs, s[begin])
        pushfirst!(ys, s[end])
    end

    e = endpoint(T, xs, ys)
    if !all((xs[end], ys[end]) .≈ e)
        push!(xs, e[begin])
        push!(ys, e[end])
    end

    xs, ys
end

Base.length(c::PerformanceCurve) = length(xvalues(c))

function MLBase.roc(T::Type{<:PerformanceCurve},
                    arena::AbstractArena,
                    edges::Vector{EdgeEvidence},
                    args...)
    g = graph(arena)
    gt = [Int(has_edge(g, e.src, e.dst)) for e in edges]
    scores = [e.evidence for e in edges]
    if all(==(scores[1]), scores)
        T(gt)
    else
        x, y = xmetric(T), ymetric(T)
        r = roc(gt, scores, args...)
        T(map(x, r), map(y, r))
    end
end

auc(c::PerformanceCurve) = let xs = xvalues(c), ys = yvalues(c)
    @views dot(ys[2:end] + ys[1:end-1], xs[2:end] - xs[1:end-1]) / 2.0
end

struct Curve
    xs::Vector{Float64}
    ys::Vector{Float64}
    function Curve(xs::AbstractVector{Float64}, ys::AbstractVector{Float64})
        if length(xs) != length(ys)
            throw(DimensionMismatch("the x- and y-position vectors must have the same length"))
        end
        new(xs, ys)
    end
end
Curve(r::PerformanceCurve) = Curve(xvalues(r), yvalues(r))

Base.length(c::Curve) = length(c.xs)

function Base.:+(a::Curve, b::Curve)
    xs, ys = Float64[], Float64[]
    i, j = 1, 1
    @views while i <= length(a) && j <= length(b)
        if a.xs[i] ≈ b.xs[j]
            push!(xs, a.xs[i])
            push!(ys, a.ys[i] + b.ys[j])
            i += 1
            j += 1
        elseif a.xs[i] < b.xs[j]
            push!(xs, a.xs[i])
            push!(ys, a.ys[i] + interpolate(b.xs[j-1:j], b.ys[j-1:j], a.xs[i]))
            i += 1
        else
            push!(xs, b.xs[j])
            push!(ys, b.ys[j] + interpolate(a.xs[i-1:i], a.ys[i-1:i], b.xs[j]))
            j += 1
        end
    end
    Curve(xs, ys)
end

function Base.:-(a::Curve, b::Curve)
    xs, ys = Float64[], Float64[]
    i, j = 1, 1
    @views while i <= length(a) && j <= length(b)
        if a.xs[i] ≈ b.xs[j]
            push!(xs, a.xs[i])
            push!(ys, a.ys[i] - b.ys[j])
            i += 1
            j += 1
        elseif a.xs[i] < b.xs[j]
            push!(xs, a.xs[i])
            push!(ys, a.ys[i] - interpolate(b.xs[j-1:j], b.ys[j-1:j], a.xs[i]))
            i += 1
        else
            push!(xs, b.xs[j])
            push!(ys, b.ys[j] - interpolate(a.xs[i-1:i], a.ys[i-1:i], b.xs[j]))
            j += 1
        end
    end
    Curve(xs, ys)
end

function interpolate(xs::AbstractVector{Float64}, ys::AbstractVector{Float64}, x::Float64)
    m = (ys[2] - ys[1]) / (xs[2] - xs[1])
    m * (x - xs[1]) + ys[1]
end

Base.:/(c::Curve, k::Number) = Curve(c.xs, c.ys / k)

Statistics.mean(cs::AbstractVector{T}) where {T <: PerformanceCurve} = T(mean(Curve.(cs)))

struct ROC <: PerformanceCurve
    fpr::Vector{Float64}
    tpr::Vector{Float64}
    function ROC(fpr::Vector{Float64}, tpr::Vector{Float64}; kwargs...)
        fpr, tpr = prepare!(ROC, deepcopy(fpr), deepcopy(tpr); kwargs...)
        new(fpr, tpr)
    end
end
ROC() = ROC([0.0, 1.0], [0.0, 1.0])
ROC(::AbstractArray{Int}) = ROC()
ROC(c::Curve) = ROC(c.xs, c.ys)

fpr(r::ROC) = r.fpr

tpr(r::ROC) = r.tpr

xmetric(::Type{ROC}) = false_positive_rate

ymetric(::Type{ROC}) = true_positive_rate

xlabel(::Type{ROC}) = :FPR

ylabel(::Type{ROC}) = :TPR

startpoint(::Type{ROC}, fpr, tpr) = (0.0, 0.0)

endpoint(::Type{ROC}, fpr, tpr) = (1.0, 1.0)

xvalues(r::ROC) = fpr(r)

yvalues(r::ROC) = tpr(r)

function sortage!(::Type{ROC}, fpr, tpr)
    if !(issorted(fpr) && issorted(tpr))
        p = sortperm(collect(zip(fpr, tpr)))
        fpr[:] = fpr[p]
        tpr[:] = tpr[p]
    end

    if !issorted(tpr)
        throw(ArgumentError("the $T curve is not monotonic"))
    end

    fpr, tpr
end

struct PRC <: PerformanceCurve
    recall::Vector{Float64}
    precision::Vector{Float64}
    function PRC(recall::Vector{Float64}, precision::Vector{Float64}; kwargs...)
        recall, precision = prepare!(PRC, deepcopy(recall), deepcopy(precision); kwargs...)
        new(recall, precision)
    end
end
PRC(baseline::Float64=0.5) = PRC([0.0, 0.0, 1.0, 1.0], [1.0, baseline, baseline, 0.0])
PRC(gt::AbstractArray{Int}) = PRC(mean(gt))
PRC(c::Curve) = PRC(c.xs, c.ys)

recall(r::PRC) = r.recall

precision(r::PRC) = r.precision

xmetric(::Type{PRC}) = MLBase.recall

ymetric(::Type{PRC}) = MLBase.precision

xlabel(::Type{PRC}) = :Recall

ylabel(::Type{PRC}) = :Precision

startpoint(::Type{PRC}, recall, precision) = (0.0, 1.0)

endpoint(::Type{PRC}, recall, precision) = (1.0, 0.0)

xvalues(r::PRC) = recall(r)

yvalues(r::PRC) = precision(r)

function prcorder((r1, p1), (r2, p2))
    isequal(r1, r2) && return isless(p2, p1)
    isless(r1, r2)
end

function sortage!(::Type{PRC}, recall, precision)
    p = sortperm(collect(zip(recall, precision)); lt = prcorder)
    recall[:] = recall[p]
    precision[:] = precision[p]

    recall, precision
end

@recipe function plotroc(r::ROC)
    a = auc(r)
    if !get(plotattributes, :noauc, false)
        label := if haskey(plotattributes, :label) && plotattributes[:label] != ""
            @sprintf "%s (AUC = %0.3f)" plotattributes[:label] a
        elseif !haskey(plotattributes, :label)
            @sprintf "AUC = %0.3f" a
        end
    end

    xlims := (0, 1)
    ylims := (0, 1)

    grid -> false
    legend --> ifelse(a ≥ 0.5, :bottomright, :topleft)
    size --> (600, 600)
    xguide --> "FPR"
    yguide --> "TPR"
    linewidth --> 2

    @series begin
        r.fpr, r.tpr
    end

    @series begin
        seriestype := :path
        line := :dash
        linewidth := 2
        seriescolor := :dimgray
        label := nothing
        [0,1], [0,1]
    end
end

safelog(x::Float64) = iszero(x) ? zero(x) : log(x)
function rescale(scores::AbstractArray{Float64})
    a, b = extrema(scores)
    @. (scores - a) / (b - a)
end

function perf(arena::AbstractArena, edges::Vector{EdgeEvidence}, kernel::Function)
    g = graph(arena)
    gt = Float64[has_edge(g, e.src, e.dst) for e in edges]
    scores = rescale([e.evidence for e in edges])
    ll = 0.
    for (g, s) in zip(gt, scores)
        ll += kernel(g, s)
    end
    ll / length(edges)
end

logloss(arena, edges) = perf(arena, edges, (g,s) -> -(g * safelog(s) + (1. - g) * safelog(1. - s)))
brier(arena, edges) = perf(arena, edges, (g,s) -> (g - s)^2)


# This file is part of Kpax3. License is MIT.

function gibbs!(data::Matrix{UInt8},
                priorR::PriorRowPartition,
                priorC::PriorColPartition,
                settings::KSettings,
                support::MCMCSupport,
                state::AminoAcidState)
  shuffleunits!(support.u)

  for a in support.u
    resizesupport!(support, state.k + 1)
    gibbsallocate!(a, data, priorR, priorC, settings, support, state)
  end

  nothing
end

function gibbsallocate!(i::Int,
                        data::Matrix{UInt8},
                        priorR::PriorRowPartition,
                        priorC::PriorColPartition,
                        settings::KSettings,
                        support::MCMCSupport,
                        state::AminoAcidState)
  hi = state.R[i]

  initsupportgibbs!(hi, priorR, state, support)

  hj = 0

  # normalizing constant
  logc = -Inf

  # temporary variables
  li = 0
  lj = 0

  g = 0
  y = 0.0
  v = 0

  if state.v[hi] > 1
    support.vi = state.v[hi] - 1

    li = support.k + 1
    lj = support.k + 2

    support.t[li] = clusterweight(support.vi, priorR)
    support.t[lj] = clusterweight(1, state.k, priorR)

    # there are three options:
    # 1) i put back into hi (likelihood does not change)
    # 2) i put into hj (remove i from hi and compute the singleton cluster)
    # 3) i put into another existing cluster (remove i from hi and compute the
    #                                         new likelihood)

    for b in 1:support.m
      # remove i from its cluster
      support.ni[b] = state.n1s[hi, b] - float(data[b, i])

      # support.lpi and support.lpj are large enough to be used as a storage
      # support.lpi will contain the global sum of clusters not containing i
      # support.lpj will contain sums associated with hi and hj
      support.lpi[1, b] = priorC.logγ[1]
      support.lpi[2, b] = priorC.logγ[2]
      support.lpi[3, b] = priorC.logγ[3]
      support.lpi[4, b] = priorC.logγ[3]

      for l in 1:support.k
        g = support.cl[l]
        y = state.n1s[g, b] + float(data[b, i])
        v = state.v[g] + 1
        gibbsupdateclustergmove!(y, v, b, l, g, state.k, priorC, support)
      end
      gibbsupdateclusteri!(state.k, b, li, priorC, support)
      gibbsupdateclusterj!(data[b, i], state.k + 1, b, li, lj, priorC, support)
    end

    gibbscomputeprobi!(li, support)
    for b in 1:support.m
      for l in 1:support.k
        gibbscomputeprobg!(b, l, li, support)
      end
      gibbscomputeprobj!(b, li, lj, support)
    end

    logc = gibbscomputenormconst(lj, support)

    l = gibbssamplecluster(logc, lj, support)

    # if l == li do nothing
    if l <= support.k
      gibbsmove!(i, hi, support.cl[l], li, l, data, priorR, support, state)
    elseif l == lj
      gibbssplit!(i, hi, li, lj, data, priorR, settings, support, state)
    end
  else
    # li and lj are the same
    lj = support.k + 1

    support.t[lj] = clusterweight(1, support.k, priorR)

    for b in 1:support.m
      # support.lpi and support.lpj are large enough to be used as a storage
      # support.lpi will contain the global sum of clusters not containing i
      # support.lpj will contain sums associated with hi and hj
      support.lpi[1, b] = priorC.logγ[1]
      support.lpi[2, b] = priorC.logγ[2]
      support.lpi[3, b] = priorC.logγ[3]

      for l in 1:support.k
        g = support.cl[l]
        y = state.n1s[g, b] + float(data[b, i])
        v = state.v[g] + 1
        gibbsupdateclustergmerge!(y, v, b, l, g, support.k, priorC, support)
      end
    end

    gibbscomputeprobi!(lj, support)
    for b in 1:support.m, l in 1:support.k
      gibbscomputeprobg!(b, l, support)
    end

    logc = gibbscomputenormconst(lj, support)

    l = gibbssamplecluster(logc, lj, support)

    # if l == lj do nothing
    if l <= support.k
      gibbsmerge!(i, hi, support.cl[l], l, data, priorR, support, state)
    end
  end

  nothing
end

function initsupportgibbs!(hi::Int,
                           priorR::PriorRowPartition,
                           state::AminoAcidState,
                           support::MCMCSupport)
  # initialize clusters other than hi
  g = 0
  support.k = 0
  for l in 1:state.k
    g = state.cl[l]
    if g != hi
      support.k += 1
      support.cl[support.k] = g
      support.t[support.k] = clusterweight(state.v[g], priorR)
    end
  end

  nothing
end

function gibbsupdateclustergmove!(y::Float64,
                                  v::Int,
                                  b::Int,
                                  l::Int,
                                  g::Int,
                                  k::Int,
                                  priorC::PriorColPartition,
                                  support::MCMCSupport)
  support.lpi[1, b] += support.lp[1, g, b]
  support.lpi[2, b] += support.lp[2, g, b]

  support.tmp[3] = priorC.logω[k][1] + support.lp[3, g, b]
  support.tmp[4] = priorC.logω[k][2] + support.lp[4, g, b]

  support.tmp[1] = if support.tmp[3] > support.tmp[4]
    support.tmp[3] + log1p(exp(support.tmp[4] - support.tmp[3]))
  else
    support.tmp[4] + log1p(exp(support.tmp[3] - support.tmp[4]))
  end

  support.lpi[3, b] += support.tmp[1]

  # when we move unit i to the singleton cluster, the total number of clusters
  # increases by one
  support.tmp[3] = priorC.logω[k + 1][1] + support.lp[3, g, b]
  support.tmp[4] = priorC.logω[k + 1][2] + support.lp[4, g, b]

  support.lpi[4, b] += if support.tmp[3] > support.tmp[4]
    support.tmp[3] + log1p(exp(support.tmp[4] - support.tmp[3]))
  else
    support.tmp[4] + log1p(exp(support.tmp[3] - support.tmp[4]))
  end

  support.lq[1, l, b] = logmarglik(y, v, priorC.A[1, b], priorC.B[1, b])
  support.lq[2, l, b] = logmarglik(y, v, priorC.A[2, b], priorC.B[2, b])
  support.lq[3, l, b] = logmarglik(y, v, priorC.A[3, b], priorC.B[3, b])
  support.lq[4, l, b] = logmarglik(y, v, priorC.A[4, b], priorC.B[4, b])

  support.tmp[3] = priorC.logω[k][1] + support.lq[3, l, b]
  support.tmp[4] = priorC.logω[k][2] + support.lq[4, l, b]

  support.tmp[2] = if support.tmp[3] > support.tmp[4]
    support.tmp[3] + log1p(exp(support.tmp[4] - support.tmp[3]))
  else
    support.tmp[4] + log1p(exp(support.tmp[3] - support.tmp[4]))
  end

  # we added extra elements to support.lpi and they should be subtracted
  support.lr[1, l, b] = support.lq[1, l, b] - support.lp[1, g, b]
  support.lr[2, l, b] = support.lq[2, l, b] - support.lp[2, g, b]
  support.lr[3, l, b] = support.tmp[2] - support.tmp[1]

  nothing
end

function gibbsupdateclustergmerge!(y::Float64,
                                   v::Int,
                                   b::Int,
                                   l::Int,
                                   g::Int,
                                   k::Int,
                                   priorC::PriorColPartition,
                                   support::MCMCSupport)
  support.lpi[1, b] += support.lp[1, g, b]
  support.lpi[2, b] += support.lp[2, g, b]

  support.tmp[3] = priorC.logω[k][1] + support.lp[3, g, b]
  support.tmp[4] = priorC.logω[k][2] + support.lp[4, g, b]

  support.tmp[1] = if support.tmp[3] > support.tmp[4]
    support.tmp[3] + log1p(exp(support.tmp[4] - support.tmp[3]))
  else
    support.tmp[4] + log1p(exp(support.tmp[3] - support.tmp[4]))
  end

  support.lpi[3, b] += support.tmp[1]

  support.lq[1, l, b] = logmarglik(y, v, priorC.A[1, b], priorC.B[1, b])
  support.lq[2, l, b] = logmarglik(y, v, priorC.A[2, b], priorC.B[2, b])
  support.lq[3, l, b] = logmarglik(y, v, priorC.A[3, b], priorC.B[3, b])
  support.lq[4, l, b] = logmarglik(y, v, priorC.A[4, b], priorC.B[4, b])

  support.tmp[3] = priorC.logω[k][1] + support.lq[3, l, b]
  support.tmp[4] = priorC.logω[k][2] + support.lq[4, l, b]

  support.tmp[2] = if support.tmp[3] > support.tmp[4]
    support.tmp[3] + log1p(exp(support.tmp[4] - support.tmp[3]))
  else
    support.tmp[4] + log1p(exp(support.tmp[3] - support.tmp[4]))
  end

  # we added extra elements to support.lpi and they should be subtracted
  support.lr[1, l, b] = support.lq[1, l, b] - support.lp[1, g, b]
  support.lr[2, l, b] = support.lq[2, l, b] - support.lp[2, g, b]
  support.lr[3, l, b] = support.tmp[2] - support.tmp[1]

  nothing
end

function gibbsupdateclusteri!(k::Int,
                              b::Int,
                              li::Int,
                              priorC::PriorColPartition,
                              support::MCMCSupport)
  y = support.ni[b]
  v = support.vi

  support.lq[1, li, b] = logmarglik(y, v, priorC.A[1, b], priorC.B[1, b])
  support.lq[2, li, b] = logmarglik(y, v, priorC.A[2, b], priorC.B[2, b])
  support.lq[3, li, b] = logmarglik(y, v, priorC.A[3, b], priorC.B[3, b])
  support.lq[4, li, b] = logmarglik(y, v, priorC.A[4, b], priorC.B[4, b])

  support.tmp[3] = priorC.logω[k][1] + support.lq[3, li, b]
  support.tmp[4] = priorC.logω[k][2] + support.lq[4, li, b]

  support.lpj[1, b] = if support.tmp[3] > support.tmp[4]
    support.tmp[3] + log1p(exp(support.tmp[4] - support.tmp[3]))
  else
    support.tmp[4] + log1p(exp(support.tmp[3] - support.tmp[4]))
  end

  nothing
end

function gibbsupdateclusterj!(y::UInt8,
                              k::Int,
                              b::Int,
                              li::Int,
                              lj::Int,
                              priorC::PriorColPartition,
                              support::MCMCSupport)
  # add i to the singleton cluster (increases k by one)
  support.tmp[3] = priorC.logω[k][1] + support.lq[3, li, b]
  support.tmp[4] = priorC.logω[k][2] + support.lq[4, li, b]

  support.lpj[2, b] = if support.tmp[3] > support.tmp[4]
    support.tmp[3] + log1p(exp(support.tmp[4] - support.tmp[3]))
  else
    support.tmp[4] + log1p(exp(support.tmp[3] - support.tmp[4]))
  end

  support.lq[1, lj, b] = logmarglik(y, 1, priorC.A[1, b], priorC.B[1, b])
  support.lq[2, lj, b] = logmarglik(y, 1, priorC.A[2, b], priorC.B[2, b])
  support.lq[3, lj, b] = logmarglik(y, 1, priorC.A[3, b], priorC.B[3, b])
  support.lq[4, lj, b] = logmarglik(y, 1, priorC.A[4, b], priorC.B[4, b])

  support.tmp[3] = priorC.logω[k][1] + support.lq[3, lj, b]
  support.tmp[4] = priorC.logω[k][2] + support.lq[4, lj, b]

  support.lpj[3, b] = if support.tmp[3] > support.tmp[4]
    support.tmp[3] + log1p(exp(support.tmp[4] - support.tmp[3]))
  else
    support.tmp[4] + log1p(exp(support.tmp[3] - support.tmp[4]))
  end

  nothing
end

function gibbscomputeprobg!(b::Int,
                            l::Int,
                            li::Int,
                            support::MCMCSupport)
  # take into consideration that cluster hi has changed
  support.tmp[1] = support.lpi[1, b] + support.lr[1, l, b] +
                   support.lq[1, li, b]
  support.tmp[2] = support.lpi[2, b] + support.lr[2, l, b] +
                   support.lq[2, li, b]
  support.tmp[3] = support.lpi[3, b] + support.lr[3, l, b] +
                   support.lpj[1, b]

  support.t[l] += if (support.tmp[1] >= support.tmp[2]) &&
                     (support.tmp[1] >= support.tmp[3])
                    support.tmp[1] +
                    log1p(exp(support.tmp[2] - support.tmp[1]) +
                          exp(support.tmp[3] - support.tmp[1]))
                  elseif (support.tmp[2] >= support.tmp[1]) &&
                         (support.tmp[2] >= support.tmp[3])
                    support.tmp[2] +
                    log1p(exp(support.tmp[1] - support.tmp[2]) +
                          exp(support.tmp[3] - support.tmp[2]))
                  else
                    support.tmp[3] +
                    log1p(exp(support.tmp[1] - support.tmp[3]) +
                          exp(support.tmp[2] - support.tmp[3]))
                  end

  nothing
end

function gibbscomputeprobg!(b::Int,
                            l::Int,
                            support::MCMCSupport)
  # when merging, cluster hi disappears
  support.tmp[1] = support.lpi[1, b] + support.lr[1, l, b]
  support.tmp[2] = support.lpi[2, b] + support.lr[2, l, b]
  support.tmp[3] = support.lpi[3, b] + support.lr[3, l, b]

  support.t[l] += if (support.tmp[1] >= support.tmp[2]) &&
                     (support.tmp[1] >= support.tmp[3])
                    support.tmp[1] +
                    log1p(exp(support.tmp[2] - support.tmp[1]) +
                          exp(support.tmp[3] - support.tmp[1]))
                  elseif (support.tmp[2] >= support.tmp[1]) &&
                         (support.tmp[2] >= support.tmp[3])
                    support.tmp[2] +
                    log1p(exp(support.tmp[1] - support.tmp[2]) +
                          exp(support.tmp[3] - support.tmp[2]))
                  else
                    support.tmp[3] +
                    log1p(exp(support.tmp[1] - support.tmp[3]) +
                          exp(support.tmp[2] - support.tmp[3]))
                  end
  nothing
end

function gibbscomputeprobi!(li::Int,
                            support::MCMCSupport)
  # cluster hi does not change => marginal likelihood does not change
  support.t[li] += support.logmlik
  nothing
end

function gibbscomputeprobj!(b::Int,
                            li::Int,
                            lj::Int,
                            support::MCMCSupport)
  support.tmp[1] = support.lpi[1, b] + support.lq[1, li, b] +
                   support.lq[1, lj, b]
  support.tmp[2] = support.lpi[2, b] + support.lq[2, li, b] +
                   support.lq[2, lj, b]
  support.tmp[3] = support.lpi[4, b] + support.lpj[2, b] + support.lpj[3, b]

  support.t[lj] += if (support.tmp[1] >= support.tmp[2]) &&
                      (support.tmp[1] >= support.tmp[3])
                     support.tmp[1] +
                     log1p(exp(support.tmp[2] - support.tmp[1]) +
                           exp(support.tmp[3] - support.tmp[1]))
                   elseif (support.tmp[2] >= support.tmp[1]) &&
                          (support.tmp[2] >= support.tmp[3])
                     support.tmp[2] +
                     log1p(exp(support.tmp[1] - support.tmp[2]) +
                           exp(support.tmp[3] - support.tmp[2]))
                   else
                     support.tmp[3] +
                     log1p(exp(support.tmp[1] - support.tmp[3]) +
                           exp(support.tmp[2] - support.tmp[3]))
                   end

  nothing
end

function gibbscomputenormconst(k::Int,
                               support::MCMCSupport)
  logc = -Inf
  y = 0.0

  for l in 1:k
    if support.t[l] > logc
      logc = support.t[l]
    end
  end

  for l in 1:k
    y += exp(support.t[l] - logc)
  end

  logc += log(y)

  logc
end

function gibbssamplecluster(logc::Float64,
                            k::Int,
                            support::MCMCSupport)
  # find the maximum index for which the following holds
  # c * p <= sum(t[1:l])
  q = logc + log(rand())

  g = 1
  y = support.t[g]

  while (q > y) && (g < k)
    g += 1
    y = support.t[g] + log1p(exp(y - support.t[g]))
  end

  g
end

function gibbsmerge!(i::Int,
                     hi::Int,
                     hj::Int,
                     lg::Int,
                     data::Matrix{UInt8},
                     priorR::PriorRowPartition,
                     support::MCMCSupport,
                     state::AminoAcidState)
  support.logmlik = support.t[lg] - clusterweight(state.v[hj], priorR)

  state.R[i] = hj

  state.emptycluster[hi] = true

  support.k = 0
  for a in 1:length(state.emptycluster)
    if !state.emptycluster[a]
      support.k += 1
      state.cl[support.k] = a
    end
  end

  state.k = support.k

  state.logpR += logratiopriorrowmerge(state.k, state.v[hj], priorR)

  state.v[hj] += 1

  resize!(state.unit[hj], state.v[hj])
  state.unit[hj][state.v[hj]] = i

  for b in 1:support.m
    state.n1s[hj, b] += float(data[b, i])

    support.lp[1, hj, b] = support.lq[1, lg, b]
    support.lp[2, hj, b] = support.lq[2, lg, b]
    support.lp[3, hj, b] = support.lq[3, lg, b]
    support.lp[4, hj, b] = support.lq[4, lg, b]
  end

  nothing
end

function gibbsmove!(i::Int,
                    hi::Int,
                    hj::Int,
                    li::Int,
                    lg::Int,
                    data::Matrix{UInt8},
                    priorR::PriorRowPartition,
                    support::MCMCSupport,
                    state::AminoAcidState)
  support.logmlik = support.t[lg] - clusterweight(state.v[hj], priorR)

  state.logpR += logratiopriorrowmove(state.v[hi], state.v[hj], priorR)

  state.R[i] = hj

  h = 0
  for a in 1:state.v[hi]
    if state.unit[hi][a] != i
      h += 1
      state.unit[hi][h] = state.unit[hi][a]
    end
  end

  state.v[hi] = support.vi
  state.v[hj] += 1

  resize!(state.unit[hj], state.v[hj])
  state.unit[hj][state.v[hj]] = i

  for b in 1:support.m
    state.n1s[hi, b] = support.ni[b]
    state.n1s[hj, b] += float(data[b, i])

    support.lp[1, hi, b] = support.lq[1, li, b]
    support.lp[2, hi, b] = support.lq[2, li, b]
    support.lp[3, hi, b] = support.lq[3, li, b]
    support.lp[4, hi, b] = support.lq[4, li, b]

    support.lp[1, hj, b] = support.lq[1, lg, b]
    support.lp[2, hj, b] = support.lq[2, lg, b]
    support.lp[3, hj, b] = support.lq[3, lg, b]
    support.lp[4, hj, b] = support.lq[4, lg, b]
  end

  nothing
end

function gibbssplit!(i::Int,
                     hi::Int,
                     li::Int,
                     lj::Int,
                     data::Matrix{UInt8},
                     priorR::PriorRowPartition,
                     settings::KSettings,
                     support::MCMCSupport,
                     state::AminoAcidState)
  resizestate!(state, state.k + 1, settings)

  support.logmlik = support.t[lj] - clusterweight(1, state.k, priorR)

  hj = findfirst(state.emptycluster)

  state.R[i] = hj

  state.emptycluster[hj] = false

  support.k = 0
  for a in 1:length(state.emptycluster)
    if !state.emptycluster[a]
      support.k += 1
      state.cl[support.k] = a
    end
  end

  state.k = support.k

  h = 0
  for a in 1:state.v[hi]
    if state.unit[hi][a] != i
      h += 1
      state.unit[hi][h] = state.unit[hi][a]
    end
  end

  state.logpR += logratiopriorrowsplit(state.k, state.v[hi], priorR)

  state.v[hi] = support.vi
  state.v[hj] = 1

  resize!(state.unit[hj], 1)
  state.unit[hj][1] = i

  for b in 1:support.m
    state.n1s[hi, b] = support.ni[b]
    state.n1s[hj, b] = float(data[b, i])

    support.lp[1, hi, b] = support.lq[1, li, b]
    support.lp[2, hi, b] = support.lq[2, li, b]
    support.lp[3, hi, b] = support.lq[3, li, b]
    support.lp[4, hi, b] = support.lq[4, li, b]

    support.lp[1, hj, b] = support.lq[1, lj, b]
    support.lp[2, hj, b] = support.lq[2, lj, b]
    support.lp[3, hj, b] = support.lq[3, lj, b]
    support.lp[4, hj, b] = support.lq[4, lj, b]
  end

  nothing
end


"""
    Magic

`DataType` used for storing magic numbers, rook or bishop move masks, and respective shift and offset values for the computation of sliding moves.
"""
struct Magic
    mask::Bitboard
    num::Bitboard
    shift::UInt8
    #offset::Int32
end
Magic(mask::UInt64, num::UInt64, shift::Integer, offset::Integer) = Magic(Bitboard(mask), Bitboard(num), shift)#, offset)

const ROOK_MAGICS = @SVector [Magic(0x000101010101017e, 0x008000400020801a, 52, 0), Magic(0x000202020202027c, 0x0840004020001003, 53, 4096),
    Magic(0x000404040404047a, 0x8880200010018108, 53, 6144), Magic(0x0008080808080876, 0x0480040800801001, 53, 8192),
    Magic(0x001010101010106e, 0x0900080002050010, 53, 10240), Magic(0x002020202020205e, 0x0200100804010200, 53, 12288),
    Magic(0x004040404040403e, 0x0200010084080200, 53, 14336), Magic(0x008080808080807e, 0x0200024100821224, 52, 16384),
    Magic(0x0001010101017e00, 0x08a0802080004000, 53, 20480), Magic(0x0002020202027c00, 0x0006401004402000, 54, 22528),
    Magic(0x0004040404047a00, 0x0800802000100080, 54, 23552), Magic(0x0008080808087600, 0x4004800803100081, 54, 24576),
    Magic(0x0010101010106e00, 0x0106000810200600, 54, 25600), Magic(0x0020202020205e00, 0x2981000802040100, 54, 26624),
    Magic(0x0040404040403e00, 0x0004000804018210, 54, 27648), Magic(0x0080808080807e00, 0x0201000192026300, 53, 28672),
    Magic(0x00010101017e0100, 0x1038218000804000, 53, 30720), Magic(0x00020202027c0200, 0x1010004000200040, 54, 32768),
    Magic(0x00040404047a0400, 0x0108420012002881, 54, 33792), Magic(0x0008080808760800, 0x2098008008100080, 54, 34816),
    Magic(0x00101010106e1000, 0x4088010004090010, 54, 35840), Magic(0x00202020205e2000, 0x2000808002000400, 54, 36864),
    Magic(0x00404040403e4000, 0x028a0c0008021025, 54, 37888), Magic(0x00808080807e8000, 0x0000220000810064, 53, 38912),
    Magic(0x000101017e010100, 0x0040400480208000, 53, 40960), Magic(0x000202027c020200, 0x8040200040100042, 54, 43008),
    Magic(0x000404047a040400, 0x2000100080802000, 54, 44032), Magic(0x0008080876080800, 0x3090004040080401, 54, 45056),
    Magic(0x001010106e101000, 0x0100080080800400, 54, 46080), Magic(0x002020205e202000, 0x8230020080800400, 54, 47104),
    Magic(0x004040403e404000, 0x0002000200080104, 54, 48128), Magic(0x008080807e808000, 0x0c00010200004084, 53, 49152),
    Magic(0x0001017e01010100, 0x0000400082800020, 53, 51200), Magic(0x0002027c02020200, 0x0400200080804007, 54, 53248),
    Magic(0x0004047a04040400, 0x8400801000802001, 54, 54272), Magic(0x0008087608080800, 0x0018100080800800, 54, 55296),
    Magic(0x0010106e10101000, 0x0810310005004800, 54, 56320), Magic(0x0020205e20202000, 0x8000020080800400, 54, 57344),
    Magic(0x0040403e40404000, 0x2000302134000208, 54, 58368), Magic(0x0080807e80808000, 0x0000298402000645, 53, 59392),
    Magic(0x00017e0101010100, 0x0000800140018022, 53, 61440), Magic(0x00027c0202020200, 0x00184020100c4000, 54, 63488),
    Magic(0x00047a0404040400, 0x6210002804002000, 54, 64512), Magic(0x0008760808080800, 0x0001000c10010020, 54, 65536),
    Magic(0x00106e1010101000, 0x4201000800110004, 54, 66560), Magic(0x00205e2020202000, 0x2002000810020004, 54, 67584),
    Magic(0x00403e4040404000, 0xa40c21480a040090, 54, 68608), Magic(0x00807e8080808000, 0x0000004081020004, 53, 69632),
    Magic(0x007e010101010100, 0x3302410080002300, 53, 71680), Magic(0x007c020202020200, 0x9210002000401040, 54, 73728),
    Magic(0x007a040404040400, 0x0092110041200300, 54, 74752), Magic(0x0076080808080800, 0x0100100109002300, 54, 75776),
    Magic(0x006e101010101000, 0x000a800800240280, 54, 76800), Magic(0x005e202020202000, 0x0100040080020080, 54, 77824),
    Magic(0x003e404040404000, 0x0000506201080400, 54, 78848), Magic(0x007e808080808000, 0x1002209110440600, 53, 79872),
    Magic(0x7e01010101010100, 0x0140800300182241, 52, 81920), Magic(0x7c02020202020200, 0x8280184280220102, 53, 86016),
    Magic(0x7a04040404040400, 0x0005118042000a22, 53, 88064), Magic(0x7608080808080800, 0x008a000890204006, 53, 90112),
    Magic(0x6e10101010101000, 0x000200304844204a, 53, 92160), Magic(0x5e20202020202000, 0x108200080930041a, 53, 94208),
    Magic(0x3e40404040404000, 0x0482000100840842, 53, 96256), Magic(0x7e80808080808000, 0x0012192404430182, 52, 98304)]

const BISHOP_MAGICS = @SVector [Magic(0x0040201008040200, 0x4404700420508600, 58, 0), Magic(0x0000402010080400, 0x9120018401104008, 59, 64),
    Magic(0x0000004020100a00, 0x4004010401100084, 59, 96), Magic(0x0000000040221400, 0x0051040080062000, 59, 128),
    Magic(0x0000000002442800, 0x1124042010010092, 59, 160), Magic(0x0000000204085000, 0x1100882108001000, 59, 192),
    Magic(0x0000020408102000, 0x180a280248040000, 59, 224), Magic(0x0002040810204000, 0x0038440210900400, 58, 256),
    Magic(0x0020100804020000, 0x80074210e1010301, 59, 320), Magic(0x0040201008040000, 0x0020101012004e42, 59, 352),
    Magic(0x00004020100a0000, 0x8210100110411001, 59, 384), Magic(0x0000004022140000, 0x00008404108010a0, 59, 416),
    Magic(0x0000000244280000, 0x2060071040002804, 59, 448), Magic(0x0000020408500000, 0x6400020202211011, 59, 480),
    Magic(0x0002040810200000, 0x000c0d0111202000, 59, 512), Magic(0x0004081020400000, 0x8004050118020221, 59, 544),
    Magic(0x0010080402000200, 0x8005804088080910, 59, 576), Magic(0x0020100804000400, 0x6004a82001020200, 59, 608),
    Magic(0x004020100a000a00, 0x0101001004002041, 57, 640), Magic(0x0000402214001400, 0x8a1800a082004282, 57, 768),
    Magic(0x0000024428002800, 0x8806001012100098, 57, 896), Magic(0x0002040850005000, 0x0022810040504000, 57, 1024),
    Magic(0x0004081020002000, 0x4040800048480801, 59, 1152), Magic(0x0008102040004000, 0x0a42041304824120, 59, 1184),
    Magic(0x0008040200020400, 0x0204106085200814, 59, 1216), Magic(0x0010080400040800, 0x0890110682040111, 59, 1248),
    Magic(0x0020100a000a1000, 0x4000500001040080, 57, 1280), Magic(0x0040221400142200, 0x0000808018020102, 55, 1408),
    Magic(0x0002442800284400, 0x2401010010104008, 55, 1920), Magic(0x0004085000500800, 0x1052008004100080, 57, 2432),
    Magic(0x0008102000201000, 0x0000950104090801, 59, 2560), Magic(0x0010204000402000, 0x1200808000220864, 59, 2592),
    Magic(0x0004020002040800, 0x8188421201082050, 59, 2624), Magic(0x0008040004081000, 0x0035011001a05c00, 59, 2656),
    Magic(0x00100a000a102000, 0x0051480201104400, 57, 2688), Magic(0x0022140014224000, 0x4110040400180210, 55, 2816),
    Magic(0x0044280028440200, 0x0414140400001100, 55, 3328), Magic(0x0008500050080400, 0x4e60008100688040, 57, 3840),
    Magic(0x0010200020100800, 0x000121022ac40200, 59, 3968), Magic(0x0020400040201000, 0x00040042904a0094, 59, 4000),
    Magic(0x0002000204081000, 0x2948010421001040, 59, 4032), Magic(0x0004000408102000, 0x4060821011020200, 59, 4064),
    Magic(0x000a000a10204000, 0x0001004030008202, 57, 4096), Magic(0x0014001422400000, 0x0018020122080401, 57, 4224),
    Magic(0x0028002844020000, 0x0042ca41a2000400, 57, 4352), Magic(0x0050005008040200, 0x01c0212040810100, 57, 4480),
    Magic(0x0020002010080400, 0x1060480f41020041, 59, 4608), Magic(0x0040004020100800, 0x020102020a040040, 59, 4640),
    Magic(0x0000020408102000, 0x1082008220100402, 59, 4672), Magic(0x0000040810204000, 0x0102020202828102, 59, 4704),
    Magic(0x00000a1020400000, 0x4022108848080004, 59, 4736), Magic(0x0000142240000000, 0x0020400508480000, 59, 4768),
    Magic(0x0000284402000000, 0x1010108590440010, 59, 4800), Magic(0x0000500804020000, 0xd812040810a10140, 59, 4832),
    Magic(0x0000201008040200, 0x0191043000a20038, 59, 4864), Magic(0x0000402010080400, 0x8010020200620000, 59, 4896),
    Magic(0x0002040810204000, 0x0001008801084280, 58, 4928), Magic(0x0004081020400000, 0x0000060882015050, 59, 4992),
    Magic(0x000a102040000000, 0x8120000100809001, 59, 5024), Magic(0x0014224000000000, 0x0200100002104420, 59, 5056),
    Magic(0x0028440200000000, 0x10d100200821010a, 59, 5088), Magic(0x0050080402000000, 0x03002c0448100110, 59, 5120),
    Magic(0x0020100804020000, 0x14004005045c0042, 59, 5152), Magic(0x0040201008040200, 0x4002901008831040, 58, 5184)]

function subindex(occupied::Bitboard, magics::Magic)
    (((occupied & magics.mask).val * magics.num.val) >> magics.shift) + 1
end

# obtain the table index using the shift, occupancy mask, and magic number. Adding the offset.
function tableindex(occupied::Bitboard, magics::Magic)
    subindex(occupied, magics) + magics.offset
end

# generate all possible sliding moves in a given move direction, for a given occupancy.
function slidermoves(sqr::Bitboard, occupied::Bitboard, directions::SArray{Tuple{4},Function,1,4})
    mask = EMPTY
    for direction in directions
    	newsqr = sqr
		while !isempty(newsqr)
			newsqr = direction(newsqr)
    		mask ⊻= newsqr
    		!isempty(newsqr & occupied) && break
    	end
    end
    return mask
end

# initiate the attacks table for rook and bishop pieces
function initSlidingTable(table::Vector{Vector{Bitboard}}, magics::SArray{Tuple{64}, Magic, 1, 64}, directions::SArray{Tuple{4},Function,1,4})
	@inbounds for sqr in 1:64
		occupied = EMPTY
		table[sqr] = Vector{Bitboard}(undef, 2^count(magics[sqr].mask))
		for i in 1:(1 << count(magics[sqr].mask))
			idx = subindex(occupied, magics[sqr])
			table[sqr][idx] = slidermoves(Bitboard(sqr), occupied, directions)
			occupied = Bitboard(occupied.val - magics[sqr].mask.val) & magics[sqr].mask
		end
	end
	return table
end