using ClinicalTrialSampleSize
using Base.Test

# TwoSampleMean
# Constructor: TwoSampleMean()
@test_throws ErrorException TwoSampleMean(-Inf, Inf, 1)
@test_throws ErrorException TwoSampleMean(25, 20, 0)

params = (
    (TwoSampleMean(3, 2, 1, true), 16.722, 0.8, 1, 0.05, "two"),
    (TwoSampleMean(10, 7, 2), 43.780, 0.9, 5, 0.05, "two"),
    (TwoSampleMean(7, 4, 1, true), 13.842, 0.9, 3, 0.1, "one"),
    (TwoSampleMean(7, 4, 1), 13.139, 0.9, 3, 0.1, "one"),
    (TwoSampleMean(50, 45, 1.5), 137.123, 0.85, (10.4, 15.2), 0.01, "one"),
    (TwoSampleMean(50, 45, 1.5, true), 138.142, 0.85, (10.4, 15.2), 0.01, "one"),
    (TwoSampleMean(120, 114, 1), 171.548, 0.95, (13, 17.5), 0.05, "two"),
    (TwoSampleMean(120, 114, 1, true), 172.634, 0.95, (13, 17.5), 0.05, "two"),
)

for (test, n, p, std, alpha, side) in params

        p_est = power(test; n = n, std = std, alpha = alpha, side = side)
        n_est = sample_size(test; power = p, std = std, alpha = alpha, side = side)

        @test abs(p_est - p) < 1e-3
        @test abs(n_est - n) < 1

end # end for

# Two-sample non-inferiority/superiority for mean
# Constructor: TwoSampleMeanInferior/Superior()
@test_throws ErrorException TwoSampleMeanInferior(-Inf, 5, 1, -1)
@test_throws ErrorException TwoSampleMeanInferior(30, 35, -3, -3)
@test_throws ErrorException TwoSampleMeanInferior(30, 35, 1, 3)
@test_throws ErrorException TwoSampleMeanSuperior(-Inf, Inf, 1, 1)
@test_throws ErrorException TwoSampleMeanSuperior(35, 30, -2, 5)
@test_throws ErrorException TwoSampleMeanSuperior(35, 30, 1, -3)

params = (
    (TwoSampleMeanInferior(2, 2, 1, -1, true), 13.170, 0.8, 1, 0.05),
    (TwoSampleMeanInferior(1.9, 2.5, 2, -1), 16.850, 0.85, 0.5, 0.05),
    (TwoSampleMeanInferior(1.94, 2.0, 1, -2.3), 46.286, 0.95, (1.1, 4.5), 0.05),
    (TwoSampleMeanInferior(1.94, 2.0, 1, -2.3, true), 47.227, 0.95, (1.1, 4.5), 0.05),
    (TwoSampleMeanSuperior(2.5, 2, 2, 0), 45.710, 0.8, 1.3, 0.1),
    (TwoSampleMeanSuperior(20, 15, 1, 3, true), 81.810, 0.85, 5.5, 0.1),
    (TwoSampleMeanSuperior(121, 119, 1.5, 2.3), 8278.386, 0.95, (3, 9), 0.1),
    (TwoSampleMeanSuperior(121, 119, 1.5, 2.3, true), 8278.324, 0.95, (3, 9), 0.1),
)

for (test, n, p, std, alpha) in params

        p_est = power(test; n = n, std = std, alpha = alpha)
        n_est = sample_size(test; power = p, std = std, alpha = alpha)

        @test abs(p_est - p) < 1e-3
        @test abs(n_est - n) < 1

end # end for

# Two-sample equivalence for mean
# Constructor: TwoSampleMeanEqual()
@test_throws ErrorException TwoSampleMeanEqual(-Inf, Inf, 1, 1)
@test_throws ErrorException TwoSampleMeanEqual(30, 30, -1, 0)
@test_throws ErrorException TwoSampleMeanEqual(30, 30, 1, -5)

params = (
    (TwoSampleMeanEqual(2, 2, 1, 0.3), 190.308, 0.8, 1, 0.05),
    (TwoSampleMeanEqual(1.94, 1.95, 2, 0, true), 8919.364, 0.85, 0.25, 0.05),
    (TwoSampleMeanEqual(112, 110, 1, 1.7), 15542.81, 0.95, (5.2, 10.3), 0.1),
    (TwoSampleMeanEqual(59, 54, 2, 3, true), 23.843, 0.8, (4, 2.5), 0.1),
)

for (test, n, p, std, alpha) in params

        p_est = power(test; n = n, std = std, alpha = alpha)
        n_est = sample_size(test; power = p, std = std, alpha = alpha)

        @test abs(p_est - p) < 1e-3
        @test abs(n_est - n) < 1

end # end for

# Two-sample proportion
# Constructor: TwoSampleProp()
@test_throws ErrorException TwoSampleProp(-1.2, 1.2, 1)
@test_throws ErrorException TwoSampleProp(0.3, 0.25, -1)

params = (
    (TwoSampleProp(0.5, 0.2, 1), 35.756, 0.8, 0.05, "two"),
    (TwoSampleProp(0.9, 0.4, 2), 10.235, 0.85, 0.05, "two"),
    (TwoSampleProp(0.44, 0.4, 1), 2603.41, 0.95, 0.1, "one"),
)

for (test, n, p, alpha, side) in params

        p_est = power(test; n = n, alpha = alpha, side = side)
        n_est = sample_size(test; power = p, alpha = alpha, side = side)

        @test abs(p_est - p) < 1e-3
        @test abs(n_est - n) < 1

end # end for

# Two-sample non-inferiority/superiority proportion
# Constructor: TwoSamplePropInferior/Superior
@test_throws ErrorException TwoSamplePropInferior(-1.2, 1.2, 1, 0)
@test_throws ErrorException TwoSamplePropInferior(0.3, 0.2, -2, -0.3)
@test_throws ErrorException TwoSamplePropInferior(0.3, 0.2, 1, 0.1)
@test_throws ErrorException TwoSamplePropSuperior(1.1, 0.3, 1, 0)
@test_throws ErrorException TwoSamplePropSuperior(0.4, 0.2, -1, 0.2)
@test_throws ErrorException TwoSamplePropSuperior(0.4, 0.2, 1, -0.2)

params = (
    (TwoSamplePropInferior(0.25, 0.3, 1, -0.06), 24575.66, 0.8, 0.05),
    (TwoSamplePropInferior(0.71, 0.8, 2, -0.05), 883.028, 0.85, 0.1),
    (TwoSamplePropSuperior(0.7, 0.5, 1, 0.1), 393.937, 0.9, 0.05),
    (TwoSamplePropSuperior(0.32, 0.27, 1.5, 0.01), 1149.05, 0.85, 0.1),
)

for (test, n, p, alpha) in params

        p_est = power(test; n = n, alpha = alpha)
        n_est = sample_size(test; power = p, alpha = alpha)

        @test abs(p_est - p) < 1e-3
        @test abs(n_est - n) < 1

end # end for

# Two-sample equivalence proportion
# Constructor: TwoSamplePropEqual
@test_throws ErrorException TwoSamplePropEqual(1.9, 1.2, 1, 0.1)
@test_throws ErrorException TwoSamplePropEqual(0.2, 0.2, -1, 0)
@test_throws ErrorException TwoSamplePropEqual(0.2, 0.2, 1, -2)

params = (
    (TwoSamplePropEqual(0.35, 0.3, 1, 0.06), 37466.83, 0.8, 0.05),
    (TwoSamplePropEqual(0.85, 0.8, 1, 0.1), 851.494, 0.85, 0.1),
    (TwoSamplePropEqual(0.4, 0.4, 2, 0.02), 9456.681, 0.95, 0.1),
)

for (test, n, p, alpha) in params

        p_est = power(test; n = n, alpha = alpha)
        n_est = sample_size(test; power = p, alpha = alpha)

        @test abs(p_est - p) < 1e-3
        @test abs(n_est - n) < 1

end # end for

# McNemar's paired test
# Constructor: McNemarProp
@test_throws ErrorException McNemarProp(1.9, 0.3)

params = (
    (McNemarProp(0.35, 0.5), 294.144, 0.8, 0.05, "two"),
    (McNemarProp(0.85, 0.8), 4742.158, 0.85, 0.1, "two"),
    (McNemarProp(0.4, 0.2), 123.608, 0.95, 0.1, "one"),
)

for (test, n, p, alpha, side) in params

        p_est = power(test; n = n, alpha = alpha, side = side)
        n_est = sample_size(test; power = p, alpha = alpha, side = side)

        @test abs(p_est - p) < 1e-3
        @test abs(n_est - n) < 1

end # end for

# Two sample crossover test for mean
# Constructor: CrossoverMean
@test_throws ErrorException CrossoverMean(-Inf, Inf)

params = (
    (CrossoverMean(4.35, 3.5, true), 136.774, 0.8, 5, 0.05, "two"),
    (CrossoverMean(0.85, 0.8), 23487.195, 0.85, 3, 0.01, "two"),
    (CrossoverMean(10.4, 10.2, true), 8.127, 0.9, 0.3, 0.1, "one"),
    (CrossoverMean(1.4, 0.2), 11.894, 0.95, 2, 0.1, "one"),
)

for (test, n, p, std, alpha, side) in params

        p_est = power(test; n = n, std = std, alpha = alpha, side = side)
        n_est = sample_size(test; power = p, std = std, alpha = alpha, side = side)

        @test abs(p_est - p) < 1e-3
        @test abs(n_est - n) < 1

end # end for

return true


module Absolute_Loss
    using LoopVectorization

    function func(output_matrix::Array{Float32}, sample_matrix::Array{Float32})
        loss_matrix = zeros(Float32, size(output_matrix))
        @avxt for i in axes(output_matrix, 1), j in axes(output_matrix, 2)
            loss_matrix[i,j] = abs(output_matrix[i,j]-sample_matrix[i,j])
        end
        return loss_matrix
    end

    function prop!(δ::Array{Float32}, output_matrix::Array{Float32}, sample_matrix::Array{Float32})
        @avxt for i in eachindex(δ)
            δ[i] = ifelse(output_matrix[i]-sample_matrix[i]>=0, 1, -1)
        end
    end
end


"""
    unitsphere(ndirection; half = false, normal = [0, 0, 1])

Create `ndirection` directions unformly distributed on the unit sphere.

If `half` is `true`, the points will be discributed on the hemisphere defined by `normal`.
"""
function unitsphere(ndirection; half = false, normal = [0, 0, 1])
    # Create Fibonacci distributed directions
    directions = create_fibonacci_sphere(ndirection)
end


## Utility functions

# Sum of square differences
sosdiff(a::Number,b::Number) = abs2(a-b)
sosdiff(A,B) = sum(ab -> sosdiff(ab...),zip(A,B))

# Hard thresholding
hard(x, beta) = abs(x) < beta ? zero(x) : x

# Partial objective
_obj(zlk,λ) = sum(z -> (abs(z) < sqrt(2λ) ? abs2(z)/2 : λ), zlk)

# Conversions between filter forms
_filtermatrix(hlist) =
    (hcat([vec(h) for h in hlist]...)::Matrix{eltype(first(hlist))},
     size(first(hlist)))
_filterlist(Hmatrix,R) = [reshape(h,map(n->n,R)) for h in eachcol(Hmatrix)]


# adjoint-rotate.jl
# test adjoint consistency for rotate methods on very small case

using SPECTrecon: plan_rotate
using SPECTrecon: imrotate, imrotate_adj
using SPECTrecon: imrotate!, imrotate_adj!
using LinearMapsAA: LinearMapAA
using Test: @test, @testset

@testset "rotate" begin
    nx = 10
    plan = plan_rotate(nx)
    show(isinteractive() ? stdout : devnull, "text/plain", plan)
    show(isinteractive() ? stdout : devnull, "text/plain", plan[1])
    @test sizeof(plan) isa Int
    @test sizeof(plan[1]) isa Int
end

@testset "rotate3" begin
    for method in (:one, :two)
        nx = 20
        θ = π/6
        T = Float32
        plan = plan_rotate(nx; T, method)
        image3 = rand(T, nx, nx, 4)
        result1 = similar(image3)
        result2 = similar(result1)
        imrotate!(result1, image3, θ, plan)
        @test maximum(result1) ≤ 1.01
        imrotate!(result2, image3, θ, plan, :thread)
        @test result1 == result2

        imrotate_adj!(result1, image3, θ, plan)
        @test maximum(result1) ≤ 1.5
        imrotate_adj!(result2, image3, θ, plan, :thread)
        @test result1 == result2
    end
end

@testset "adjoint-rotate" begin
    Ntest = 7
    θ_list = (0:Ntest-1) / Ntest * 2π
    M = 16
    N = M
    T = Float32

    for method in (:one, :two), θ in θ_list
        idim = (M, N)
        odim = (M, N)
        x = randn(T, M, N)
        forw = x -> imrotate(x, θ; method)
        back = x -> imrotate_adj(x, θ; method)
        A = LinearMapAA(forw, back, (prod(odim), prod(idim)); T, odim, idim)
        @test Matrix(A') ≈ Matrix(A)'
    end
end


using Waterfall
include(joinpath(WATERFALL_DIR,"bin","io.jl"))
include(joinpath(WATERFALL_DIR,"src","utils","correlate.jl"))

function cutoff(df::DataFrames.DataFrame; nstep=6, value=:Value)
    ii = [1; sortperm(abs.(df[2:end-1,value]); rev=true)[1:nstep].+1; size(df,1)]
    ii = 1:nstep
    return df[ii,:]
end

function plot(x::Waterfall.Cascade)
    A = copy.(x.correlation-I)
    df = convert(DataFrames.DataFrame, Waterfall.rowprod!(x))
    N = size(df,2)

    StatsPlots.gr(size=(1000,1000), markerstrokewidth=1)

    s = StatsPlots.@df df StatsPlots.corrplot(cols(1:N),
        grid = false,
        markeralpha = 0.4,
        tickfontsize = 8,
        xaxis = (tickfontrotation = 60.0),
    )

    [StatsPlots.annotate!(s[ii,jj],
        Statistics.mean(StatsPlots.xlims(s[ii,jj])),
        StatsPlots.ylims(s[ii,jj])[end],
        StatsPlots.text(Printf.@sprintf("%.2g", A[ii-1,jj-1]), :center, :middle, 8)
        )
        for ii in 3:N-1 for jj in 2:ii-1 if A[ii-1,jj-1]!==0.0
    ]

    StatsPlots.savefig("correlation.png")
end

nstep = 4
nsample = 1000
nperm = 10
ncor = 2

plot(Waterfall.Cascade(cutoff(df; nstep=nstep);
    interactivity=(0.5,0.75),
    nsample=nsample,
    ncor=ncor,
    kwargs...),
)

# plot_correlation(cascade)
# plot_cascade(cascade)


#-----------------------------------------------------------------------# common
abstract type HistogramStat{T} <: OnlineStat{T} end

# Index of `edges` that `y` belongs in, depending on if bins are `left` closed and if
# the bin on the end is `closed` instead of half-open.
function binindex(edges::AbstractVector, y, left::Bool, closed::Bool)
    a, b = extrema(edges)
    y < a && return 0
    y > b && return length(edges)
    closed && y == a && return 1
    closed && y == b && return length(edges) - 1
    if left
        if isa(edges, AbstractRange)
            return floor(Int, (y - a) / step(edges)) + 1
        else
            return searchsortedlast(edges, y)
        end
    else
        if isa(edges, AbstractRange)
            return ceil(Int, (y - a) / step(edges))
        else
            return searchsortedfirst(edges, y) - 1
        end
    end
end

# requires: edges(o), midpoints(o), counts(o)
split_candidates(o::HistogramStat) = midpoints(o)
Statistics.mean(o::HistogramStat) = mean(midpoints(o), fweights(counts(o)))
Statistics.var(o::HistogramStat) = var(midpoints(o), fweights(counts(o)); corrected=true)
Statistics.std(o::HistogramStat) = sqrt(var(o))
Statistics.median(o::HistogramStat) = quantile(o, .5)
function Base.extrema(o::HistogramStat)
    x, y = midpoints(o), counts(o)
    x[findfirst(x -> x > 0, y)], x[findlast(x -> x > 0, y)]
end
function Statistics.quantile(o::HistogramStat, p = [0, .25, .5, .75, 1])
    x, y = midpoints(o), counts(o)
    inds = findall(x -> x != 0, y)
    quantile(x[inds], fweights(y[inds]), p)
end

#-----------------------------------------------------------------------# Hist
"""
    Hist(edges; left = true, closed = true)

Create a histogram with bin partition defined by `edges`.

- If `left`, the bins will be left-closed.
- If `closed`, the bin on the end will be closed.
    - E.g. for a two bin histogram ``[a, b), [b, c)`` vs. ``[a, b), [b, c]``

# Example

    o = fit!(Hist(-5:.1:5), randn(10^6))

    # approximate statistics
    using Statistics

    mean(o)
    var(o)
    std(o)
    quantile(o)
    median(o)
    extrema(o)
"""
struct Hist{T, R} <: HistogramStat{T}
    edges::R
    counts::Vector{Int}
    out::Vector{Int}
    left::Bool
    closed::Bool

    function Hist(edges::R, T::Type = eltype(edges); left::Bool=true, closed::Bool=true) where {R<:AbstractVector}
        new{T,R}(edges, zeros(Int, length(edges) - 1), [0,0], left, closed)
    end
end
nobs(o::Hist) = sum(o.counts) + sum(o.out)
value(o::Hist) = (x=o.edges, y=o.counts)

midpoints(o::Hist) = midpoints(o.edges)
counts(o::Hist) = o.counts
edges(o::Hist) = o.edges

function area(o::Hist)
    c = o.counts
    e = o.edges
    if isa(e, AbstractRange)
        return step(e) * sum(c)
    else
        return sum((e[i+1] - e[i]) * c[i] for i in 1:length(c))
    end
end

function pdf(o::Hist, y)
    i = binindex(o.edges, y, o.left, o.closed)
    if i < 1 || i > length(o.counts)
        return 0.0
    else
        return o.counts[i] / area(o)
    end
end

function _fit!(o::Hist, y)
    i = binindex(o.edges, y, o.left, o.closed)
    if 1 ≤ i < length(o.edges)
        o.counts[i] += 1
    else
        o.out[1 + (i > 0)] += 1
    end
end

function _merge!(o::Hist, o2::Hist)
    if o.edges == o2.edges
        for j in eachindex(o.counts)
            o.counts[j] += o2.counts[j]
        end
    else
        @warn("Histogram edges are not aligned.  Merging is approximate.")
        for (yi, wi) in zip(midpoints(o2.edges), o2.counts)
            for k in 1:wi
                _fit!(o, yi)
            end
        end
    end
    for j in eachindex(o.out)
        o.out[j] += o2.out[j]
    end
end

#-----------------------------------------------------------------------# ExpandingHist
const ExpandableRange = Union{StepRange, StepRangeLen, LinRange}

"""
    ExpandingHist(nbins)

An adaptive histogram where the bin edges keep doubling in size in order to contain every observation.
`nbins` must be an even number.  Bins are left-closed and the rightmost bin is closed, e.g.

- `[a, b)`, `[b, c)`, `[c, d]`

# Example
    o = fit!(ExpandingHist(200), randn(10^6))

    using Plots
    plot(o)

# Details

How `ExpandingHist` works is best understood through example.  Suppose we start with a histogram
of edges/counts as follows:

```
|1|2|5|3|2|
```

- Now we observe a data point that is not contained in the bin edges:

```
|1|2|5|3|2|       *
```

- In order to contain the point, the range of the edges doubles in the direction of the new
  data point and adjacent bins merge their counts:

```
|1|2|5|3|2|       *
 \\ / \\ / \\ /      ↓
  ↓   ↓   ↓       ↓
| 3 | 8 | 2 | 0 | 1 |
```

- Note that multiple iterations of bin-doubling may occur until the new point is contained by the
  bin edges.
"""
mutable struct ExpandingHist{T, R <: StepRangeLen} <: HistogramStat{T}
    edges::R
    counts::Vector{Int}
    n::Int
    function ExpandingHist(init::R, T::Type=Number) where {R <: ExpandableRange}
        new{T, R}(init, zeros(Int, length(init) - 1), 0)
    end
end
function ExpandingHist(b::Int)
    @assert iseven(b)
    ExpandingHist(range(0, stop=0, length = b + 1), Number)
end
value(o::ExpandingHist) = (x=o.edges, y=o.counts)

Base.show(io::IO, o::ExpandingHist) = print(io, "ExpandingHist | n = $(nobs(o)) | nbins = $(length(o.counts))")

midpoints(o::ExpandingHist) = midpoints(o.edges)
counts(o::ExpandingHist) = o.counts
edges(o::ExpandingHist) = o.edges

function _fit!(o::ExpandingHist, y)
    isfinite(y) || error("ExpandingHist requires finite values.  Found $y.")
    o.n += 1

    # init
    @inbounds if nobs(o) == 1
        o.edges = range(y, stop=y, length=length(o.edges))
        o.counts[1] = 1
    elseif step(o.edges) == 0.0
        a = o.edges[1]
        if y == a
            o.counts[1] += 1
        elseif y < a
            o.edges = range(y, stop=a, length=length(o.edges))
            o.counts[end] = o.counts[1]
            o.counts[1] = 1
        else
            o.edges = range(a, stop=y, length=length(o.edges))
            o.counts[end] = 1
        end
    else
        expand!(o, y)
        i = binindex(o.edges, y, true, true)
        o.counts[i] += 1
    end
end

function expand!(o::ExpandingHist, y)
    a, b = extrema(o.edges)
    w = b - a
    w = w == 0.0 ? y - a : w
    halfnbin = round(Int, length(o.counts) / 2)
    @inbounds if y > b  # find K such that y <= a + 2^K * w
        K = ceil(Int, log2((y - a) / w))
        C = 2 ^ K
        o.edges = range(a, stop = a + C*w, length=length(o.edges))
        for _ in 1:min(K, halfnbin)
            for i in eachindex(o.counts)
                o.counts[i] = i ≤ halfnbin ? sum(view(o.counts, (2i-1):(2i))) : 0
            end
        end
    elseif y < a # find K such that y >= b - 2^K * w
        K = ceil(Int, log2((b - y) / w))
        C = 2 ^ K
        o.edges = range(b - C*w, stop = b, length=length(o.edges))
        for _ in 1:min(K, halfnbin)
            n = length(o.counts)
            for i in 0:(length(o.counts) - 1)
                rng = (n-2i-1):(n-2i)
                o.counts[end-i] = i+1 ≤ halfnbin ? sum(view(o.counts, rng)) : 0
            end
        end
    end
end


using PyPlot

function calculate_midpoint(a,b)
  x,y = ((a[1]+b[1])/2), ((a[2]+b[2])/2)
  return (x,y)
end

function sierpinski(maxiter,pts)
  while maxiter > 0
    a = pts[4]
    b = pts[rand(1:3)]
    pts[4] = calculate_midpoint(a,b)
    ax[:scatter](pts[4][1],pts[4][2])
    maxiter = maxiter -1
  end
end

fig,ax = subplots()
p = [(1.0,0.0),(2.5,5.0),(5.0,0.0),(0.0,0.0)]
sierpinski(1500,p)


"""
    clockwise(line::Union{LineString, Vector{Position}})::Bool

Take a ring and return true or false whether or not the ring is clockwise or counter-clockwise.

# Examples
```jldoctest
julia> line = LineString([[0, 0], [1, 1], [1, 0], [0, 0]])
LineString(Array{Float64,1}[[0.0, 0.0], [1.0, 1.0], [1.0, 0.0], [0.0, 0.0]])

julia> clockwise(line)
true
```
"""
function clockwise(line::Union{LineString, Vector{Position}})::Bool

    let ring end
    # geotype fails with Vector{Vector{...}}
    if geotype(line) === :LineString
        ring = line.coordinates
    else
        ring = line[1]
    end

    sum = 0
    i = 1

    prev = []
    cur = []
    while i < length(ring)
        prev = isempty(cur) ? ring[1] : cur
        cur = ring[i]
        sum += (cur[1] - prev[1]) * (cur[2] + prev[2])
        i += 1
    end

    return sum > 0
end

"""
    concave(poly::Polygon)::Bool

Take a polygon and return true or false as to whether it is concave or not.

# Examples
```jldoctest
julia> poly = Polygon([[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]])
Polygon(Array{Array{Float64,1},1}[[[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0], [0.0, 0.0]]])

julia> concave(poly)
false
```
"""
function concave(poly::Polygon)::Bool
    coords = poly.coordinates

    length(coords[1]) <= 4 && return false

    sign = false
    n = length(coords[1]) - 1

    for i in 1:n
        j = ((i + 1) % n) === 0 ? 1 : (i + 1) % n
        m = ((i + 2) % n) === 0 ? 1 : (i + 2) % n

        dx1 = coords[1][m][1] - coords[1][j][1]
        dy1 = coords[1][m][2] - coords[1][j][2]
        dx2 = coords[1][i][1] - coords[1][j][1]
        dy2 = coords[1][i][2] - coords[1][j][2]

        cross = (dx1 * dy2) - (dy1 * dx2)

        if i === 0
            sign = cross > 0
        elseif sign !== (cross > 0)
            return true
        end
    end

    return false
end

function equal(geo1::T, geo2::T) where {T <: AbstractGeometry}
    geotype(geo1) !== geotype(geo2) && return false

    geotype(geo1) === :Point && return comparePoints(geo1.coordinates, geo2.coordinates)
    geotype(geo1) === :LineString && return compareLines(geo1.coordinates, geo2.coordinates)
end

function comparePoints(p1::Position, p2::Position)
    length(p1) !== length(p2) && return false

    for i in eachindex(p1)
        round(p1[i]; digits=10) !== round(p2[i]; digits=10) && return false
    end

    return true
end

function compareLines(p1::Vector{Position}, p2::Vector{Position})
    # TODO: complete this
    length(p1[1]) !== length(p2[1]) && return false
end

"""
    parallel(line1::LineString, line2::LineString)::Bool

Return `true` if each segment of `line1` is parallel to the correspondent segment of `line2`

# Examples
```jldoctest
julia> line1 = LineString([[9.170356, 45.477985], [9.164434, 45.482551], [9.166644, 45.484003]])
LineString(Array{Float64,1}[[9.17036, 45.478], [9.16443, 45.4826], [9.16664, 45.484]])

julia> line2 = LineString([[9.169356, 45.477985], [9.163434, 45.482551], [9.165644, 45.484003]])
LineString(Array{Float64,1}[[9.16936, 45.478], [9.16343, 45.4826], [9.16564, 45.484]])

julia> parallel(line1, line2)
true
```
"""
function parallel(line1::LineString, line2::LineString)::Bool
    seg1 = linesegment(line1)
    seg2 = linesegment(line2)

    for i in eachindex(seg1)
        coors2 = nothing
        coors1 = seg1[i].coordinates

        try
            coors2 = seg2[i].coordinates
        catch e
            isa(e, BoundsError) && break

        end
        isParallel(coors1, coors2) == false && return false
    end

    return true
end

@inline function isParallel(p1::Vector{Position}, p2::Vector{Position})
    slope1 = bearing_to_azimuth(rhumb_bearing(p1[1], p2[1]))
    slope2 = bearing_to_azimuth(rhumb_bearing(p1[2], p2[2]))

    return slope1 === slope2
end

"""
    point_on_line(point::Point, line::LineString, ignoreEndVertices::Bool=false)::Bool

Return true if a point is on a line. Accept a optional parameter to ignore the
start and end vertices of the linestring.

# Examples
```jldoctest
julia> point = Point([1,1])
Point([1.0, 1.0])

julia> line = LineString([[0, 0], [3, 3], [4, 4]])
LineString(Array{Float64,1}[[0.0, 0.0], [3.0, 3.0], [4.0, 4.0]])

julia> point_on_line(point, line)
true
```
"""
function point_on_line(point::Point, line::LineString, ignoreEndVertices::Bool=false)::Bool
    pCoords = point.coordinates
    lCoords = line.coordinates

    ignore = "none"
    for i in 1:length(lCoords) - 1
        if ignoreEndVertices == true
            i === 1 && (ignore = "start")
            i === length(lCoords) - 2 && (ignore = "end")
            (i === 1 && i + 1 === length(lCoords) - 1) && (ignore = "both")
        end
        isPointOnSegment(lCoords[i], lCoords[i + 1], pCoords, ignore) && return true
    end
    return false
end

@inline function isPointOnSegment(start::Position, stop::Position, coords::Position, excludeBoundary::String="none")::Bool
    x, y = coords
    x1, y1 = start
    x2, y2 = stop

    dxc = x - x1
    dyc = y - y1
    dx1 = x2 - x1
    dy1 = y2 - y1

    cross = dxc * dy1 - dyc * dx1
    cross != 0 && return false

    if excludeBoundary === "none"
        if abs(dx1) >= abs(dy1)
            return dx1 > 0 ? x1 <= x && x <= x2 : x2 <= x && x <= x1
        end
        return dy1 > 0 ? y1 <= y && y <= y2 : y2 <= y && y <= y1
    elseif excludeBoundary === "start"
        if abs(dx1) >= abs(dy1)
             return dx1 > 0 ? x1 < x && x <= x2 : x2 <= x && x < x1
        end
        return dy1 > 0 ? y1 < y && y <= y2 : y2 <= y && y < y1
    elseif excludeBoundary === "end"
        if abs(dx1) >= abs(dy1)
            return dx1 > 0 ? x1 <= x && x < x2 : x2 < x && x <= x1
        end
        return dy1 > 0 ? y1 <= y && y < y2 : y2 < y && y <= y1
    elseif excludeBoundary === "both"
        if abs(dx1) >= abs(dy1)
            return dx1 > 0 ? x1 < x && x < x2 : x2 < x && x < x1
        end
        return dy1 > 0 ? y1 < y && y < y2 : y2 < y && y < y1
    end
    return false
end

"""
    point_in_polygon(point::Point, polygon::Union{Polygon, MultiPolygon}, ignoreBoundary::Bool=false)::Bool

Take a Point and a Polygon and determine if the point
resides inside the polygon. The polygon can be convex or concave. The function accounts for holes.

# Examples
```jldoctest
julia> point = Point([-77, 44])
Point([-77.0, 44.0])

julia> poly = Polygon([[ [-81, 41], [-81, 47], [-72, 47], [-72, 41], [-81, 41]]])
Polygon(Array{Array{Float64,1},1}[[[-81.0, 41.0], [-81.0, 47.0], [-72.0, 47.0], [-72.0, 41.0], [-81.0, 41.0]]])

julia> point_in_polygon(point, poly)
true
```
"""
function point_in_polygon(point::Point, polygon::Union{Polygon, MultiPolygon}, ignoreBoundary::Bool=false)::Bool

    pt = point.coordinates
    poly = polygon.coordinates

    inBBox(pt, bbox(polygon)) == false && return false

    geotype(polygon) === :Polygon && (poly = [poly])

    inside = false
    for i in eachindex(poly)
        if inRing(pt, poly[i][1], ignoreBoundary)
            inHole = false
            k = 1

            while k < length(poly[i]) && !inHole
                inRing(pt, poly[i][k], !ignoreBoundary) == true && (inHole = true)
                k += 1
            end

            !inHole && (inside = true)
        end
    end
    return inside
end

function inRing(pt::Position, ring::Vector{Position}, ignoreBoundary::Bool=false)
    inside = false

    (ring[1][1] == ring[length(ring) - 1][1] && ring[1][2] == ring[length(ring) - 1][1]) && (ring = ring[1, length(ring) - 1])

    for i in 1:length(ring) - 1
        j = i + 1

        xi = ring[i][1]
        yi = ring[i][2]
        xj = ring[j][1]
        yj = ring[j][2]

        onBoundary = (pt[2] * (xi - xj) + yi * (xj - pt[1]) + yj * (pt[1] - xi) == 0) &&
            ((xi - pt[1]) * (xj - pt[1]) <= 0) && ((yi - pt[2]) * (yj - pt[2]) <= 0)

        onBoundary && return !ignoreBoundary

        intersect = ((yi > pt[2]) !== (yj > pt[2])) && (pt[1] < (xj - xi) * (pt[2] - yi) / (yj - yi) + xi)

        intersect && (inside =  !inside)
    end

    return inside
end

function inBBox(pt::Position, bbox::Vector{Float64})
    return bbox[1] <= pt[1] &&  bbox[2] <= pt[2] &&
        bbox[3] >= pt[1] && bbox[4] >= pt[2]
end

"""
    contains(ft1::AbstractGeometry, ft2::AbstractGeometry)::Bool

Return true if the second geometry is completely contained by the first geometry.
The interiors of both geometries must intersect and, the interior and boundary of the secondary (geometry b)
must not intersect the exterior of the primary (geometry a).
`contains` returns the exact opposite result of `within`.

# Examples
```jldoctest
julia> line = LineString([[1, 1], [1, 2], [1, 3], [1, 4]])
LineString(Array{Float64,1}[[1.0, 1.0], [1.0, 2.0], [1.0, 3.0], [1.0, 4.0]])

julia> point = Point([1, 2])
Point([1.0, 2.0])

julia> contains(line, point)
true
```
"""
function contains(ft1::AbstractGeometry, ft2::AbstractGeometry)::Bool
    type1 = geotype(ft1)
    type2 = geotype(ft2)

    coords1 = ft1.coordinates
    coords2 = ft2.coordinates

    if type1 === :Point
        if type2 === :Point
            return coords1[1] == coords2[1] && coords1[2] == coords2[2]
        end
        throw(error("$(type2) is not a supported type."))
    elseif type1 === :LineString
        if type2 === :Point
            return point_on_line(ft2, ft1, true)
        elseif type2 === :LineString
            return lineOnLine(ft1, ft2)
        else
            throw(error("$(type2) is not a supported type."))
        end

    elseif type1 === :Polygon
        if type2 === :Point
            return point_in_polygon(ft2, ft1, true)
        elseif type2 === :LineString
            return lineInPolygon(ft1, ft2)
        elseif type2 === :Polygon
            return polygonInPolygon(ft1, ft2)
        else
            throw(error("$(type2) is not a supported type."))
        end
    else
        throw(error("$(type1) is not a supported type."))
    end
end

contains(ft1::Feature, ft2::Feature) = contains(ft1.geometry, ft2.geometry)

function lineInPolygon(poly::Polygon, line::LineString)
    out = false

    polybox = bbox(poly)
    linebox = bbox(line)

    !(bboxOverlap(polybox, linebox)) && return false

    coords = line.coordinates

    for i in 1:length(coords) - 1
        mid = [(coords[i][1] + coords[i + 1][1]) / 2, (coords[i][2] + coords[i + 1][2]) / 2]
        if point_in_polygon(Point(mid), poly, true)
            out = true
            break
        end
    end
    return out
end

function lineOnLine(line1::LineString, line2::LineString)
    for i in eachindex(line1.coordinates)
        !(point_on_line(line1.coordinates[i], line2)) && return false
    end
    return true
end

function polygonInPolygon(ft1::Polygon, ft2::Polygon, reverse::Bool=false)
    polybox1 = bbox(ft1)
    polybox2 = bbox(ft2)
    coords = []

    if reverse
        coords = ft1.coordinates
        !(bboxOverlap(polybox2, polybox1)) && return false

        for ring in coords
            for coord in ring
                !(point_in_polygon(Point(coord), ft2)) && return false
            end
        end
    else
        coords = ft2.coordinates
        !(bboxOverlap(polybox1, polybox2)) && return false

        for ring in coords
            for coord in ring
                !(point_in_polygon(Point(coord), ft1)) && return false
            end
        end
    end

    return true
end

function bboxOverlap(box1::Vector{T}, box2::Vector{T}) where {T <: Real}
    box1[1] > box2[1] && return false
    box1[3] < box2[3] && return false
    box1[2] > box2[2] && return false
    box1[4] < box2[4] && return false
    return true
end

"""
    within(ft1::AbstractGeometry, ft2::AbstractGeometry)::Bool

Return `true` if the first geometry is completely within the second geometry.
The interiors of both geometries must intersect and, the interior and boundary of the primary (geometry a)
must not intersect the exterior of the secondary (geometry b).
`within` returns the exact opposite result of `contains`.

# Examples
```jldoctest
julia> line = LineString([[1, 1], [1, 2], [1, 3], [1, 4]])
LineString(Array{Float64,1}[[1.0, 1.0], [1.0, 2.0], [1.0, 3.0], [1.0, 4.0]])

julia> point = Point([1, 2])
Point([1.0, 2.0])

julia> within(point, line)
true
```
"""
function within(ft1::AbstractGeometry, ft2::AbstractGeometry)::Bool
    type1 = geotype(ft1)
    type2 = geotype(ft2)

    coords1 = ft1.coordinates
    coords2 = ft2.coordinates

    if type1 === :Point
        if type2 === :LineString
            return point_on_line(ft1, ft2, true)
        elseif type2 === :Polygon
            return point_in_polygon(ft1, ft2, true)
        end
        throw(error("$(type2) is not a supported type."))
    elseif type1 === :LineString
        if type2 === :Polygon
            return lineInPolygon(ft1, ft2)
        elseif type2 === :LineString
            return lineOnLine(ft1, ft2)
        else
            throw(error("$(type2) is not a supported type."))
        end

    elseif type1 === :Polygon
        if type2 === :Polygon
            return polygonInPolygon(ft1, ft2, true)
        else
            throw(error("$(type2) is not a supported type."))
        end
    else
        throw(error("$(type1) is not a supported type."))
    end
end

function lineInPolygon(line::LineString, poly::Polygon)
    polybox = bbox(poly)
    linebox = bbox(line)

    !(bboxOverlap(polybox, linebox)) && return false

    coords = line.coordinates
    inside = false

    for i in 1:length(coords) - 1
        !(point_in_polygon(Point(coords[i]), poly)) && return false
        !inside && (inside = point_in_polygon(Point(coords[i]), poly, true))
        if !inside
            mid = [(coords[i][1] + coords[i + 1][1]) / 2, (coords[i][2] + coords[i + 1][2]) / 2]
            inside = point_in_polygon(Point(mid), poly, true)
        end
    end
    return inside
end

"""
    disjoint(geom1::AbstractGeometry, geom2::AbstractGeometry)::Bool

Return `true` if the intersection of the two geometries is an empty set.

# Examples
```jldoctest
julia> poly = Polygon([[[-1, 2], [3, 2], [3, 3], [-1, 3], [-1, 2]]])
Polygon(Array{Array{Float64,1},1}[[[-1.0, 2.0], [3.0, 2.0], [3.0, 3.0], [-1.0, 3.0], [-1.0, 2.0]]])

julia> point = Point([1, 1])
Point([1.0, 1.0])

julia> disjoint(poly, point)
true
```
"""
function disjoint(geom1::AbstractGeometry, geom2::AbstractGeometry)::Bool
    type1 = geotype(geom1)
    type2 = geotype(geom2)

    coords1 = geom1.coordinates
    coords2 = geom2.coordinates

    if type1 === :Point
        if type2 === :Point
            return !(coords1[1] == coords2[1] && coords1[2] == coords2[2])
        elseif type2 === :LineString
            return !point_on_line(geom1, geom2)
        elseif type2 === :Polygon
            !point_in_polygon(geom1, geom2)
        end

    elseif type1 === :LineString
        if type2 === :Point
            return !point_on_line(geom2, geom1)
        elseif type2 === :LineString
            return !lineOnLine(geom1, geom2)
        elseif type2 === :Polygon
            return !lineInPolygon(geom2, geom1)
        end
    elseif type1 === :Polygon
        if type2 === :Point
            return !point_in_polygon(geom2, geom1)
        elseif type2 === :LineString
            return !lineInPolygon(geom1, geom2)
        elseif type2 === :Polygon
            return !polyInPoly(geom2, geom1)
        end
    end
    return false
end

disjoint(geom1::AbstractFeature, geom2::AbstractFeature) = disjoint(geom1.geometry, geom2.geometry)

function polyInPoly(poly1::Polygon, poly2::Polygon)
    coords1 = poly1.coordinates
    coords2 = poly2.coordinates

    for ring in coords1
        for coord in ring
            (point_in_polygon(Point(coord), poly2)) && return true
        end
    end

    for ring in coords2
        for coord in ring
            (point_in_polygon(Point(coord), poly1)) && return true
        end
    end

    inter = line_intersects(polygon_to_line(poly1), polygon_to_line(poly2))
    inter != nothing && return true

    return false

end

"""
    line_intersects(line1::AbstractLineString, line2::AbstractLineString)

Find a point that intersects LineStrings with two coordinates each.

# Examples
```jldoctest
julia> line1 = LineString([[124.584961,-12.768946],[126.738281,-17.224758]])
LineString(Array{Float64,1}[[124.585, -12.7689], [126.738, -17.2248]])

julia> line2 = LineString([[123.354492,-15.961329],[127.22168,-14.008696]])
LineString(Array{Float64,1}[[123.354, -15.9613], [127.222, -14.0087]])

julia> line_intersects(line1, line2)
Point([125.584, -14.8357])
```
"""
function line_intersects(line1::AbstractLineString, line2::AbstractLineString)
    coords1 = line1.coordinates
    coords2 = line2.coordinates

    (length(coords1) == 2 && length(coords2) == 2) && return intersects(line1, line2)

    result = []

    for i in 1:length(coords1) - 1
        for j in 1:length(coords2) - 1
            inter = intersects(LineString([coords1[i], coords1[i + 1]]), LineString([coords2[j], coords2[j + 1]]))
            inter != nothing && push!(result, inter.coordinates)
        end
    end
    return unique!(result)
end

function intersects(line1::AbstractLineString, line2::AbstractLineString)
    coords1 = line1.coordinates
    coords2 = line2.coordinates

    (length(coords1) != 2 || length(coords2) != 2) && throw(error("Lines must contain only 2 coordinates."))

    x1, y1 = coords1[1]
    x2, y2 = coords1[2]
    x3, y3 = coords2[1]
    x4, y4 = coords2[2]

    d = ((y4 - y3) * (x2 - x1)) - ((x4 - x3) * (y2 - y1))
    a = ((x4 - x3) * (y1 - y3)) - ((y4 - y3) * (x1 - x3))
    b = ((x2 - x1) * (y1 - y3)) - ((y2 - y1) * (x1 - x3))

    if d == 0
        if a == 0 && b == 0
            return nothing
        end
        return nothing
    end

    ã = a / d
    b̃ = b / d

    if ã >= 0 && ã <= 1 && b̃ >= 0 && b̃ <= 1
        x = x1 + (ã * (x2 - x1))
        y = y1 + (ã * (y2 - y1))
        return Point([x, y])
    end

    return nothing
end

"""
     crosses(ft1::AbstractGeometry, ft2::AbstractGeometry)::Bool

Return `true` if the intersection results in a geometry whose dimension is one less than
the maximum dimension of the two source geometries and the intersection set is interior to
both source geometries.

# Examples
```jldoctest
julia> line = LineString([[1, 1], [1, 2], [1, 3], [1, 4]])
LineString(Array{Float64,1}[[1.0, 1.0], [1.0, 2.0], [1.0, 3.0], [1.0, 4.0]])

julia> line2 = LineString([[-2, 2], [4, 2]])
LineString(Array{Float64,1}[[-2.0, 2.0], [4.0, 2.0]])

julia> crosses(line2, line)
true
```
"""
function crosses(ft1::AbstractGeometry, ft2::AbstractGeometry)::Bool
    type1 = geotype(ft1)
    type2 = geotype(ft2)

    if type1 === :MultiPoint
        if type2 === :LineString
            return MpCrossLs(ft1, ft2)
        elseif type2 === :Polygon
            return MpCrossPoly(ft1, ft2)
        else
            throw(error("Geometry $(type2) is not supported."))
        end
    elseif type1 === :LineString
        if type2 === :MultiPoint
            return MpCrossLs(ft2, ft1)
        elseif type2 === :Polygon
            return LsCrossPoly(ft1, ft2)
        elseif type2 === :LineString
            return LsCrossLs(ft1, ft2)
        else
            throw(error("Geometry $(type2) is not supported."))
        end
    elseif type1 === :Polygon
        if type2 === :MultiPoint
            return MpCrossPoly(ft2, ft1)
        elseif type2 === :LineString
            return LsCrossPoly(ft2, ft1)
        else
            throw(error("Geometry $(type2) is not supported."))
        end
    end

    throw(error("Geometry $(type1) is not supported."))
end

crosses(ft1::Feature, ft2::Feature) = crosses(ft1.geometry, ft2.geometry)

function MpCrossLs(geom1::MultiPoint, geom2::LineString)
    intPoint = false
    extPoint = false

    pLength = length(geom1.coordinates)
    i = 1

    while i < pLength && !intPoint && !extPoint

        for j in 1:length(geom2.coordinates) - 1
            incVertices = "both"

            (j === 1 || j === length(geom2.coordinates) - 2) && (incVertices = "none")

            if isPointOnSegment(geom2.coordinates[j], geom2.coordinates[j + 1], geom1.coordinates[i], incVertices)
                intPoint = true
            else
                extPoint = true

            end

        end
        i += 1
    end

    return intPoint && extPoint
end

function LsCrossLs(line1::LineString, line2::LineString)
    inter = line_intersects(line1, line2)

    if length(inter) > 0
        for i in 1:length(line1.coordinates) - 1
            for j in 1:length(line2.coordinates) - 1
                incVertices = "both"

                (j === 1 || j === length(line2.coordinates) - 2) && (incVertices = "none")

                isPointOnSegment(line1.coordinates[i], line1.coordinates[i + 1], line2.coordinates[j], incVertices) && return true
            end
        end
    end
    return false
end

function LsCrossPoly(line::LineString, poly::Polygon)
    line2 = polygon_to_line(poly)
    inter = line_intersects(line, line2)

    (length(inter) > 0) && return true

    return false
end

function MpCrossPoly(mp::MultiPoint, poly::Polygon)
    intPoint = false
    extPoint = false
    pLength = length(mp.coordinates[1])
    i = 1

    while i < pLength && intPoint && extPoint
        if point_in_polygon(Point(mp.coordinates[1][i]), poly)
            intPoint = true
        else
            extPoint = true
        end
        i += 1
    end
    return intPoint && extPoint
end

"""
    overlap(geom1::AbstractGeometry, geom2::AbstractGeometry)::Bool

Compare two Geometries of the same dimension and return true if their intersection set results in a geometry
different from both but of the same dimension. It applies to Polygon/Polygon, LineString/LineString,
Multipoint/Multipoint, MultiLineString/MultiLineString and MultiPolygon/MultiPolygon.

# Examples
```jldoctest
julia> poly1 = Polygon([[[0,0],[0,5],[5,5],[5,0],[0,0]]])
Polygon(Array{Array{Float64,1},1}[[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [5.0, 0.0], [0.0, 0.0]]])

julia> poly2 = Polygon([[[1,1],[1,6],[6,6],[6,1],[1,1]]])
Polygon(Array{Array{Float64,1},1}[[[1.0, 1.0], [1.0, 6.0], [6.0, 6.0], [6.0, 1.0], [1.0, 1.0]]])

julia> overlap(poly1, poly2)
true
```
"""
function overlap(geom1::AbstractGeometry, geom2::AbstractGeometry)::Bool
    # TODO: add support for LineStrings and ML

    type1 = geotype(geom1)
    type2 = geotype(geom2)

    !isequal(type1, type2) && throw(error("Gometries must be of the same type."))
    type1 === :Point && throw(error("$(type1) geometry is not supported."))

    if length(geom1.coordinates) === length(geom2.coordinates)
        geom1.coordinates ≈ geom2.coordinates && return false
    end

    overlap = 0
    coords1 = geom1.coordinates
    coords2 = geom2.coordinates

    if isequal(type1, :MultiPoint)
        for i in eachindex(coords1)
            for j in eachindex(coords2)
                (coords1[i][1] == coords2[j][1] && coords1[i][2] == coords2[j][2]) && (overlap += 1)
            end
        end
    elseif isequal(type1, :Polygon)
        line1 = polygon_to_line(geom1)
        line2 = polygon_to_line(geom2)

        (line_intersects(line1, line2) != []) && (overlap += 1)

    elseif isequal(type1, :MultiPolygon)
        for poly1 in coords1
            for poly2 in coords2
                line1 = polygon_to_line(poly1)
                line2 = polygon_to_line(poly2)

                (line_intersects(line1, line2) != []) && (overlap += 1)
            end
        end
    else
        throw(error("Unsupported geometry."))
    end

    return overlap > 0
end

"""
    overlap(feat1::AbstractFeature, feat2::AbstractFeature)::Bool

Compare two Features of the same dimension and return true if their intersection set results in a geometry
different from both but of the same dimension. It applies to Polygon/Polygon, LineString/LineString,
Multipoint/Multipoint, MultiLineString/MultiLineString and MultiPolygon/MultiPolygon.
"""
overlap(feat1::AbstractFeature, feat2::AbstractFeature)::Bool = overlap(feat1.geometry, feat2.geometry)


### A Pluto.jl notebook ###
# v0.12.18

using Markdown
using InteractiveUtils

# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
macro bind(def, element)
    quote
        local el = $(esc(element))
        global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : missing
        el
    end
end

# ╔═╡ 6b1a2152-34a8-4ae5-96ab-fc16fbfd0302
using BenchmarkTools

# ╔═╡ 62d2aac2-bb90-4c62-a01c-a12f024fe374
using CSV, DataFrames

# ╔═╡ 1d3176d2-864f-417a-b068-dd876c9fa8ac
using Plots

# ╔═╡ 21e851a3-bb00-4b77-9705-99b83839e2fe
begin
	using PlutoUI
	md"This is a hidden cell that imports stuff. Julia implicitly returns the value of the last statement in each block, be it a begin, let, function, if or whatever block"
end

# ╔═╡ e03f0d7b-2ad0-43d2-9a26-058ef91fd8b6
md"""requires julia1.5.3! install from [here](https://julialang.org/downloads/#current_stable_release) and make sure you have [latest source](https://github.com/pankgeorg/learning/blob/main/present/lightning-pluto-pls18.jl) of this notebook if you want run a live copy"""

# ╔═╡ 53d6af8c-487d-11eb-2662-8f34383b413b
md"""
# Julia Programming Language & Pluto.jl

Panagiotis, casually contributing @ Pluto.jl

Also ECE2009, front-end developer@intelligencia.ai

"""

# ╔═╡ 9f7462dd-b97c-46de-894c-01afee404d33
md"""
## Julia Basics
"""

# ╔═╡ fb0a072d-53b5-4bbb-a6ed-6c808ee28729
md"""
Designed to be
- fast
- intuitive
- look like math
- functional-ish
- matlab/R/Python

but mainly *fast* (except for when it JITs)
"""

# ╔═╡ dcba08f8-be7d-4c8b-b77a-003b9eec92ed
md"""
### Ranges
"""

# ╔═╡ 7f2e9fd2-edc2-42e0-8423-b30fd79d0d48
1:10, 1:0.01:1

# ╔═╡ 59c1e944-0f77-4cc1-a631-2804e5668ab2
@bind opt Select(["α", "β", "γ", "άλλο"])

# ╔═╡ 5276707f-95f7-44c6-9d5e-9d4e928ed9d0
md"""
### Control statements
"""

# ╔═╡ b7e611f7-dc5d-48de-b690-a8e05c36b570
if opt == "α"
	"άλφα"
elseif opt == "β"
	"βήτα"
elseif opt == "γ"
	"γάμμα"
else
	"άλλο"
end

# ╔═╡ 4cac95ea-f31f-4109-9861-ad8b7bf00bf4
opt == "α" ? 1 : 2 # JavaScript-y!

# ╔═╡ 157d5726-6638-4abf-8360-7132b98456ac
md"""
### Array Comprehensions
"""

# ╔═╡ a3091d16-5472-409c-8921-015d63ceb58a
[i + t - 1  for i ∈ 1:10, t ∈ 1:10]

# ╔═╡ 6e187f02-0e33-4cf0-8fc7-a423bfe712c8
[i for i ∈ 1:10][0]

# ╔═╡ 8431090a-594b-45b5-98d3-66b5a35a9273
md"""
#### Yes! That's 1-indexed!!"""

# ╔═╡ 48d467d2-56fc-4957-9535-7fca98d6bc20
begin
	arr = []
	for i ∈ 1:10
		push!(arr, i)
	end
	arr
end

# ╔═╡ 953f1054-d9d3-45cb-b366-f974db8e70a0
md"""
#### Mutations are ok!

be explicit about it: `functionname!` is the naming convention
"""

# ╔═╡ 5d47dfe7-39d2-4f1c-9e3f-7a008c3ebdd9
n = 1:12

# ╔═╡ e6811d5d-c96d-48d0-a21e-45a2d2eb6ca3
n .^ 2

# ╔═╡ d5e62d4b-13a3-4372-9188-91dbce784029
md"""
where . is the the [vectorise](https://docs.julialang.org/en/v1/manual/mathematical-operations/) operator(?) !
"""

# ╔═╡ 43940c48-6fc0-4ec2-ba45-c20d861fcff7
md"""
### Broadcasting
"""

# ╔═╡ acbe0d3d-7c52-4972-a9d0-22e72ead5a9b

# ╔═╡ f9692cb2-4585-4e7e-b6cb-188c03d41d30
broadcast

# ╔═╡ 101dc1bc-2d18-44d6-958f-7d8d973e68bb
+

# ╔═╡ 8f19bd2d-441f-432b-abb9-7e5db7478356
md"""
### Macros

Inspired by lisp!
"""

# ╔═╡ c995e340-e830-4e7e-a37f-08a65352fca1
md"""
### Compile Something
"""

# ╔═╡ 0c4807db-102e-4683-b630-24c43390c415
π, pi

# ╔═╡ 52d61ca8-54e5-46bd-9683-203f3c8e3744
6 *  sum([1/i^2 for i in 1:100000]) - π ^ 2

# ╔═╡ ad8116b5-c8a8-4c60-b7f4-35f6dbc21cdf
function calculatepi(📏)
	# Add dots to constants to see what happens!
	Σ = 0.
	🐑 = 0.
	while abs(6. * Σ - π ^ 2.) > 📏
		🐑= 🐑 + 1.
		Σ = Σ + 1. / 🐑 ^ 2.
	end
	√(6. * Σ)
end

# ╔═╡ de7d263b-c0fc-4255-8edd-41877acef893
calculatepi

# ╔═╡ a2f20d88-8338-4a20-be0d-228ec56edd4c
calculatepi(0.000001)

# ╔═╡ da28e85a-19a4-4b84-9e62-f95f56910618
md"""
# Types in Julia

are opt in
"""

# ╔═╡ 7c68475d-a5e9-48ef-b74e-d1cad43ffb3b
md"""
here `a` is a float (probably Float64) while the range is a range of `integers`
"""

# ╔═╡ c826c4c3-7118-496a-94e5-f9c190fa00d6
with_terminal() do
	@info @btime let
		a = 0.0
		for i ∈ 1:10_000_000
			a = a + i
		end
		a
	end
end

# ╔═╡ e56f9f56-275d-42d9-90c9-973ed23e15d0
md"""here `a` is explicitly `Int64` and the range is also `Int64`,

caring for types gives you **_speed_**
"""

# ╔═╡ cd40c360-e6e5-41c9-9683-2d0c5f0ac38f
with_terminal() do
	@info @btime let
		a = 0::Int64
		for i ∈ 1:10_000_000
			a = a::Int64 + i::Int64
		end
		a
	end
end

# ╔═╡ d3152411-3848-4f1b-aac3-13acc79d4a88
md"""
And not caring for types just __*works*__
"""

# ╔═╡ 62b3e448-bdd9-4a0b-9e22-a179f54aeec6
with_terminal() do
	code_native(calculatepi, Tuple{Float64})
end

# ╔═╡ 9545f500-bb4c-40d3-a1ea-574bb377277a
code_typed(calculatepi, Tuple{Float64})

# ╔═╡ 4c367f62-0a1c-4dc9-83f0-bde497d0cdd8
md"""
# More Types in Julia

or "Why Julia is not part of PL2!"

"""

# ╔═╡ d6151f91-0311-4a89-b315-eee9e5d41eb0
struct Point{T<:AbstractFloat}
	a::T
	b::T
end

# ╔═╡ cf05995f-6954-490c-b1af-fe25d1e668c2
begin
	# Point{Float64}(2,2)
	# Point{Float64}(2,missing)
	# Point{Float64}(2,nothing)
end

# ╔═╡ 7504b732-0532-41ff-a051-e087502b2565
struct PointMaybeMissing2{T<:Union{AbstractFloat, Missing}}
	a::T
	b::T
end

# ╔═╡ bd0db4ee-adc9-4809-b6ba-86aa75c8b49c
Type{>:Missing} == Type{Union{T, Missing}} where T

# ╔═╡ d132585d-98c8-4905-9fbc-846480d9fce0
Type{<:AbstractFloat} == Type{Union{T, Missing}} where T<:AbstractFloat

# ╔═╡ 671d5eaf-0c7e-4885-b1f2-e19c1160fec4
md"""

bonus,

### Multiple dispatch!
"""

# ╔═╡ 98474311-ee54-4e17-815f-a1d67618c351
begin
	import Base.show
	function show(number::Number)
		"$(number)"
	end
	function show(number::Float64)
		"$(number) from show for Float64"
	end
	function show(number::Int64)
		"$(number) from show for Int64"
	end
	function show(number::AbstractFloat)
		"$(number) is an AbstractFloat"
	end
	#function show(number::BigFloat)
	#	"$(number) can be abritrarily large!"
	#end
end

# ╔═╡ f75aac94-465d-423b-9746-a3357bf82031
show(4)

# ╔═╡ c608693a-657d-447b-85b8-24a05125cb61
show(4.1)

# ╔═╡ ba41cede-7dde-47a8-bfa1-5370634390dc
begin
	import Base.MPFR
	four = Base.MPFR.BigFloat(4)
	show(four)
end

# ╔═╡ 80d45ef8-ff90-4781-92db-439569526e0b
md"""

### Method Specificity

"Call the most specific matching method" [(@JeffBezanson)](https://www.youtube.com/watch?v=TPuJsgyu87U&ab_channel=TheJuliaProgrammingLanguage)

1. Strict subtype (<:, not ==) → more specific
- ???
"""

# ╔═╡ 8ffd70a3-8e5b-4100-91a0-ddc7698a5fd6
md"""
No ambiguity:

	1. (AbstractArray)
	2. (Any, Integer)

1 is *not* more specific than 2, because one argument is different than two arguments

Let's add this to the mix

	3. (AbstractArray, Int...)

`Int...` is *any number* of Ints, so these work: ([1,2,3], 4, 5,) or ([1,2,3])

Then

	 AbstractArray, Int... (3) <: Any, Integer (2)

and but also, for 0 Ints:

	AbstractArray (1) <: AbstractArray, Int... (3)

*but* we said that 1 <::> 2 --> `Subset` relation is not transitive? 💥💥
??????

#### Patched:

1 argument methods are more specific 🎉🎉🎈🎊🥳

"""

# ╔═╡ d6b800dc-a386-4de2-acc0-6c32c0dd5261
md"""
#### But!
clopied pasted from Jeff Bezanson

| Number Hierarchy | Union Hierarchy |
| :---- | :--- |
|$(Resource("https://i.imgur.com/JmBPf8y.png")) | $(Resource("https://i.imgur.com/H08jTat.png")) |

##### Note: 3 → 4

    Union{`Float64`, `AbstractComplex`} ∩ `AbstractFloat` ≡ `Float64` ∩ `AbstractFloat`

Rest left as an exercise to the reader 🤗✨🐱🏍

"""

# ╔═╡ 99b645ec-d3ca-4303-a51e-134f11e659d0
md"""
### Are these really anyones' problems?? 🤷♂🤷♀🤷♂🤷♀
"""

# ╔═╡ c2a31489-3315-4cd7-83da-b4b834eb0d1d
md"""
# Pluto.jl: a fresh notebook for Julia

written by [fonsi](https://github.com/fonsp) and [malyvsen](https://github.com/malyvsen) and [dral](https://github.com/dralletje), casually patched by [me](#82862e32-ab19-4766-86be-4f77bdb7fcfb)

### Keys:

- reactive - as in Excel!
- files are valid Julia files (can run with `julia ./fun-program.jl`)
- no hidden state
- can reorder cells to tell _your_ story (and backend runs the *DAG*, thanks [observable](http://observablehq.com/))
- literate programming fundamentals
- leverages `show` methods with `text/html` to show julia structs in HTML/CSS/**JavaScript**

### Why:

- reproducible research
- version control notebooks
- interactive articles/publications for the masses
- lower the entry barrier to programming (beginner tool - no vim mode - no emacs mode - ever)
- solve the [9 problems with notebooks](https://web.eecs.utk.edu/~azh/pubs/Chattopadhyay2020CHI_NotebookPainpoints.pdf)

and, of course
- quarantine fun

"""

# ╔═╡ 9dfabf04-6301-4376-ab51-bee89bce6672
md"""
### In Action | Human Machine learning
heavily inspired from [malyvsen](https://github.com/malyvsen)
"""

# ╔═╡ 63511ed2-fc8c-46ea-8186-505d52f4ad6b
weather_df = begin
	file = download("https://gist.githubusercontent.com/pankgeorg/57d38097b56c99a8e4da9b62fd87a3d0/raw/693429a56ea0707381ad94b6b0943d8fcf17ec8d/20201020to20201228_neopsychiko_weather.csv")
	CSV.read(file, DataFrame)
end

# ╔═╡ 54b2b95e-ef97-495c-9937-50663dcb084b
filter_df = weather_df;

# ╔═╡ 11df8461-9a1b-4a8d-a13f-641f9c9e223d
describe(filter_df);  # small statistics

# ╔═╡ 94e79d8b-3a59-4e47-82a1-7093e9e696a9
md"""
Hand pick the values below

| 🎨 🎮 | Control Panel | unit | varname | explanation |
| :--- | :--- | :---: | :---: | :--- |
| Temperature | $(@bind temp Slider(0:0.1:30; default=15.7, show_value=true)) | °C | temp | (daily) offset |
| Period | $(@bind period Slider(22:0.1:26; default=24, show_value=true)) | hours | period | (daily) period |
| Phase | $(@bind phase Slider(0:0.1:24; default=8.3, show_value=true)) | hours | phase | (daily) phase |
| A | $(@bind A Slider(0:0.1:30; default=3.2, show_value=true)) | °C | A | (daily) var |
| Temperature * | $(@bind stemp Slider(-10:0.1:10; default=0.3, show_value=true)) | °C | stemp | offset |
| Period * | $(@bind speriod Slider(0:365; default=89, show_value=true)) | days | speriod | period |
| Phase * | $(@bind sphase Slider(0:365; default=68, show_value=true)) | days | sphase | phase |
| A * | $(@bind sA Slider(0:0.1:30; default=5.8, show_value=true)) | °C | sA | var |

\* Second, monthly component
"""

# ╔═╡ 9330f564-a26b-4cff-ab89-4bfd87ba5351
temp, period, phase, A

# ╔═╡ c6dc73f6-85d0-411f-a521-2df29d5b35e6
begin
	l = length(filter_df[!, "Created At"])
	p = plot(
		filter_df[!, "Created At"],
		filter_df[!, "Average of Temperature"],
		labels="Temperature",
		size = (600, 200),
		dpi = 450
	)
	model_day = temp .+ A / 2 * sin.(2 * pi * (i - phase) / (period) for i in 1:l)
	model_year = stemp .+ sA / 2 * sin.(2 * pi * (i - 24 * sphase) / (24 * speriod) for i in 1:l)
	model = model_day .+ model_year
	p
	plot!(filter_df[!, "Created At"], model, labels="Model")
end

# ╔═╡ 245ebac6-d4fd-476e-8e8b-38cbc89cc2a0
error = round(sum(abs.((model .-  filter_df[!, "Average of Temperature"])).^2 ))

# ╔═╡ 32977a26-02ac-44c0-b984-0a3c9d92b41f
md"""
## Pluto Vision / Roadmap

- Simpler than excel
- Accessible
- Schools
- Work in Chromebooks
- gUIs for everything
- Cloud Native ™®™®
"""

# ╔═╡ fb7fbafb-d107-438c-be9d-048b20dc3564
md"""
## Links

- Pluto.jl → [https://github.com/fonsp/Pluto.jl](https://github.com/fonsp/Pluto.jl)
- Julia → [https://julialang.org/](https://julialang.org/)
- [Computational Thinking @ MIT](https://computationalthinking.mit.edu/Fall20/), taught with Julia & Pluto!
- [What’s Wrong with Computational Notebooks? Pain Points, Needs, and Design Opportunities](https://web.eecs.utk.edu/~azh/pubs/Chattopadhyay2020CHI_NotebookPainpoints.pdf)
- Other cool notebooks [deepnote.ai](https://deepnote.com/), [nextjournal](http://nextjournal.com/) and of course [jupyter](https://jupyter.org/)
- [What is bad about Julia](https://www.youtube.com/watch?v=TPuJsgyu87U&ab_channel=TheJuliaProgrammingLanguage) by Jeff Bezanson, Julia Computing
- [The Future of Spreadsheets](https://www.youtube.com/watch?v=WTnwl_skD-Q&t=50s&ab_channel=MicrosoftResearch), Microsoft Research
- Pluto fonts are `Alegreya Sans`, `Vollkorn` and [`Julia Mono`](https://github.com/cormullion/juliamono) (for all these UTF-8 math characters and ligatures)
- pankgeorg ~ you can find me `~`@`{gmail.com}`, `github.com/~`, `~.com`

# Thank you!

---
"""

# ╔═╡ 04c54def-40a1-4423-8c47-1f554bacffbd
html"""
Custom styles(!)
CSS is awesome

"""

# ╔═╡ a9ea2fe3-04db-41c4-8982-43e4e65de952
excelImg = Resource("https://upload.wikimedia.org/wikipedia/commons/thumb/7/7f/Microsoft_Office_Excel_%282018%E2%80%93present%29.svg/103px-Microsoft_Office_Excel_%282018%E2%80%93present%29.svg.png")

# ╔═╡ dc94c160-b3af-428f-aa16-e0e0d4e75179
dataScience = html"""dAtA SciEnCe!1!11"""

# ╔═╡ 82862e32-ab19-4766-86be-4f77bdb7fcfb
md"""
# Me

I'm a software developer for the masses (front-end in React, Angular, backend in Python)

Contributing to [Pluto.jl](https://github.com/fonsp/Pluto.jl/graphs/contributors), one feature at a time!

#### Past

I've done some serious development in

$(excelImg)

which is probably the [most widely used](https://www.youtube.com/watch?v=WTnwl_skD-Q&t=50s&ab_channel=MicrosoftResearch) programming language out there

#### Currently

- Writing React applications for intelligencia.ai (we're hiring!)
- Face software engineering challenges deploying, explaining and presenting $(dataScience) stuff

#### Also

- Gardening
- Maintaining a [weather station](https://metabase.pankgeorg.com/public/dashboard/680eb6ec-ebdf-4691-ab73-994f14d4540e??created_at=past0days~)
- Cooking (but no pastry yet)
"""

# ╔═╡ 8b03809a-1c36-472f-b53c-f227e3844a42
let
    import Pkg
    Pkg.add(url="https://github.com/Pocket-titan/DarkMode")
	Pkg.add("BenchmarkTools")
	Pkg.add("CSV")
	Pkg.add("Plots")
	Pkg.add("DataFrames")
	Pkg.add("PlutoUI")
    import DarkMode
    DarkMode.Toolbox(theme="lucario", ligatures=true)
end

# ╔═╡ Cell order:
# ╟─e03f0d7b-2ad0-43d2-9a26-058ef91fd8b6
# ╟─53d6af8c-487d-11eb-2662-8f34383b413b
# ╟─9f7462dd-b97c-46de-894c-01afee404d33
# ╟─fb0a072d-53b5-4bbb-a6ed-6c808ee28729
# ╟─dcba08f8-be7d-4c8b-b77a-003b9eec92ed
# ╠═7f2e9fd2-edc2-42e0-8423-b30fd79d0d48
# ╟─59c1e944-0f77-4cc1-a631-2804e5668ab2
# ╟─5276707f-95f7-44c6-9d5e-9d4e928ed9d0
# ╠═b7e611f7-dc5d-48de-b690-a8e05c36b570
# ╠═4cac95ea-f31f-4109-9861-ad8b7bf00bf4
# ╟─157d5726-6638-4abf-8360-7132b98456ac
# ╠═a3091d16-5472-409c-8921-015d63ceb58a
# ╠═6e187f02-0e33-4cf0-8fc7-a423bfe712c8
# ╟─8431090a-594b-45b5-98d3-66b5a35a9273
# ╠═48d467d2-56fc-4957-9535-7fca98d6bc20
# ╟─953f1054-d9d3-45cb-b366-f974db8e70a0
# ╠═e6811d5d-c96d-48d0-a21e-45a2d2eb6ca3
# ╠═5d47dfe7-39d2-4f1c-9e3f-7a008c3ebdd9
# ╟─d5e62d4b-13a3-4372-9188-91dbce784029
# ╟─43940c48-6fc0-4ec2-ba45-c20d861fcff7
# ╠═acbe0d3d-7c52-4972-a9d0-22e72ead5a9b
# ╠═f9692cb2-4585-4e7e-b6cb-188c03d41d30
# ╠═101dc1bc-2d18-44d6-958f-7d8d973e68bb
# ╟─8f19bd2d-441f-432b-abb9-7e5db7478356
# ╟─c995e340-e830-4e7e-a37f-08a65352fca1
# ╠═0c4807db-102e-4683-b630-24c43390c415
# ╠═de7d263b-c0fc-4255-8edd-41877acef893
# ╠═52d61ca8-54e5-46bd-9683-203f3c8e3744
# ╠═a2f20d88-8338-4a20-be0d-228ec56edd4c
# ╠═ad8116b5-c8a8-4c60-b7f4-35f6dbc21cdf
# ╟─da28e85a-19a4-4b84-9e62-f95f56910618
# ╠═6b1a2152-34a8-4ae5-96ab-fc16fbfd0302
# ╟─7c68475d-a5e9-48ef-b74e-d1cad43ffb3b
# ╠═c826c4c3-7118-496a-94e5-f9c190fa00d6
# ╟─e56f9f56-275d-42d9-90c9-973ed23e15d0
# ╠═cd40c360-e6e5-41c9-9683-2d0c5f0ac38f
# ╟─d3152411-3848-4f1b-aac3-13acc79d4a88
# ╠═62b3e448-bdd9-4a0b-9e22-a179f54aeec6
# ╠═9545f500-bb4c-40d3-a1ea-574bb377277a
# ╟─4c367f62-0a1c-4dc9-83f0-bde497d0cdd8
# ╠═d6151f91-0311-4a89-b315-eee9e5d41eb0
# ╠═cf05995f-6954-490c-b1af-fe25d1e668c2
# ╠═7504b732-0532-41ff-a051-e087502b2565
# ╠═bd0db4ee-adc9-4809-b6ba-86aa75c8b49c
# ╠═d132585d-98c8-4905-9fbc-846480d9fce0
# ╟─671d5eaf-0c7e-4885-b1f2-e19c1160fec4
# ╠═98474311-ee54-4e17-815f-a1d67618c351
# ╠═f75aac94-465d-423b-9746-a3357bf82031
# ╠═c608693a-657d-447b-85b8-24a05125cb61
# ╠═ba41cede-7dde-47a8-bfa1-5370634390dc
# ╟─80d45ef8-ff90-4781-92db-439569526e0b
# ╟─8ffd70a3-8e5b-4100-91a0-ddc7698a5fd6
# ╟─d6b800dc-a386-4de2-acc0-6c32c0dd5261
# ╟─99b645ec-d3ca-4303-a51e-134f11e659d0
# ╟─c2a31489-3315-4cd7-83da-b4b834eb0d1d
# ╟─9dfabf04-6301-4376-ab51-bee89bce6672
# ╠═62d2aac2-bb90-4c62-a01c-a12f024fe374
# ╠═63511ed2-fc8c-46ea-8186-505d52f4ad6b
# ╠═1d3176d2-864f-417a-b068-dd876c9fa8ac
# ╠═54b2b95e-ef97-495c-9937-50663dcb084b
# ╠═11df8461-9a1b-4a8d-a13f-641f9c9e223d
# ╠═9330f564-a26b-4cff-ab89-4bfd87ba5351
# ╟─94e79d8b-3a59-4e47-82a1-7093e9e696a9
# ╠═245ebac6-d4fd-476e-8e8b-38cbc89cc2a0
# ╠═c6dc73f6-85d0-411f-a521-2df29d5b35e6
# ╟─32977a26-02ac-44c0-b984-0a3c9d92b41f
# ╟─fb7fbafb-d107-438c-be9d-048b20dc3564
# ╟─04c54def-40a1-4423-8c47-1f554bacffbd
# ╟─a9ea2fe3-04db-41c4-8982-43e4e65de952
# ╟─dc94c160-b3af-428f-aa16-e0e0d4e75179
# ╟─21e851a3-bb00-4b77-9705-99b83839e2fe
# ╟─82862e32-ab19-4766-86be-4f77bdb7fcfb
# ╟─8b03809a-1c36-472f-b53c-f227e3844a42


module RT

include("vec3.jl")
import .V3: Vec3, at_len, x, y, z

using LinearAlgebra
using Images

struct Ray
    origin::Vec3
    arrow::Vec3
    Ray(o::Vec3, a::Vec3) = new(o, normalize(a))
end

struct Camera
    scr_w::UInt16
    scr_h::UInt16
    view_w::Float64
    view_h::Float64
    eye_pos::Vec3
    eye_up::Vec3
    eye_right::Vec3
    view_tl::Vec3
end

function Camera(scr_w, scr_h)
    ASPECT_R = scr_w / scr_h
    VIEW_HH = 1.0
    VIEW_HW = ASPECT_R * VIEW_HH
    view_h = VIEW_HH * 2
    view_w = VIEW_HW * 2

    eye_pos = Vec3([0, 0, 0])
    EYE_DIR = Vec3([0, 0, -1])
    eye_up = Vec3([0, 1, 0])
    eye_right = EYE_DIR × eye_up

    FOCAL_LEN = 1.0
    VIEW_CENTER = eye_pos + at_len(EYE_DIR, FOCAL_LEN)
    view_tl = VIEW_CENTER + at_len(eye_up, VIEW_HH) - at_len(eye_right, VIEW_HW)

    Camera(scr_w, scr_h, view_w, view_h, eye_pos, eye_up, eye_right, view_tl)
end

get_ray(self::Camera, u, v) =
    Ray(
        self.eye_pos,
        self.view_tl
        + at_len(self.eye_right, u * self.view_w)
        - at_len(self.eye_up, v * self.view_h)
        - self.eye_pos
    )

function render(self::Camera, ray_color_fn, print_progress=true)
    img = zeros(RGB, self.scr_h, self.scr_w)
    for j in 1:self.scr_h
        if print_progress && j % 100 == 0
            println("$j of $(self.scr_h)")
        end
        v = (j - .5) / self.scr_h
        for i in 1:self.scr_w
            u = (i - .5) / self.scr_w
            ray = get_ray(self, u, v)
            img[j, i] = ray_color_fn(ray)
        end
    end
    img
end

abstract type Hittable end

struct HitRecordTest
    t::Float64
    ray::Ray
    obj::Hittable
end

struct HitRecordFull
    front::Bool
    normal::Vec3
    point::Vec3
end

struct Sphere <: Hittable
    center::Vec3
    r::Float64
end

function hit_test(self::Sphere, ray::Ray, tmin=0, tmax=Inf)::Union{HitRecordTest, Nothing}
    oc = ray.origin - self.center
    half_b = oc ⋅ ray.arrow
    c = oc ⋅ oc - self.r^2
    Δ = half_b^2 - c
    if Δ < 0
        return nothing
    else
        sΔ = sqrt(Δ)
        root = -half_b - sΔ
        if root < tmin || root > tmax
            root = -half_b + sΔ
            if root < tmin || root > tmax
                return nothing
            end
        end
        return HitRecordTest(root, ray, self)
    end
end

function hit_full(self::Sphere, hit::HitRecordTest)::HitRecordFull
    p = at_len(hit.ray.arrow, hit.t)
    normal = normalize(p - self.center)
    front = true
    if hit.ray.arrow ⋅ normal > 0
        front = false
        normal = -normal
    end
    HitRecordFull(front, normal, p)
end

function bg_color(ray::Ray)::RGB
    t = (y(ray.arrow) + 1) / 2
    blue = RGB(.5, .7, 1)
    white = RGB(1, 1, 1)
    t * blue + (1 - t) * white
end

spheres = [
    Sphere(Vec3([0, 0, -1]), .5),
    Sphere(Vec3([0, -500.5, -1]), 500),
]

function ray_color(ray::Ray)::RGB
    hit = nothing
    for sphere in spheres
        new_hit = hit_test(sphere, ray)
        if !isnothing(new_hit) && (isnothing(hit) || new_hit.t < hit.t)
            hit = new_hit
        end
    end
    if !isnothing(hit)
        full = hit_full(hit.obj, hit)
        factor = full.front ? 1 : .5
        col = (full.normal + Vec3([1, 1, 1])) * factor / 2
        RGB(x(col), y(col), z(col))
    else
        bg_color(ray)
    end
end

end # module RT


# Gibbs Sampling Bayesian Probabilistic Matrix Factorization
#
# Reference
# ----------
#   Salakhutdinov, Ruslan, and Andriy Mnih. "Bayesian probabilistic matrix factorization using Markov chain Monte Carlo."
#   Proceedings of the 25th international conference on Machine learning. ACM, 2008.
#
mutable struct GBPMFModel
    R::Array
    N::Int64
    M::Int64
    D::Int64
    T::Int64
    U::Array
    V::Array
    α::Float64
    β₀::Float64
    μ₀::Float64
    ν₀::Float64
    W₀::Array
end

function fit(model::GBPMFModel)
    #set initial value
    model.U = zeros((model.T, model.N, model.D)) #user latent matrix
    model.V = zeros((model.T, model.M, model.D)) #item latent matrix
    model.U[1, :, :] = rand(model.N, model.D)
    model.V[1, :, :] = rand(model.M, model.D)

    T = model.T
    N = model.N
    M = model.M

    for t in 1:(T-1) #1 is initial value
        #u matrix
        Θᵤ = hyper_prameter_posterior(model, model.U[t, :, :])
        μᵤ, Λᵤ = hyper_parameter_sampling(Θᵤ[1], Θᵤ[2], Θᵤ[3], Θᵤ[4])

        #v matrix
        Θᵥ = hyper_prameter_posterior(model, model.V[t, :, :])
        μᵥ, Λᵥ = hyper_parameter_sampling(Θᵥ[1], Θᵥ[2], Θᵥ[3], Θᵥ[4])

        for n in 1:N
            μᵢ_asterisk, Λᵢ_asterisk_inv = u_matrix_posterior(model, model.V[t, :, :], μᵤ, Λᵤ, n)
            model.U[t + 1, n, :] = latent_matrix_sampling(μᵢ_asterisk, Λᵢ_asterisk_inv)
        end

        for m in 1:M
            μᵢ_asterisk, Λᵢ_asterisk_inv = v_matrix_posterior(model, model.U[t + 1, :, :], μᵥ, Λᵥ, m)
            model.V[t + 1, m, :] = latent_matrix_sampling(μᵢ_asterisk, Λᵢ_asterisk_inv)
        end

    end
end

function hyper_prameter_posterior(model::GBPMFModel, latent_matrix)
    N = size(latent_matrix)[2]
    β₀ = model.β₀
    μ₀ = model.μ₀
    ν₀ = model.ν₀
    W₀ = model.W₀

    latent_matrix_bar = (1 / N) * sum(latent_matrix[:, :], dims = 1)
    S̄ = (1 / N) .* sum([latent_matrix[i, :]' .* latent_matrix[i, :] for i = 1:N], dims = 1)[1]

    μ₀_asterisk = ((β₀ * μ₀ .+ N * latent_matrix_bar) / (β₀ + N))[1, :]
    β₀_asterisk = β₀ + N
    ν₀_asterisk = ν₀ + N

    W₀_asterisk = inv(inv(W₀) .+ N .* S̄ .+ ((β₀ * N) / (β₀ + N)) .* (μ₀ .- latent_matrix_bar)' * (μ₀ .- latent_matrix_bar))
    W₀_asterisk = Array(Symmetric(W₀_asterisk))

    return μ₀_asterisk, β₀_asterisk, ν₀_asterisk, W₀_asterisk
end

function hyper_parameter_sampling(μ₀_asterisk, β₀_asterisk, ν₀_asterisk, W₀_asterisk)
    Λ = rand(Wishart(ν₀_asterisk, W₀_asterisk), 1)[1]
    μ = rand(MvNormal(μ₀_asterisk, Array(Symmetric(inv(β₀_asterisk * Λ)))), 1)

    return μ, Λ
end

function u_matrix_posterior(model::GBPMFModel, latent_matrix, μᵤ, Λᵤ, n)
    R = model.R
    α = model.α

    r = R[R[:, 1] .==  n - 1, :] #user's rate item
    nonzero_ix = (R[R[:, 1] .==  n - 1, :][:, 2] .+ 1) #user's rate item index

    if nonzero_ix == [] #when no rate then return random value
        μᵢ_asterisk = zeros(size(latent_matrix)[2])
        Λᵢ_asterisk = one(zeros(size(latent_matrix)[2], size(latent_matrix)[2]))
        return μᵢ_asterisk, Λᵢ_asterisk
    end

    Λᵢ_asterisk = Λᵤ .+ α * sum([latent_matrix[i, :]' .*  latent_matrix[i, :] for i in nonzero_ix], dims = 1)[1]
    Λᵢ_asterisk_inv = Array(Symmetric(inv(Λᵢ_asterisk)))

    μᵢ_asterisk = (inv(Λᵢ_asterisk) * ((α * sum([latent_matrix[i, :] .* r[r[:, 2] .== i - 1, :][:, 3] for i in nonzero_ix], dims = 1)[1]') + (Λᵤ * μᵤ)')')[:, 1]

    return μᵢ_asterisk, Λᵢ_asterisk_inv
end

function v_matrix_posterior(model::GBPMFModel, latent_matrix, μᵥ, Λᵥ, m)
    R = model.R
    α = model.α

    r = R[R[:, 2] .==  m - 1, :] #item rate user
    nonzero_ix = (R[R[:, 2] .==  m - 1, :][:, 1] .+ 1) #item rate user index

    if nonzero_ix == [] #when no rate then return random value
        μᵢ_asterisk = zeros(size(latent_matrix)[2])
        Λᵢ_asterisk = one(zeros(size(latent_matrix)[2], size(latent_matrix)[2]))
        return μᵢ_asterisk, Λᵢ_asterisk
    end

    Λᵢ_asterisk = Λᵥ .+ α * sum([latent_matrix[i, :]' .*  latent_matrix[i, :] for i in nonzero_ix], dims = 1)[1]
    Λᵢ_asterisk_inv = Array(Symmetric(inv(Λᵢ_asterisk)))

    μᵢ_asterisk = (inv(Λᵢ_asterisk) * ((α * sum([latent_matrix[i, :] .* r[r[:, 1] .== i - 1, :][:, 3] for i in nonzero_ix], dims = 1)[1]') + (Λᵥ * μᵥ)')')[:, 1]

    return μᵢ_asterisk, Λᵢ_asterisk_inv
end

function latent_matrix_sampling(μᵢ_asterisk, Λᵢ_asterisk_inv)
    return  rand(MvNormal(μᵢ_asterisk, Λᵢ_asterisk_inv), 1)
end

function predict_all(model::GBPMFModel, burn_in)
    T = model.T
    sum([model.U[i, :, :] *  model.V[i,:,  :]' for i = burn_in:T], dims = 1)[1]  / (T - burn_in )
end

function predict(model::GBPMFModel, new_data, burn_in)
    R_p = predict_all(model, burn_in)
    r = zeros(size(new_data)[1])
    for i in 1:size(new_data)[1]
        r[i] = R_p[new_data[i, 1] + 1, new_data[i, 2] + 1]
    end
    return r
end


module Optimize

include("orbits.jl")
include("observables.jl")

using Schwarz.Orbits: OrbitLibrary, BinnedLibrary
using Schwarz.Observables: KinematicData, binedges

function objective(lib::BinnedLibrary, data::KinematicData)
    #=
    From an orbit library and kinematic data, return the objective function,
    its gradient, constraints, etc.
    =#
    # lib = BinnedLibrary(lib, data.edges)
    # m = fractional mass times sigma^2
    # m_lib is n_orbit rows by n_bins columns matrix
    m_lib = [lib.orbits[i].mass[j] * lib.orbits[i].dispersion[j]
             for i in 1:length(lib), j in 1:length(data)]
    mass_lib = [lib.orbits[i].mass[j]
                for i in 1:length(lib), j in 1:length(data)]
    m_data = [data.mass[j] * data.dispersion[j] for j in 1:length(data)]
    dm_data = [sqrt((data.unc_mass[j] * data.dispersion[j])^2
                    + (data.unc_dispersion[j] * data.mass[j])^2)
               for j in 1:length(data)]
    u_data = 1 ./ dm_data .^ 2
    Q = Array{Float64, 2}(length(lib), length(lib))
    for i in 1:length(lib), j in 1:i
        if i == j
            Q[i, j] = 2 * sum(u_data .* m_lib[i, :].^2)
        else
            x = 4 * sum(u_data .* m_lib[i, :] .* m_lib[j, :])
            Q[i, j] = x
            Q[j, i] = x
        end
    end
    c = -2 * [sum(u_data .* m_data .* m_lib[i, :]) for i in 1:length(lib)]

    # objective
    function eval_f(weights::Vector{Float64})
        0.5 * dot(weights, Q * weights) + dot(c, weights)
    end

    # gradient of objective
    function eval_grad_f(weights::Vector{Float64}, grad_f::Vector{Float64})
        grad_f[:] = (Q * weights) .+ c
    end

    # constraints
    function eval_g(weights::Vector{Float64}, g::Vector{Float64})
        weighted_mass_lib = [dot(weights, mass_lib[:, j]) for j in 1:length(data)]
        # upper bound of 1
        g[1:(end - 1)] = abs(weighted_mass_lib .- data.mass) ./ data.unc_mass
        # upper and lower bound of 1
        g[end] = sum(weights)
    end

    # jacobian of constraints
    function eval_jac_g(weights::Vector{Float64}, mode,
                        rows::Vector{Int32}, cols::Vector{Int32},
                        values::Vector{Float64})
        #=
        rows: n_bins + 1 constraints, cols: n_orbit variables
        =#
        weighted_mass_lib = [dot(weights, mass_lib[:, j]) for j in 1:length(data)]
        sgn = map(j -> sign(data.mass[j] - weighted_mass_lib[j]),
                  1:length(data))
        if mode == :Structure
            idx = 1
            for row in 1:(length(data) + 1)
                for col in 1:length(lib)
                    rows[idx] = row
                    cols[idx] = col
                    idx += 1
                end
            end
        else
            count = 1
            for i in 1:length(lib)
                values[count:(count - 1 + length(data))] =
                    [sgn[j] * mass_lib[i, j] / data.unc_mass[j]
                     for j in 1:length(data)]
                values[count + length(data)] = 1.0
                count += length(data) + 1
            end
        end
    end

    function eval_h(weights::Vector{Float64}, mode,
                    rows::Vector{Int32}, cols::Vector{Int32},
                    obj_factor::Float64, lambda::Vector{Float64},
                    values::Vector{Float64})
        if mode == :Structure
            idx = 1
            for row in 1:(length(lib))
                for col in 1:row
                    rows[idx] = row
                    cols[idx] += 1
                end
            end
        else
            idx = 1
            for row in 1:(length(lib))
                for col in 1:row
                    values[idx] = obj_factor * Q[row, col]
                    idx += 1
                end
            end
        end
    end

    return eval_f, eval_grad_f, eval_g, eval_jac_g, eval_h
end

function fit(lib::BinnedLibrary, data::KinematicData)
    nbins = length(data)
    n = length(lib) # number of variables
    x_L = zeros(n)
    x_U = ones(n)
    m = nbins + 1 # number of constraints
    g_L = vcat(zeros(nbins), 1)
    g_U = ones(m)
    nele_jac = m * n
    nele_hess = sum(1:n)
    eval_f, eval_grad_f, eval_g, eval_jac_g, eval_h = objective(lib, data)

    println("Creating problem")
    prob = createProblem(n, x_L, x_U, m, g_L, g_U, nele_jac, nele_hess,
                         eval_f, eval_g, eval_grad_f, eval_jac_g, eval_h)
    prob.x = ones(n) / n
    println("Solving problem")
    status = solveProblem(prob)
    println(Ipopt.ApplicationReturnStatus[status])
    println("Weights: ", prob.x)
    println("Objective: ", prob.obj_val)
    return prob
end

end


@testset "Extensive tests" begin
    @testset "Lines on a quintic surface in 3-space" begin
        sys, q₀, gamma = fano_quintic()
        @time res = solve(
            sys;
            gamma = gamma,
            target_parameters = q₀,
            start_system = :total_degree,
            show_progress = true,
        )
        @test nsolutions(res) == 2875
        @test ndistinct_certified(certify(sys, res, q₀)) == 2875

        @time poly_res = solve(sys; target_parameters = q₀, start_system = :polyhedral)
        @test nsolutions(poly_res) == 2875
    end

    @testset "3264" begin
        F = steiner()
        p = randn(ComplexF64, 30)
        res = solve(F; target_parameters = p)
        @test nresults(res; only_nonsingular = true) == 3264
        @test ndistinct_certified(certify(F, res, p)) == 3264

        real_conics = [
            10124547 // 662488724,
            8554609 // 755781377,
            5860508 // 2798943247,
            -251402893 // 1016797750,
            -25443962 // 277938473,
            1 // 1,
            520811 // 1788018449,
            2183697 // 542440933,
            9030222 // 652429049,
            -12680955 // 370629407,
            -24872323 // 105706890,
            1 // 1,
            6537193 // 241535591,
            -7424602 // 363844915,
            6264373 // 1630169777,
            13097677 // 39806827,
            -29825861 // 240478169,
            1 // 1,
            13173269 // 2284890206,
            4510030 // 483147459,
            2224435 // 588965799,
            33318719 // 219393000,
            92891037 // 755709662,
            1 // 1,
            8275097 // 452566634,
            -19174153 // 408565940,
            5184916 // 172253855,
            -23713234 // 87670601,
            28246737 // 81404569,
            1 // 1,
        ]

        real_res = solve(
            F,
            solutions(res);
            start_parameters = p,
            target_parameters = real_conics,
            tracker_options = (parameters = :conservative,),
        )

        @test nsolutions(real_res) == 3264
        real_cert = certify(F, real_res, real_conics)
        @test ndistinct_certified(real_cert) == 3264
        @test ndistinct_real_certified(real_cert) == 3264

        direct_real_res = solve(F; target_parameters = real_conics)
        @test nresults(direct_real_res; only_nonsingular = true) == 3264
    end
end


module XYCoverPartialBase

include("./../../constraints/geometric/constants.jl")
using JuMP
using .Constants

export xy_cover

function xy_cover(model)

    #max(flbx_front_left_bot_x[i], flbx_front_left_bot_x[j])
    #max(flby_front_left_bot_y[i], flby_front_left_bot_y[j])

    @variable(model, max_flbx_ij[i=1:length(boxes), j=1:length(boxes); i!=j] >= 0)
    @constraint(model, get_max_flbx_of_ij[i=1:length(boxes), j=1:length(boxes); i!=j],
        model[:max_flbx_ij][i, j] .>=
        [model[:flbx_front_left_bot_x][i], model[:flbx_front_left_bot_x][j]]
    )

    @variable(model, max_flby_ij[i=1:length(boxes), j=1:length(boxes); i!=j] >= 0)
    @constraint(model, get_max_flby_of_ij[i=1:length(boxes), j=1:length(boxes); i!=j],
        model[:max_flby_ij][i, j] .>=
        [model[:flby_front_left_bot_y][i], model[:flby_front_left_bot_y][j]]
    )

    #min(flbx_front_left_bot_x[i] + box_i_x_axis_cover, flbx_front_left_bot_x[j] + box_j_x_axis_cover)
    #min(flby_front_left_bot_y[i] + box_i_y_axis_cover, flby_front_left_bot_y[j] + box_j_y_axis_cover)

    @variable(model, min_flbx_ij[i=1:length(boxes), j=1:length(boxes); i!=j] >= 0)
    @constraint(model, get_min_flbx_of_ij[i=1:length(boxes), j=1:length(boxes); i!=j], (
        [model[:flbx_front_left_bot_x][i] + model[:box_x_axis_cover][i], model[:flbx_front_left_bot_x][j] + model[:box_x_axis_cover][j]] .>=
        model[:min_flbx_ij][i, j])
    )

    @variable(model, min_flby_ij[i=1:length(boxes), j=1:length(boxes); i!=j] >= 0)
    @constraint(model, get_min_flby_of_ij[i=1:length(boxes), j=1:length(boxes); i!=j], (
        [model[:flby_front_left_bot_y][i] + model[:box_y_axis_cover][i], model[:flby_front_left_bot_y][j] + model[:box_y_axis_cover][j]] .>=
        model[:min_flby_ij][i, j])
    )

    #x_axis_covered = min(flbx_front_left_bot_x[i] + box_i_x_axis_cover, flbx_front_left_bot_x[j] + box_j_x_axis_cover) - max(flbx_front_left_bot_x[i], flbx_front_left_bot_x[j])
    #y_axis_covered = min(flby_front_left_bot_y[i] + box_i_y_axis_cover, flby_front_left_bot_y[j] + box_j_y_axis_cover) - max(flby_front_left_bot_y[i], flby_front_left_bot_y[j])

    @expression(model, x_axis_covered[i=1:length(boxes), j=1:length(boxes); i!=j],
        model[:min_flbx_ij][i, j] - model[:max_flbx_ij][i, j]
    )
    @expression(model, y_axis_covered[i=1:length(boxes), j=1:length(boxes); i!=j],
        model[:min_flby_ij][i, j] - model[:max_flby_ij][i, j]
    )

    return model
end

end


using LinearAlgebra
function sfvf(x, y)
    sign(x) * max(abs(x) - y, 0)
end
function fit(x, t; alpha = 0.1, tol = 0.0001, mi = 1000000)
    function update(n, d, x, t, w, alpha)
        l = length(w)
        w[1] = sum(t - x * w[2:l]) / n
        wvec = ones(n) * w[1]
        for k = 1:d
            ww = w[2:l]
            ww[k] = 0
            q = dot(vec(t) - wvec - x * ww, x[:, k])
            r = dot(x[:, k], x[:, k])
            w[k+1] = sfvf(q / r, alpha)
        end
    end
    if size(x)[1] != size(t)[1]
        x = x'
        t = t'
    end
    n, d = size(x)
    w = zeros(d + 1)
    e = 0.0
    for i = 1:mi
        eb = e
        update(n, d, x, t, w, alpha)
        e = sum(broadcast(abs, w)) / size(w)[1]
        if abs(e - eb) <= tol
            return w
        end
    end
end
function predict(x, w)
    x2 = ones(1, size(x)[2])
    x = vcat(x2, x)
    return w' * x
end


export HS

# References:
# * Lefkimmiatis, Stamatios, John Paul Ward, and Michael Unser. "Hessian Schatten-norm regularization for linear inverse problems." IEEE transactions on image processing 22.5 (2013): 1873-1888.
# * Lefkimmiatis, Stamatios, and Michael Unser. "Poisson image reconstruction with Hessian Schatten-norm regularization." IEEE transactions on image processing 22.11 (2013): 4314-4327.

function Δr1r1(x)
    return @tullio res[i, j] := x[i+2, j+0] - 2 * x[i+1, j] + x[i, j] (i in 1:size(x)[1]-2, j in 1:size(x)[2]-2)
end

function Δr2r2(x)
    return @tullio res[i, j] := x[i+0, j+2] - 2 * x[i, j+1] + x[i, j] (i in 1:size(x)[1]-2, j in 1:size(x)[2]-2)
end

function Δr1r2(x)
    return @tullio res[i, j] := x[i+1, j+1] - x[i+1, j] - x[i, j+1]  + x[i, j] (i in 1:size(x)[1]-2, j in 1:size(x)[2]-2)
end

"""
    HS(; p=1)

Hessian Schatten norm. `p` determines which Schatten norm is used.

This regularizer only works with 2D arrays at the moment.
"""
function HS(;p=1)
    if isone(p)
        return HS1
    end

    f(x) = HSp(x, p=p)
    return f
end

"""
Hessian schatten norm for p=1 efficiently with Tullio.
"""
function HS1(arr)
    H11 = Δr1r1(arr)
    H22 = Δr2r2(arr)
    return schatten_norm_1(H11, H22)
end

function schatten_norm_1(a, d)
    @tullio A[i, j] := a[i, j] + d[i, j]
    @tullio res = abs(1f-8 + A[i, j])
end

"""
Hessian schatten norm for p.
But not as fast as p=1
"""
function HSp(arr; p=1)
    H11 = Δr1r1(arr)
    H22 = Δr2r2(arr)
    H12 = Δr1r2(arr)

    res = schatten_norm_tullio(H11, H12, H22, p)
    return sum(res)
end

function schatten_norm(H11, H12, H22, p)
    λ₁, λ₂ = eigvals_symmetric(H11, H12, H22)
    return (λ₁^p + λ₂^p )^(1/p)
end

function schatten_norm_tullio(H11, H12, H22, p)
    λ₁, λ₂ = eigvals_symmetric_tullio(H11, H12, H22)
    return @tullio res = abs((1f-8 + λ₁[i, j]^p + λ₂[i, j]^p))^(1/p)
end

"""
    eigvals_symmetric(a,b,c)

Calculate the eigenvalues of the matrix
[a b; b d] analytically.
"""
function eigvals_symmetric(a, b, d)
    A = a+d
    B = sqrt((a-d)^2+4*b^2)
    λ₁ = 0.5 * (A + B)
    λ₂ = 0.5 * (A - B)
    return λ₁, λ₂
end

function eigvals_symmetric_tullio(a, b, d)
    @tullio A[i, j] := a[i, j] + d[i, j]
    @tullio B[i, j] := sqrt(1f-8 + (a[i, j]-d[i, j])^2+4*b[i, j]^2)
    @tullio λ₁[i, j] := 0.5 * (A[i, j] + B[i, j])
    @tullio λ₂[i, j] := 0.5 * (A[i, j] - B[i, j])
    return λ₁, λ₂
end


struct mv_group_gaussian <: distibution_sample
    μ::Vector{AbstractArray{Float64,1}}
    Σ::AbstractArray{Float64,2}
    invΣ::AbstractArray{Float64,2}
    logdetΣ::Float64
end

function log_likelihood!(r::AbstractArray,x::AbstractArray, distibution_sample::mv_group_gaussian ,group::Int64 = 1)
    z = x .- distibution_sample.μ[group]
    dcolwise_dot!(r,z, distibution_sample.invΣ * z)
    r .= -((length(distibution_sample.Σ) * Float64(log(2π)) + logdet(distibution_sample.Σ))/2) .-r
end


using Test
using DiffEqPhysics, ForwardDiff, OrdinaryDiffEq
using StaticArrays, LinearAlgebra, Random

test_solve(prob...) = mapreduce(p->solve(p, Tsit5(), dt=1//2).u, ==, prob)

p0, q0   = rand(2)
H(dθ, θ, p) = dθ / 2 - 9.8 * cos(θ)
dp(dθ, θ, p, t) = - 9.8 * sin(θ)
dq(dθ, θ, p, t) = 0.5
acc      = (v, x, p, t) -> ForwardDiff.derivative(x -> -H(v[1], x, p), x[1])
vel      = (v, x, p, t) -> ForwardDiff.derivative(v -> H(v, x[1], p), v[1])
prob_1   = DynamicalODEProblem(acc, vel, p0, q0, (0., 10.))

@testset "prob1($h)" for h in (H, (dp, dq))
    prob1    = HamiltonianProblem(h, p0, q0, (0., 10.))
    @test test_solve(prob1, prob_1)
end

p0 = @SVector rand(2)
q0 = @SVector rand(2)
H4(dθ, θ, p, t) = dθ[1] / 2 + dθ[2] / 2 - 9.8 * cos(θ[1]) - 9.8 * cos(θ[2]) + t
dq(dθ, θ, p, t) = @SVector [0.5, 0.5]
dp(dθ, θ, p, t) = @SVector [-9.8 * sin(θ[1]), -9.8 * sin(θ[2])]
acc      = (v, x, p, t) -> ForwardDiff.gradient(x -> -H4(v, x, p, t), x)
vel      = (v, x, p, t) -> ForwardDiff.gradient(v -> H4(v, x, p, t), v)
prob_2   = DynamicalODEProblem(acc, vel, p0, q0, (0., 10.))

@testset "prob2($h)" for h in (H4, (dp, dq))
    prob2    = HamiltonianProblem(h, p0, q0, (0., 10.))
    @test test_solve(prob2, prob_2)
end

H(dθ, θ, p) = (dθ / 2 - 9.8 * cos.(θ))[1]
p0, q0 = [rand(5) for i in 1:2]
dq(dx, dΘ, θ, p, t) = (dx .= [0.5, 0, 0, 0, 0])
dp(dv, dθ, θ, p, t) = (dv .= [(-9.8 * sin.(θ))[1], 0, 0, 0, 0])
acc = (dv, v, x, p, t) -> ForwardDiff.gradient!(dv, x -> -H(v, x, p), x)
vel = (dx, v, x, p, t) -> ForwardDiff.gradient!(dx, v -> H(v, x, p), v)
prob_3 = DynamicalODEProblem(acc, vel, p0, q0, (0., 10.))

@testset "prob3($h)" for h in (H, (dp, dq))
    prob3  = HamiltonianProblem(h, p0, q0, (0., 10.))
    @test test_solve(prob3, prob_3)
end

H4(dθ, θ, p, t) = (dθ / 2 - 9.8 * cos.(θ))[1] + t
p0, q0 = [rand(5) for i in 1:2]
dq(dx, dΘ, θ, p, t) = (dx .= [0.5, 0, 0, 0, 0])
dp(dv, dθ, θ, p, t) = (dv .= [(-9.8 * sin.(θ))[1], 0, 0, 0, 0])
acc = (dv, v, x, p, t) -> ForwardDiff.gradient!(dv, x -> -H4(v, x, p, t), x)
vel = (dx, v, x, p, t) -> ForwardDiff.gradient!(dx, v -> H4(v, x, p, t), v)
prob_4 = DynamicalODEProblem(acc, vel, p0, q0, (0., 10.))

@testset "prob4($h)" for h in (H4, (dp, dq))
    prob4  = HamiltonianProblem(h, p0, q0, (0., 10.))
    @test test_solve(prob4, prob_4)
end


# From https://github.com/JuliaIntervals/TaylorModels.jl/issues/127
@testset "Taylor model" begin
    using ReachableControls.TaylorModels

    n(z, a) = a * z
    d(z, a) = 1 + a * z
    G(z, a) = n(z, a) / d(z, a)

    dom_a = 0.5 ± 0.125 # domain for a
    dom_ω = 1 ± (1 / 16)  # domain for ω

    set_variables("δa δω", order = 6)
    aN = TaylorModelN(1, 3, Interval(mid(dom_a)) × Interval(mid(dom_ω)), dom_a × dom_ω)
    ωN = TaylorModelN(2, 3, Interval(mid(dom_a)) × Interval(mid(dom_ω)), dom_a × dom_ω)

    gNr, gNi = G(as_real(ωN)*im, as_real(aN))

    gNi(IntervalBox(dom_a - mid(dom_a), dom_ω - mid(dom_ω)))
end


# Write function to automate plotting qqplot
# - Plots standard quantile-quantile plot for assessing linear model fits

function plot_qq(model)

    # Extract predicted values from model object
    pred = GLM.predict(model)

    # Extract residuals from model object
    res = GLM.residuals(model)

    # Calculate mean of residuals
    res_mean = StatsBase.mean(res)

    # Calculate length (n) of residuals vector
    n = length(res)

    # Calculate standardised residuals
    stdRes = (res - res_mean * ones(n)) / (Statistics.std(res))

    # Define quantiles
    qx = Distributions.quantile.(Distributions.Normal(),
                                    range(0.5,
                                    stop = (n - 0.5),
                                    length = (n)) / (n + 1))

    # Create plot
    p = Gadfly.plot(
                    # Add points layer
                    Gadfly.layer(x = qx,
                                 y = sort(stdRes),
                                 Gadfly.Geom.point),
                    # Add 1:1 line
                    Gadfly.layer(x = [-3,3],
                                 y = [-3,3],
                                 Gadfly.Geom.line,
                                 Gadfly.style(line_style = [:dot])),
                    # Change plot aesthetics
                    Gadfly.Guide.title("Normal Q-Q plot"),
                    Gadfly.Guide.xlabel("Theoretical Quantiles"),
                    Gadfly.Guide.ylabel("Standardized residuals"))

end


@defcomp LGcycle begin
    e_globalLGemissions=Parameter(index=[time],unit="Mtonne/year")
    e_0globalLGemissions=Parameter(unit="Mtonne/year", default=557.2112715473608)
    c_LGconcentration=Variable(index=[time],unit="ppbv")
    pic_preindustconcLG=Parameter(unit="ppbv", default=0.)
    exc_excessconcLG=Variable(unit="ppbv")
    c0_LGconcbaseyr=Parameter(unit="ppbv", default=0.11)
    re_remainLG=Variable(index=[time],unit="Mtonne")
    nte_natLGemissions=Variable(index=[time],unit="Mtonne/year")
    air_LGfractioninatm=Parameter(unit="%", default=100.)
    tea_LGemissionstoatm=Variable(index=[time],unit="Mtonne/year")
    teay_LGemissionstoatm=Variable(index=[time],unit="Mtonne/t")
    y_year=Parameter(index=[time],unit="year")
    y_year_0=Parameter(unit="year")
    res_LGatmlifetime=Parameter(unit="year", default=1000.)
    den_LGdensity=Parameter(unit="Mtonne/ppbv", default=100000.)
    stim_LGemissionfeedback=Parameter(unit="Mtonne/degreeC", default=0.)
    rtl_g0_baselandtemp=Parameter(unit="degreeC", default=0.9258270139190647)
    rtl_g_landtemperature=Parameter(index=[time],unit="degreeC")
    re_remainLGbase=Variable(unit="Mtonne")

    function run_timestep(p, v, d, t)
        if is_first(t)
            #eq.3 from Hope (2006) - natural emissions (carbon cycle) feedback, using global temperatures calculated in ClimateTemperature component
            nte0=p.stim_LGemissionfeedback*p.rtl_g0_baselandtemp
            v.nte_natLGemissions[t]=p.stim_LGemissionfeedback*p.rtl_g0_baselandtemp
            #eq.6 from Hope (2006) - emissions to atmosphere depend on the sum of natural and anthropogenic emissions
            tea0=(p.e_0globalLGemissions+nte0)*p.air_LGfractioninatm/100
            v.tea_LGemissionstoatm[t]=(p.e_globalLGemissions[t]+v.nte_natLGemissions[t])*p.air_LGfractioninatm/100
            v.teay_LGemissionstoatm[t]=(v.tea_LGemissionstoatm[t]+tea0)*(p.y_year[t]-p.y_year_0)/2
            #adapted from eq.1 in Hope(2006) - calculate excess concentration in base year
            v.exc_excessconcLG=p.c0_LGconcbaseyr-p.pic_preindustconcLG
            #Eq. 2 from Hope (2006) - base-year remaining emissions
            v.re_remainLGbase=v.exc_excessconcLG*p.den_LGdensity
            v.re_remainLG[t]=v.re_remainLGbase*exp(-(p.y_year[t]-p.y_year_0)/p.res_LGatmlifetime)+
                v.teay_LGemissionstoatm[t]*p.res_LGatmlifetime*(1-exp(-(p.y_year[t]-p.y_year_0)/p.res_LGatmlifetime))/(p.y_year[t]-p.y_year_0)
        else
            #eq.3 from Hope (2006) - natural emissions (carbon cycle) feedback, using global temperatures calculated in ClimateTemperature component
            #Here assume still using area-weighted average regional temperatures (i.e. land temperatures) for natural emissions feedback
            v.nte_natLGemissions[t]=p.stim_LGemissionfeedback*p.rtl_g_landtemperature[t-1]
            #eq.6 from Hope (2006) - emissions to atmosphere depend on the sum of natural and anthropogenic emissions
            v.tea_LGemissionstoatm[t]=(p.e_globalLGemissions[t]+v.nte_natLGemissions[t])*p.air_LGfractioninatm/100
            #eq.7 from Hope (2006) - average emissions to atm over time period
            v.teay_LGemissionstoatm[t]=(v.tea_LGemissionstoatm[t]+v.tea_LGemissionstoatm[t-1])*(p.y_year[t]-p.y_year[t-1])/2
            #eq.10 from Hope (2006) - remaining emissions in atmosphere
            v.re_remainLG[t]=v.re_remainLG[t-1]*exp(-(p.y_year[t]-p.y_year[t-1])/p.res_LGatmlifetime)+
                v.teay_LGemissionstoatm[t]*p.res_LGatmlifetime*(1-exp(-(p.y_year[t]-p.y_year[t-1])/p.res_LGatmlifetime))/(p.y_year[t]-p.y_year[t-1])
        end

    #eq.11 from Hope(2006) - LG concentration
        v.c_LGconcentration[t]=p.pic_preindustconcLG+v.exc_excessconcLG*v.re_remainLG[t]/v.re_remainLGbase

    end
end


include("euler/euler.jl")
using .Numbers: digits_sum
using .Primes: is_prime

function compute(n::Int)::Int
    harshad_numbers = collect(1:9)
    result = 0
    for _ ∈ 1:n - 2
        new_harshad_numbers = Vector{Int}()
        for i ∈ 0:9
            for harshad ∈ harshad_numbers
                if (harshad * 10 + i) % (digits_sum(harshad) + i) == 0
                    new_harshad = harshad * 10 + i
                    push!(new_harshad_numbers, new_harshad)
                    if is_prime(new_harshad ÷ digits_sum(new_harshad)) for j ∈ [1, 3, 7, 9] if is_prime(new_harshad * 10 + j) result += new_harshad * 10 + j end end end
                end
            end
        end
        harshad_numbers = new_harshad_numbers
    end
    return result
end


using ReachabilityBenchmarks, MathematicalSystems, Reachability, Plots

include(@relpath "cdplayer_model.jl")
include(@relpath "cdplayer_specifications.jl")

S = cdplayer_model()
X0, options = cdplayer_specification()
options = Dict(options)

# initial value problem
problem = InitialValueProblem(S, X0)

# partition with 1D blocks
partition = [i:i for i in 1:120]

# reachability algorithms
algorithm_reach = BFFPSV18(:δ => 1e-3, :vars => [1], :partition => partition,
                           :assume_sparse => true)
algorithm_check = BFFPSV18(:δ => 1e-3, :vars => [1, 2], :partition => partition,
                           :assume_sparse => true)

# check property (does not verify for the given initial states)
options[:mode] = "check"
solution = solve(problem, options; op=algorithm_check)
# @assert solution.satisfied

# compute flowpipe
options[:mode] = "reach"
solution = solve(problem, options; op=algorithm_reach)

# project flowpipe to time and x₁
solution.options[:plot_vars] = [0, 1]
solution_proj = project(solution)

# plot projection
plot(solution_proj)
savefig("cdplayer")


## Farfield dynamics
#==========================================================================================#

"""
    trefftz_velocity(r_i, r_j, Γ_j)

Compute the induced velocity at point ``r_i`` of the wake in the Trefftz plane due to the strength ``\\Gamma_j`` at ``r_j``.
"""
trefftz_velocity(r_i, r_j, Γ_j) = let r = r_i - r_j; Γ_j / 2π * SVector(1, 0, 0) × r / norm(r)^2 end

"""
    trefftz_influence_matrix(centers, normals, points)

Compute the aerodynamic influence coefficient matrix of the wake in the Trefftz plane given the center points, normal vectors, and the points of the wake panels.
"""
trefftz_influence_matrix(centers, normals, points) = [ dot(trefftz_velocity(r_i, r_j2, 1.) - trefftz_velocity(r_i, r_j1, 1.), n_i) for (r_i, n_i) in zip(centers, normals), (r_j2, r_j1) in zip(points[2:end], points) ]

"""
    doublet_normal_derivatives(wake_lines :: Vector{<: Line}, Δφs, normals)

Compute the normal derivative strengths of the doublets given the wake `Line`s, net doublet strengths ``\\Delta \\phis``, and associated normal vectors.
"""
function doublet_normal_derivatives(wake_lines :: Vector{<: Line}, Δφs, normals)
    centers = center.(wake_lines)
    pts 	= points(wake_lines)
    AIC   	= trefftz_influence_matrix(centers, normals, pts)
    ∂φ_∂n 	= AIC * Δφs
end

project_vector(vector, U) = vector - dot(U, vector) * U
normal(wake_proj_vector, U) = normalize(U × wake_proj_vector)
dihedral(wake_proj_vec)  = atan(wake_proj_vec[3], wake_proj_vec[2])

"""
    trefftz_preprocessing(horseshoes :: AbstractArray{<: Horseshoe}, α, β)

Project `Horseshoe`s into the Trefftz plane aligned with the wind axes angles ``\\alpha,~\\beta``, and compute normal vectors, projection angles and lengths.
"""
function trefftz_preprocessing(horseshoes :: AbstractArray{<: Horseshoe}, α, β)
    # Reference velocity for broadcasting
    U_ref = (Ref ∘ SVector)(1, 0, 0)

    # Transform to wind axes
    wake_lines     = @. body_to_wind_axes(bound_leg(horseshoes[end,:][:]), α, β)

    # Project trailing edge horseshoes' bound legs into Trefftz plane along wind axes
    wake_vectors   = @. vector(wake_lines);
    wake_proj_vecs = @. project_vector(wake_vectors, U_ref)

    # Normals, dihedral angles, and projection lengths
    wake_normals   = @. normal(wake_proj_vecs, U_ref)
    wake_dihedrals = @. dihedral(wake_proj_vecs)
    wake_lengths   = @. norm(wake_proj_vecs)

    wake_lines, wake_normals, wake_dihedrals, wake_lengths
end

"""
    trefftz_compute(Δφs, Δs, ∂φ_∂n, θs, V, ρ)

Compute the aerodynamic forces in the Trefftz plane given cumulative doublet strengths ``\\Delta \\phi``s, Trefftz panel lengths ``Δs``, doublet-normal directional derivatives ``\\partial \\phi / \\partial n``, Trefftz panel angles ``\\theta``s, the freestream speed and density ``V, \\rho``.
"""
function trefftz_compute(Δφs, Δs, ∂φ_∂n, θs, V, ρ)
    D_i = - 1/2 * ρ * sum(@. Δφs * Δs * ∂φ_∂n)
    Y   = - ρ * V * sum(@. Δφs * Δs * sin(θs))
    L   = ρ * V * sum(@. Δφs * Δs * cos(θs))

    SVector(D_i, Y, L)
end

# ifelse(symmetry, SVector(D_i, 0, 2L), SVector(D_i, Y, L))

"""
    trefftz_forces(Γs, horseshoes, freestream, ρ)

Compute the aerodynamic forces in the Trefftz plane normal to the freestream given horseshoes, their associated strengths Γs, and a density ρ.
"""
function trefftz_forces(Γs, horseshoes :: AbstractArray{<: Horseshoe}, speed, α, β, ρ)
    # Get projections of horseshoes into Trefftz plane with the associated normals, dihedral angles and lengths
    wake_lines, normals, dihedrals, Δs = trefftz_preprocessing(horseshoes, α, β)

    # Compute directional derivatives of doublets in the normal direction
    Δφs   = vec(sum(Γs, dims = 1))
    ∂φ_∂n = doublet_normal_derivatives(wake_lines, Δφs, normals)

    # Compute forces
    trefftz_compute(Δφs, Δs, ∂φ_∂n, dihedrals, speed, ρ)
end