-- | The RangeSlider is a slider component with two knobs, allowing
-- | the user to specify both a minimum and maximum value to filter
-- | data by. It may be dragged with the mouse or moved with the
-- | keyboard like a regular slider component.  The RangeSlider is
-- | designed to let the user adjust in multiples of a provided
-- | epsilon (smallest difference)
module Gargantext.Components.RangeSlider where

import Prelude
import Data.Int (fromNumber)
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Nullable (Nullable, null)
import Data.Traversable (traverse_)
import Data.Tuple.Nested ((/\))
import DOM.Simple as DOM
import DOM.Simple.Document (document)
import DOM.Simple.Event as Event
import DOM.Simple.EventListener as EL
import DOM.Simple (DOMRect)
import Global (toFixed)
import Effect (Effect)
import Effect.Uncurried (mkEffectFn1)
import Math as M
import Reactix as R
import Reactix.DOM.HTML as H

import Gargantext.Utils.Math (roundToMultiple)
import Gargantext.Utils.Range as Range
import Gargantext.Utils.Reactix as R2

thisModule = "Gargantext.Components.RangeSlider"
-- data Axis = X | Y

type Bounds = Range.NumberRange
type Epsilon = Number

-- To avoid overloading the terms 'min' and 'max' here, we treat 'min'
-- and 'max' as being the bounds of the scale and 'low' and 'high' as
-- being the selected values
type Props =
  ( bounds :: Bounds                  -- The minimum and maximum values it is possible to select
  , initialValue :: Range.NumberRange -- The user's selection of minimum and maximum values
  , epsilon :: Number                 -- The smallest possible change (for mouse)
  , step :: Number                    -- The 'standard' change (for keyboard)
  -- , axis :: Axis                   -- Which direction to move in
  , width :: Number
  , height :: Number
  , onChange :: Range.NumberRange -> Effect Unit )

rangeSlider :: Record Props -> R.Element
rangeSlider props = R.createElement rangeSliderCpt props []

data Knob = MinKnob | MaxKnob

data RangeUpdate = SetMin Number | SetMax Number

rangeSliderCpt :: R.Component Props
rangeSliderCpt = R2.hooksComponent thisModule "rangeSlider" cpt
  where
    cpt props _ = do
      -- rounding precision (i.e. how many decimal digits are in epsilon)
      let precision = fromMaybe 0 $ fromNumber $ max 0.0 $ - M.floor $ (M.log props.epsilon) / M.ln10

      -- scale bar
      scaleElem <- (R.useRef null) :: R.Hooks (R.Ref (Nullable DOM.Element)) -- dom ref
      -- scale sel bar
      scaleSelElem <- (R.useRef null) :: R.Hooks (R.Ref (Nullable DOM.Element)) -- dom ref
      -- low knob
      lowElem <- (R.useRef null) :: R.Hooks (R.Ref (Nullable DOM.Element)) -- a dom ref to the low knob
      -- high knob
      highElem <- (R.useRef null) :: R.Hooks (R.Ref (Nullable DOM.Element)) -- a dom ref to the high knob
      -- The value of the user's selection
      value /\ setValue <- R.useState' $ initialValue props

      -- the knob we are currently in a drag for. set by mousedown on a knob
      dragKnob /\ setDragKnob <- R.useState' $ (Nothing :: Maybe Knob)

      -- the handler functions for trapping mouse events, so they can be removed
      mouseMoveHandler <- (R.useRef $ Nothing) :: R.Hooks (R.Ref (Maybe (EL.Callback Event.MouseEvent)))
      mouseUpHandler <- (R.useRef $ Nothing) :: R.Hooks (R.Ref (Maybe (EL.Callback Event.MouseEvent)))
      let destroy = \_ -> do
            destroyEventHandler "mousemove" mouseMoveHandler
            destroyEventHandler "mouseup" mouseUpHandler
            R.setRef mouseMoveHandler $ Nothing
            R.setRef mouseUpHandler $ Nothing

      R2.useLayoutEffect1' dragKnob $ \_ -> do
        let scalePos = R2.readPositionRef scaleElem
        let lowPos = R2.readPositionRef lowElem
        let highPos = R2.readPositionRef highElem

        case dragKnob of
          Just knob -> do
            let drag = (getDragScale knob scalePos lowPos highPos) :: Maybe Range.NumberRange

            let onMouseMove = EL.callback $ \(event :: Event.MouseEvent) -> do
                  case reproject drag scalePos props.bounds props.epsilon (R2.domMousePosition event) of
                    Just val -> do
                      setKnob knob setValue value val
                      props.onChange $ knobSetter knob value val
                    Nothing -> destroy unit
            let onMouseUp = EL.callback $ \(_event :: Event.MouseEvent) -> do
                  --props.onChange $ knobSetter knob value val
                  setDragKnob $ const Nothing
                  destroy unit
            EL.addEventListener document "mousemove" onMouseMove
            EL.addEventListener document "mouseup" onMouseUp
            R.setRef mouseMoveHandler $ Just onMouseMove
            R.setRef mouseUpHandler $ Just onMouseUp
          Nothing -> destroy unit
      pure $ H.div { className, aria }
        [ renderScale scaleElem props value
        , renderScaleSel scaleSelElem props value
        , renderKnob MinKnob lowElem  value props.bounds setDragKnob precision
        , renderKnob MaxKnob highElem value props.bounds setDragKnob precision
        ]
    className = "range-slider"
    aria = { label: "Range Slider Control. Expresses filtering data by a minimum and maximum value range through two slider knobs. Knobs can be adjusted with the arrow keys." }

destroyEventHandler
  :: forall e
  .  Event.IsEvent e
  => String -> R.Ref (Maybe (EL.Callback e)) -> Effect Unit
destroyEventHandler name ref = traverse_ destroy $ R.readRef ref
  where
    destroy handler = do
      EL.removeEventListener document name handler
      R.setRef ref Nothing

setKnob :: Knob -> R2.Setter Range.NumberRange -> Range.NumberRange -> Number -> Effect Unit
setKnob knob setValue r val = setValue $ const $ knobSetter knob r val

knobSetter :: Knob -> Range.NumberRange -> Number -> Range.NumberRange
knobSetter MinKnob = Range.withMin
knobSetter MaxKnob = Range.withMax

getDragScale :: Knob -> Maybe DOMRect -> Maybe DOMRect -> Maybe DOMRect -> Maybe Range.NumberRange
getDragScale knob scalePos lowPos highPos = do
  scale <- scalePos
  low <- lowPos
  high <- highPos
  pure $ Range.Closed { min: min knob scale low, max: max knob scale high }
  where
    min MinKnob scale _ = scale.left
    min MaxKnob _ low = low.left
    max MinKnob _ high = high.left
    max MaxKnob scale _ = scale.right

renderScale :: R.Ref (Nullable DOM.Element) -> Record Props -> Range.NumberRange -> R.Element
renderScale ref {width,height} (Range.Closed {min, max}) =
   H.div { ref, className, width, height, aria } []
  where
    className = "scale"
    aria = { label: "Scale running from " <> show min <> " to " <> show max }

renderScaleSel :: R.Ref (Nullable DOM.Element) -> Record Props -> Range.NumberRange -> R.Element
renderScaleSel ref props (Range.Closed {min, max}) =
    H.div { ref, className, style} []
  where
    className = "scale-sel"
    style = {left: computeLeft, width: computeWidth}
    percOffsetMin = Range.normalise props.bounds min
    percOffsetMax = Range.normalise props.bounds max
    computeLeft = (show $ 100.0 * percOffsetMin) <> "%"
    computeWidth = (show $ 100.0 * (percOffsetMax - percOffsetMin)) <> "%"


renderKnob :: Knob -> R.Ref (Nullable DOM.Element) -> Range.NumberRange -> Bounds -> R2.Setter (Maybe Knob) -> Int -> R.Element
renderKnob knob ref (Range.Closed value) bounds set precision =
  H.div { ref, tabIndex, className, aria, onMouseDown, style } [
      H.div { className: "button" }
        [
          H.text $ text $ toFixed precision val
        ]
  ]
  where
    text (Just num) = num
    text Nothing = "error"
    tabIndex = 0
    className = "knob"
    aria = { label: labelPrefix knob <> "value: " <> show val }
    labelPrefix MinKnob = "Minimum "
    labelPrefix MaxKnob = "Maximum "
    onMouseDown = mkEffectFn1 $ \_ -> set $ const $ Just knob
    percOffset = Range.normalise bounds val
    style = { left: (show $ 100.0 * percOffset) <> "%" }
    val = case knob of
      MinKnob -> value.min
      MaxKnob -> value.max

-- TODO round to nearest epsilon
reproject :: Maybe Range.NumberRange -> Maybe DOMRect -> Bounds -> Epsilon -> R2.Point -> Maybe Number
reproject drag scalePos bounds epsilon (R2.Point mousePos) = do
  drag_ <- drag
  scale_ <- rectRange <$> scalePos
  let normal = Range.normalise scale_ (Range.clamp drag_ mousePos.x)
  let val = Range.projectNormal bounds normal
  pure $ round epsilon bounds val

rectRange :: DOMRect -> Range.NumberRange
rectRange rect = Range.Closed { min, max }
  where min = rect.left
        max = rect.right

initialValue :: Record Props -> Range.NumberRange
initialValue props = roundRange props.epsilon props.bounds props.initialValue

round :: Epsilon -> Bounds -> Number -> Number
round epsilon bounds = roundToMultiple epsilon <<< Range.clamp bounds

roundRange :: Epsilon -> Bounds -> Range.NumberRange -> Range.NumberRange
roundRange epsilon bounds (Range.Closed initial) = Range.Closed { min, max }
  where min = round epsilon bounds initial.min
        max = round epsilon bounds initial.max