-- | 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 Data.Generic.Rep (class Generic)
import Data.Eq.Generic (genericEq)
import Data.Foldable (maximum)
import Data.Int (fromNumber)
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Number as DN
import Data.Number.Format as DNF
import Data.Nullable (Nullable, null)
import Data.Traversable (traverse_)
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 Effect (Effect)
import Reactix as R
import Reactix.DOM.HTML as H
import Toestand as T

import Gargantext.Prelude

import Gargantext.Components.Bootstrap.Types (ComponentStatus(..))
import Gargantext.Utils.Math (roundToMultiple)
import Gargantext.Utils.Range as Range
import Gargantext.Utils.Reactix as R2

here :: R2.Here
here = R2.here "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
  , status       :: ComponentStatus )

data Knob = MinKnob | MaxKnob
derive instance Generic Knob _
instance Eq Knob where
  eq = genericEq

data RangeUpdate = SetMin Number | SetMax Number

rangeSlider :: Record Props -> R.Element
rangeSlider props = R.createElement rangeSliderCpt props []
rangeSliderCpt :: R.Component Props
rangeSliderCpt = here.component "rangeSlider" cpt where
  cpt props _ = do
    -- rounding precision (i.e. how many decimal digits are in epsilon)
    let (Range.Closed { min: minR, max: maxR }) = props.initialValue
    let decPrecision num =
          -- int digits
          (fromMaybe 0 $ fromNumber $ DN.ceil $ (DN.log num) / DN.ln10)
          -- float digits
          + (fromMaybe 0 $ fromNumber $ DN.ceil $ -(DN.log (num - (DN.floor num))) / DN.ln10)
    let epsilonPrecision = decPrecision props.epsilon
    let minPrecision = decPrecision minR
    let maxPrecision = decPrecision maxR
    --let precision = fromMaybe 0 $ fromNumber $ max 0.0 epsilonPrecision
    let precision = fromMaybe 0 $ maximum [0, epsilonPrecision, minPrecision, maxPrecision]

    -- 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 <- T.useBox $ initialValue props
    value' <- T.useLive T.unequal value

    -- the knob we are currently in a drag for. set by mousedown on a knob
    dragKnob <- T.useBox (Nothing :: Maybe Knob)
    dragKnob' <- T.useLive T.unequal dragKnob

    -- 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 value 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
                T.write_ Nothing dragKnob
                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 dragKnob precision props.status
      , renderKnob MaxKnob highElem value' props.bounds dragKnob precision props.status
      ]
  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 -> T.Box Range.NumberRange -> Range.NumberRange -> Number -> Effect Unit
setKnob knob value r val = T.write_ (knobSetter knob r val) value

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 = "range-slider__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 = "range-slider__scale-sel"
    style = {left: computeLeft, width: computeWidth}
    percOffsetMin = Range.normalise props.bounds min
    percOffsetMax = Range.normalise props.bounds max
    computeLeft = formatter $ 100.0 * percOffsetMin
    computeWidth = formatter $ 100.0 * (percOffsetMax - percOffsetMin)
    formatter n = (DNF.toStringWith (DNF.fixed 0) n) <> "%"


renderKnob :: Knob -> R.Ref (Nullable DOM.Element) -> Range.NumberRange -> Bounds -> T.Box (Maybe Knob) -> Int -> ComponentStatus -> R.Element
renderKnob knob ref (Range.Closed value) bounds set precision status =
  H.div { ref, tabIndex, className, aria, on: { mouseDown: onMouseDown }, style } [
      H.div { className: "range-slider__placeholder " }
        [
          H.text $ DNF.toStringWith (DNF.precision precision) val
        ]
  ]
  where
    tabIndex = 0
    className = "range-slider__knob " <> (show status)
    aria = { label: labelPrefix knob <> "value: " <> show val }
    labelPrefix :: Knob -> String
    labelPrefix MinKnob = "Minimum "
    labelPrefix MaxKnob = "Maximum "
    onMouseDown _ = case status of
      Disabled -> pure unit
      _ -> T.write_ (Just knob) set
    percOffset = Range.normalise bounds val
    style = { left: (show $ 100.0 * percOffset) <> "%" }
    val :: Number
    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