module Viewer

using Blink
using Colors
using Printf

using Main.Engine

export ConstructionViewer, display!, opentools!, closetools!

# === Blink utilities ===

append_to_head!(w, type, content) = @js w begin
  @var element = document.createElement($type)
  element.appendChild(document.createTextNode($content))
  document.head.appendChild(element)
end

style!(w, stylesheet) = append_to_head!(w, "style", stylesheet)

script!(w, code) = append_to_head!(w, "script", code)

# === construction viewer ===

mutable struct ConstructionViewer
  win::Window
  
  function ConstructionViewer()
    # create window and open developer console
    win = Window(Blink.Dict(:width => 620, :height => 830))
    
    # set stylesheet
    style!(win, """
      body {
        background-color: #ccc;
      }
      
      /* the maximum dimensions keep Ganja from blowing up the canvas */
      #view {
        display: block;
        width: 600px;
        height: 600px;
        margin-top: 10px;
        margin-left: 10px;
        border-radius: 10px;
        background-color: #f0f0f0;
      }
      
      #control-panel {
        width: 600px;
        height: 200px;
        box-sizing: border-box;
        padding: 5px 10px 5px 10px;
        margin-top: 10px;
        margin-left: 10px;
        overflow-y: scroll;
        border-radius: 10px;
        background-color: #f0f0f0;
      }
      
      #control-panel > div {
        margin-top: 5px;
        padding: 4px;
        border-radius: 5px;
        border: solid;
        font-family: monospace;
      }
    """)
    
    # load Ganja.js. for an automatically updated web-hosted version, load from
    #
    #   https://unpkg.com/ganja.js
    #
    # instead
    loadjs!(win, "http://localhost:8000/ganja-1.0.204.js")
    
    # create global functions and variables
    script!(win, """
      // create algebra
      var CGA3 = Algebra(4, 1);
      
      // initialize element list and palette
      var elements = [];
      var palette = [];
      
      // declare handles for the view and its options
      var view;
      var viewOpt;
      
      // declare handles for the controls
      var controlPanel;
      var visToggles;
      
      // create scene function
      function scene() {
        commands = [];
        for (let n = 0; n < elements.length; ++n) {
          if (visToggles[n].checked) {
            commands.push(palette[n], elements[n]);
          }
        }
        return commands;
      }
      
      function updateView() {
        requestAnimationFrame(view.update.bind(view, scene));
      }
    """)
    
    @js win begin
      # create view
      viewOpt = Dict(
        :conformal => true,
        :gl => true,
        :devicePixelRatio => window.devicePixelRatio
      )
      view = CGA3.graph(scene, viewOpt)
      view.setAttribute(:id, "view")
      view.removeAttribute(:style)
      document.body.replaceChildren(view)
      
      # create control panel
      controlPanel = document.createElement(:div)
      controlPanel.setAttribute(:id, "control-panel")
      document.body.appendChild(controlPanel)
    end
    
    new(win)
  end
end

mprod(v, w) =
  v[1]*w[1] + v[2]*w[2] + v[3]*w[3] + v[4]*w[4] - v[5]*w[5]

function display!(viewer::ConstructionViewer, elements::Matrix)
  # load elements
  elements_full = []
  for elt in eachcol(Engine.unmix * elements)
    if mprod(elt, elt) < 0.5
      elt_full = [0; elt; fill(0, 26)]
    else
      # `elt` is a spacelike vector, representing a generalized sphere, so we
      # take its Hodge dual before passing it to Ganja.js. the dual represents
      # the same generalized sphere, but Ganja.js only displays planes when
      # they're represented by vectors in grade 4 rather than grade 1
      elt_full = [fill(0, 26); -elt[5]; -elt[4]; elt[3]; -elt[2]; elt[1]; 0]
    end
    push!(elements_full, elt_full)
  end
  @js viewer.win elements = $elements_full.map((elt) -> @new CGA3(elt))
  
  # generate palette. this is Gadfly's `default_discrete_colors` palette,
  # available under the MIT license
  palette = distinguishable_colors(
    length(elements_full),
    [LCHab(70, 60, 240)],
    transform = c -> deuteranopic(c, 0.5),
    lchoices = Float64[65, 70, 75, 80],
    cchoices = Float64[0, 50, 60, 70],
    hchoices = range(0, stop=330, length=24)
  )
  palette_packed = [RGB24(c).color for c in palette]
  @js viewer.win palette = $palette_packed
  
  # create visibility toggles
  @js viewer.win begin
    controlPanel.replaceChildren()
    visToggles = []
  end
  for (elt, c) in zip(eachcol(elements), palette)
    vec_str = join(map(t -> @sprintf("%.3f", t), elt), ", ")
    color_str = "#$(hex(c))"
    style_str = "background-color: $color_str; border-color: $color_str;"
    @js viewer.win begin
      @var toggle = document.createElement(:div)
      toggle.setAttribute(:style, $style_str)
      toggle.checked = true
      toggle.addEventListener(
        "click",
        () -> begin
          toggle.checked = !toggle.checked
          toggle.style.backgroundColor = toggle.checked ? $color_str : "inherit";
          updateView()
        end
      )
      toggle.appendChild(document.createTextNode($vec_str))
      visToggles.push(toggle);
      controlPanel.appendChild(toggle);
    end
  end
  
  # update view
  @js viewer.win updateView()
end

function opentools!(viewer::ConstructionViewer)
  size(viewer.win, 1240, 830)
  opentools(viewer.win)
end

function closetools!(viewer::ConstructionViewer)
  closetools(viewer.win)
  size(viewer.win, 620, 830)
end

end

# ~~~ sandbox setup ~~~

elements = let
  a = sqrt(BigFloat(3)/2)
  sqrt(0.5) * BigFloat[
    1    1   -1   -1    0
    1   -1    1   -1    0
    1   -1   -1    1    0
    0.5  0.5  0.5  0.5  1+a
    0.5  0.5  0.5  0.5  1-a
  ]
end

# show construction
viewer = Viewer.ConstructionViewer()
Viewer.display!(viewer, elements)