module Isocontours
using WGLMakie, Bonito, Observables, StaticArrays, ForwardDiff, LinearAlgebra, Colors

dist2d(x, y) = sqrt(sum(((_x, _y),) -> (_y - _x)^2, zip(x, y)))


function ϕ(x, R, ε, m)
    r = norm(x)
    θ = atan(x[2], x[1])
    return r - rstar(θ, R, ε, m)
end

function iso_d_point(θ, R, ε, m, d)
    x = rstar(θ, R, ε, m) .* SA[cos(θ), sin(θ)]
    return iso_d_point(x, R, ε, m, d)
end

function iso_d_point(x::AbstractVector, R, ε, m, d)
    ν = compute_ν(x, R, ε, m, d)
    return x + d * ν
end

function compute_ν(θ, R, ε, m, d)
    x = rstar(θ, R, ε, m) .* SA[cos(θ), sin(θ)]
    return compute_ν(x, R, ε, m, d)
end

function compute_ν(x::AbstractVector, R, ε, m, d)
    ν = ForwardDiff.gradient(_x -> ϕ(_x, R, ε, m), x)
    ν = normalize(ν)
    return ν
end

rstar(θ, R, ε, m) = R * (1 + ε * cos(m * θ))

function isocontours()
    # Page()
    app = App() do session::Session
        h = 350
        aspect = 2
        fig = Figure(size=(aspect * h, h), fontsize=20)
        # fig = Figure(size=(aspect * h, h), fontsize=20, backgroundcolor=:transparent)

        # Settings
        n = 100
        R = 1.0
        ε = 0.1
        θ = LinRange(0, π, n)
        m = 6
        R_control = R / 4

        xmin = -1.5R
        xmax = -xmin
        ymin = 0.0
        ymax = 1.5 * R
        ax = Axis(
            fig[1, 1],
            limits=(xmin, xmax, ymin, ymax),
            leftspinevisible=false,
            rightspinevisible=false,
            bottomspinevisible=false,
            topspinevisible=false,
            aspect=aspect,
            # backgroundcolor=:transparent
        )
        hidedecorations!(ax)
        deregister_interaction!(ax, :rectanglezoom)
        deregister_interaction!(ax, :dragpan)
        deregister_interaction!(ax, :scrollzoom)
        deregister_interaction!(ax, :limitreset)

        # Control zone
        center = Point2f(0.0, 0.0)
        arc_points = [Point2f(R_control * cos(θ), R_control * sin(θ)) for θ in LinRange(0, π, 100)]
        poly!(ax, [center, arc_points..., center], color=colorant"rgb(243,243,243)")

        # dbg
        dbg = Observable("dbg")
        # text!(0.0, 0.4, text=dbg, align=(:center, :bottom))

        # Redefine mouseposition
        visible_xymin = Observable(true)
        visible_xymax = Observable(true)
        a_x = b_x = 0.0
        a_y = b_y = 0.0
        x1 = x2 = 0.0
        y1 = y2 = 0.0
        ms = 30
        c = :red
        scatter!([xmin], [ymin], visible=visible_xymin, markersize=ms, color=c)
        scatter!([xmax], [ymax], visible=visible_xymax, markersize=ms, color=c)
        function mymouseposition(ax)
            x, y = mouseposition(ax)
            return a_x * x + b_x, a_y * y + b_y
        end

        # Draw iso-d
        d = Observable(0.0)
        points_d = @lift([Point2f(iso_d_point(_θ, R, ε, m, $d)...) for _θ in θ])
        lines!(points_d; linestyle=:dash, color=:red)
        d_as_str = @lift("d=" * string(round($d, digits=2)))
        text!(0.0, 0.3, text=d_as_str, align=(:center, :bottom))

        # Draw iso-0
        c = colorant"#1154a6"
        points_d0 = [Point2f(rstar(_θ, R, ε, m) .* SA[cos(_θ), sin(_θ)]...) for _θ in θ]
        lines!(points_d0, color=c)

        # Draw normal vector
        l_ν = 0.15
        θ_ν = Observable(π / 4)
        ν_0_visible = Observable(true)
        c = :red
        points_ν_iso0 = @lift([Point2f(rstar($θ_ν, R, ε, m) .* [cos($θ_ν), sin($θ_ν)])])
        directions_ν_iso0 = @lift([Point2f(l_ν .* compute_ν($θ_ν, R, ε, m, $d)...)])
        arrows2d!(points_ν_iso0, directions_ν_iso0, color=c, visible=ν_0_visible)
        points_ν = @lift([Point2f(iso_d_point($θ_ν, R, ε, m, $d)...)])
        # directions_ν = @lift([Point2f(l_ν .* compute_ν(iso_d_point($θ_ν, R, ε, m, $d), R, ε, m, $d)...)])
        directions_ν = @lift([Point2f(l_ν .* compute_ν($θ_ν, R, ε, m, $d)...)])
        xytext_ν = @lift($points_ν[1] + $directions_ν[1])
        arrows2d!(points_ν, directions_ν, color=c)
        text!(xytext_ν, text=L"\nu = \nabla d", color=c)

        # Gradient and tangential projection
        ∇_visible = Observable(false)
        lr = 0.4
        lθ = 0.4
        points_∇ = points_ν
        directions_∇ = [Point2f(lr, lθ)]
        xytext_∇ = @lift($points_∇[1] + directions_∇[1])
        c = :darkorange
        text!(xytext_∇, text=L"\nabla u", color=c, offset=(5, 0), visible=∇_visible)
        arrows2d!(points_∇, directions_∇; color=c, visible=∇_visible)
        points_∇Γ = points_∇
        directions_∇Γ = lift(θ_ν, d) do _θ, _d
            ∇u = directions_∇[1].data
            ν = compute_ν(_θ, R, ε, m, _d)
            ∇Γu = ∇u .- (∇u ⋅ ν) .* ν
            return [Point2f(∇Γu...)]
        end
        xytext_∇Γ = @lift($points_∇[1] + $directions_∇Γ[1])
        c = :green
        xytext_∇Γ = text!(xytext_∇Γ, text=L"\nabla_d u", color=c, offset=(5, 0), visible=∇_visible)
        arrows2d!(points_∇Γ, directions_∇Γ; color=c, visible=∇_visible)

        # Listeners
        xclick = 0.0
        yclick = 0.0
        leftPressed = false
        selected_mode = :d
        on(events(ax).mousebutton) do event
            if event.button == Mouse.left && event.action == Mouse.press

                # dbg[] = string(mouseposition(ax) - SA[Δx, Δy])

                if visible_xymin[]
                    x1, y1 = mouseposition(ax)
                    visible_xymin[] = false
                    dbg[] = string(mouseposition(ax))
                    return
                elseif visible_xymax[]
                    x2, y2 = mouseposition(ax)
                    a_x = (xmax - xmin) / (x2 - x1)
                    b_x = xmax - a_x * x2
                    a_y = (ymax - ymin) / (y2 - y1)
                    b_y = ymax - a_y * y2
                    visible_xymax[] = false
                    dbg[] = string(mouseposition(ax))
                    return
                else
                    dbg[] = string(mymouseposition(ax))
                end

                if dist2d(mymouseposition(ax), (0, 0)) < R_control
                    if selected_mode == :d
                        selected_mode = :ν
                        msg = "Changing interaction mode from :d to :ν"
                        println(msg)
                        # dbg[] = msg
                    elseif (selected_mode == :ν) && (ν_0_visible[] == true)
                        ν_0_visible[] = false
                        ∇_visible[] = true
                        msg = "do not change selected mode, hide ν0"
                        println(msg)
                        # dbg[] = msg
                    else
                        selected_mode = :d
                        ν_0_visible[] = true
                        ∇_visible[] = false
                        msg = "Changing interaction mode from :ν to :d"
                        println(msg)
                        # dbg[] = msg
                    end
                    return
                else
                    # dbg[] = to_world(fig.scene, Point2f(2h, h))
                    # dbg[] = string(mouseposition_px(ax))
                    # dbg[] = string(events(ax).mouseposition)
                    # dbg[] = string(to_world(fig.scene, mouseposition(ax.scene)))
                    # dbg[] = string(mouseposition(ax.scene))
                    # dbg[] = string(mouseposition(fig))
                    # dbg[] = string(dist2d(mouseposition(ax), (0, 0)))
                end

                xclick, yclick = mymouseposition(ax)
                leftPressed = true
            else
                leftPressed = false
            end
        end
        on(events(ax).mouseposition) do mp
            leftPressed || return

            x, y = mymouseposition(ax)

            if selected_mode == :d
                X = SA[x, y]
                dir = X - SA[xclick, yclick]
                xclick = x
                yclick = y
                d[] += norm(dir) * sign(dir ⋅ X)
            elseif selected_mode == :ν
                θ_ν[] = atan(y, x)
            end
        end

        # Add everything to the DOM
        return DOM.div(fig)
    end
    return app
end

if haskey(ENV, "JULIA_EDITOR") && (ENV["JULIA_EDITOR"] == "code")
    @warn "VSCode display (not in slide)"
    display(isocontours())
end


end