{-|
Module      : FRP.Netwire.Input.GLFW
Description : netwire-input instances for use with GLFW
Copyright   : (c) Pavel Krajcevski, 2017
License     : MIT
Maintainer  : Krajcevski@gmail.com
Stability   : experimental
Portability : POSIX

This module contains data types with instances  needed to create wires
that can be used with the netwire-input combinators. In particular, this
package implements 'GLFWInputT' which has instances of 'MonadKeyboard' and
'MonadMouse'

-}

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE UndecidableInstances #-}

module FRP.Netwire.Input.GLFW (
  -- * GLFW Input

  -- ** Basic Input Monad
  GLFWInput, runGLFWInput,

  -- ** Monad Transformer
  GLFWInputT, runGLFWInputT,

  -- * Typeclass
  MonadGLFWInput(..),

  -- * State Types
  GLFWInputControl, GLFWInputState,
  getInput, mkInputControl, pollGLFW
) where

--------------------------------------------------------------------------------
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Graphics.UI.GLFW as GLFW
import Control.Applicative
import Control.Concurrent.STM
import Control.Monad.RWS
import Control.Monad.State
import Control.Monad.Except
import Control.Monad.Cont
import Control.Monad.Identity
import GHC.Float hiding (clamp)

import FRP.Netwire.Input
--------------------------------------------------------------------------------

clamp :: Ord a => a -> a -> a -> a
clamp x a b
  | x < a = a
  | x > b = b
  | otherwise = x

newRange :: Floating a => a -> (a, a) -> (a, a) -> a
newRange x (omin, omax) (nmin, nmax) =
  nmin + (nmax - nmin) * ((x - omin) / (omax - omin))

newRangeC :: (Ord a, Floating a) => a -> (a, a) -> (a, a) -> a
newRangeC x o n@(nmin, nmax) = clamp (newRange x o n) nmin nmax

modeToGLFWMode :: CursorMode -> GLFW.CursorInputMode
modeToGLFWMode CursorMode'Reset = GLFW.CursorInputMode'Disabled
modeToGLFWMode CursorMode'Disabled = GLFW.CursorInputMode'Disabled
modeToGLFWMode CursorMode'Hidden = GLFW.CursorInputMode'Hidden
modeToGLFWMode CursorMode'Enabled = GLFW.CursorInputMode'Normal

-- | The GLFW input state is a record that keeps track of which buttons and keys
-- are currently pressed. Because GLFW works with callbacks, a call to pollEvents
-- must be made in order to process any of the events. At this time, all of the
-- appropriate callbacks are fired in order of the events received, and this record
-- is updated to reflect the most recent input state.
data GLFWInputState = GLFWInputState {
  keysPressed :: Map.Map GLFW.Key Int,
  keysReleased :: Set.Set GLFW.Key,
  mbPressed :: Map.Map GLFW.MouseButton Int,
  mbReleased :: Set.Set GLFW.MouseButton,
  cursorPos :: (Float, Float),
  cmode :: CursorMode,
  scrollAmt :: (Double, Double)
} deriving(Show)

instance Key GLFW.Key
instance MouseButton GLFW.MouseButton

-- | The 'GLFWInputT' monad transformer is simply a state monad transformer using
-- 'GLFWInputState'
newtype GLFWInputT m a =
  GLFWInputT (StateT GLFWInputState m a)
  deriving ( Functor
           , Applicative
           , Alternative
           , Monad
           , MonadFix
           , MonadIO
           , MonadWriter w
           , MonadReader r
           , MonadError e
           , MonadPlus
           , MonadCont
           , MonadTrans
           )

instance MonadState s m => MonadState s (GLFWInputT m) where
  get = lift get
  put = lift . put
  state = lift . state

-- | Describes a monad that provides stateful access to a 'GLFWInputState'. By
-- being able to modify the state, the context that satisfies this typeclass
-- can decide to debounce or "take ownership" of the button presses at a
-- specific point of the computation. This should be done via the 'MonadKey' and
-- 'MonadMouse' instances.
class Monad m => MonadGLFWInput m where
  -- | Retrieves the current input state
  getGLFWInput :: m GLFWInputState
  -- | Places a modified input state back into the context. This should probably
  -- not be called directly.
  putGLFWInput :: GLFWInputState -> m ()

instance Monad m => MonadGLFWInput (GLFWInputT m) where
  getGLFWInput :: GLFWInputT m GLFWInputState
  getGLFWInput = GLFWInputT get

  putGLFWInput :: GLFWInputState -> GLFWInputT m ()
  putGLFWInput = GLFWInputT . put

-- | To execute a computation with the current input snapshot, we need to give
-- supply the current 'GLFWInputState'. This comes from the 'GLFWInputControl'
-- associated with the given window.
runGLFWInputT :: GLFWInputT m a -> GLFWInputState -> m (a, GLFWInputState)
runGLFWInputT (GLFWInputT m) = runStateT m

-- | The 'GLFWInput' monad is simply the 'GLFWInputT' transformer around the
-- identity monad.
type GLFWInput = GLFWInputT Identity

-- | Runs the 'GLFWInput' computation with a current input snapshot and returns
-- the potentially modified input.
runGLFWInput :: GLFWInput a -> GLFWInputState -> (a, GLFWInputState)
runGLFWInput m = runIdentity . runGLFWInputT m

instance MonadGLFWInput m => MonadKeyboard GLFW.Key m where
  keyIsPressed :: GLFW.Key -> m Bool
  keyIsPressed key = liftM (isKeyDown key) getGLFWInput

  releaseKey :: GLFW.Key -> m ()
  releaseKey key = getGLFWInput >>= (putGLFWInput . debounceKey key)

instance MonadGLFWInput m => MonadMouse GLFW.MouseButton m where
  mbIsPressed :: GLFW.MouseButton -> m Bool
  mbIsPressed mb = liftM (isButtonPressed mb) getGLFWInput

  releaseButton :: GLFW.MouseButton -> m ()
  releaseButton mb = getGLFWInput >>= (putGLFWInput . debounceButton mb)

  cursor :: m (Float, Float)
  cursor = liftM cursorPos getGLFWInput

  setCursorMode :: CursorMode -> m ()
  setCursorMode mode = do
    ipt <- getGLFWInput
    putGLFWInput (ipt { cmode = mode })

  scroll :: m (Double, Double)
  scroll = liftM scrollAmt getGLFWInput

kEmptyInput :: GLFWInputState
kEmptyInput = GLFWInputState { keysPressed = Map.empty,
                               keysReleased = Set.empty,
                               mbPressed = Map.empty,
                               mbReleased = Set.empty,
                               cursorPos = (0, 0),
                               cmode = CursorMode'Enabled,
                               scrollAmt = (0, 0) }

isKeyDown :: GLFW.Key -> GLFWInputState -> Bool
isKeyDown key = (Map.member key) . keysPressed

withPressedKey :: GLFWInputState -> GLFW.Key -> (a -> a) -> a -> a
withPressedKey input key fn
  | isKeyDown key input = fn
  | otherwise = id

debounceKey :: GLFW.Key -> GLFWInputState -> GLFWInputState
debounceKey key input = input { keysPressed = Map.delete key (keysPressed input) }

isButtonPressed :: GLFW.MouseButton -> GLFWInputState -> Bool
isButtonPressed mb = Map.member mb . mbPressed

withPressedButton :: GLFWInputState -> GLFW.MouseButton -> (a -> a) -> a -> a
withPressedButton input mb fn = if isButtonPressed mb input then fn else id

debounceButton :: GLFW.MouseButton -> GLFWInputState -> GLFWInputState
debounceButton mb input = input { mbPressed = Map.delete mb (mbPressed input) }

-- | This is an 'STM' variable that holds the current input state. It cannot be
-- manipulated directly, but it is updated by GLFW each time 'pollGLFW' is called.
data GLFWInputControl = IptCtl (TVar GLFWInputState) GLFW.Window

setCursorToWindowCenter :: GLFW.Window -> IO ()
setCursorToWindowCenter win = do
  (w, h) <- GLFW.getWindowSize win
  GLFW.setCursorPos win (fromIntegral w / 2.0) (fromIntegral h / 2.0)

-- | Returns a current snapshot of the input
getInput :: GLFWInputControl -> IO GLFWInputState
getInput (IptCtl var _) = readTVarIO var

setInput :: GLFWInputControl -> GLFWInputState -> IO ()
setInput (IptCtl var win) ipt = do

  -- Do we need to change the cursor mode?
  curMode <- GLFW.getCursorInputMode win
  let newMode = modeToGLFWMode (cmode ipt)
  unless (newMode == curMode) $
    GLFW.setCursorInputMode win newMode

  -- Write the new input
  atomically $ writeTVar var (ipt { scrollAmt = (0, 0) })

resetCursorPos :: GLFWInputState -> GLFWInputState
resetCursorPos input = input { cursorPos = (0, 0) }

resolveReleased :: GLFWInputState -> GLFWInputState
resolveReleased input = input {
  keysPressed = Map.map (+1) $
                foldl (flip Map.delete) (keysPressed input) (Set.elems $ keysReleased input),
  keysReleased = Set.empty,
  mbPressed = Map.map (+1) $
              foldl (flip Map.delete) (mbPressed input) (Set.elems $ mbReleased input),
  mbReleased = Set.empty
  }

--------------------------

scrollCallback :: GLFWInputControl -> GLFW.Window -> Double -> Double -> IO ()
scrollCallback (IptCtl ctl _) _ xoff yoff =
  atomically $ modifyTVar' ctl updateScroll
  where
    updateScroll :: GLFWInputState -> GLFWInputState
    updateScroll = (\input -> input { scrollAmt = (xoff, yoff) })

keyCallback :: GLFWInputControl -> GLFW.Window ->
               GLFW.Key -> Int -> GLFW.KeyState -> GLFW.ModifierKeys -> IO ()
keyCallback (IptCtl ctl _) _ key _ keystate _ =
  atomically $ modifyTVar' ctl modifyKeys
  where
    modifyKeys :: GLFWInputState -> GLFWInputState
    modifyKeys input = case keystate of
      GLFW.KeyState'Pressed -> input {
        keysPressed = Map.union (keysPressed input) (Map.singleton key 0) }
      GLFW.KeyState'Released -> input {
        keysPressed = Map.update removeReleased key (keysPressed input),
        keysReleased =
          case Map.lookup key (keysPressed input) of
            -- If the key was just added... queue it up
            Just 0 -> Set.insert key (keysReleased input)
            -- If the key isn't pressed then it must have been debounced... do nothing
            -- If the key wasn't just added we're removing it above... do nothing...
            _ -> keysReleased input
        }
      _ -> input

    removeReleased :: Int -> Maybe Int
    removeReleased 0 = Just 0
    removeReleased _ = Nothing

mouseButtonCallback :: GLFWInputControl -> GLFW.Window ->
                       GLFW.MouseButton -> GLFW.MouseButtonState ->
                       GLFW.ModifierKeys -> IO ()
mouseButtonCallback (IptCtl ctl _) _ button state _ =
  atomically $ modifyTVar' ctl modify
  where
    modify :: GLFWInputState -> GLFWInputState
    modify input = case state of
      GLFW.MouseButtonState'Pressed -> input {
        mbPressed = Map.union (mbPressed input) (Map.singleton button 0) }
      GLFW.MouseButtonState'Released -> input {
        mbPressed = Map.update removeReleased button (mbPressed input),
        mbReleased =
          case Map.lookup button (mbPressed input) of
            Just 0 -> Set.insert button (mbReleased input)
            _ -> mbReleased input
        }

    removeReleased :: Int -> Maybe Int
    removeReleased 0 = Just 0
    removeReleased _ = Nothing

cursorPosCallback :: GLFWInputControl -> GLFW.Window -> Double -> Double -> IO ()
cursorPosCallback (IptCtl ctl _) win x y = do
  (w, h) <- GLFW.getWindowSize win
  let xf = newRangeC (double2Float x) (0, fromIntegral w) (-1, 1)
      yf = newRangeC (double2Float y) (0, fromIntegral h) (-1, 1)
  atomically $ modifyTVar' ctl (\ipt -> ipt { cursorPos = (xf, yf)})

-- | Creates and returns an 'STM' variable for the window that holds all of the
-- most recent input state information
mkInputControl :: GLFW.Window -> IO GLFWInputControl
mkInputControl win = do
  ctlvar <- newTVarIO kEmptyInput
  let ctl = IptCtl ctlvar win
  GLFW.setScrollCallback win (Just $ scrollCallback ctl)
  GLFW.setKeyCallback win (Just $ keyCallback ctl)
  GLFW.setCursorPosCallback win (Just $ cursorPosCallback ctl)
  GLFW.setMouseButtonCallback win (Just $ mouseButtonCallback ctl)
  return ctl

-- | Allows GLFW to interact with the windowing system to update the current
-- state. The old state must be passed in order to properly reset certain
-- properties such as the scroll wheel. The returned input state is identical
-- to a subsequent call to 'getInput' right after a call to 'GLFW.pollEvents'
pollGLFW :: GLFWInputState -> GLFWInputControl -> IO GLFWInputState
pollGLFW ipt iptctl@(IptCtl _ win) = do
  let ipt' = resolveReleased ipt

  -- Do we need to reset the cursor?
  if cmode ipt' == CursorMode'Reset
    then do
    setCursorToWindowCenter win
    setInput iptctl (resetCursorPos ipt')
    else setInput iptctl ipt'

  GLFW.pollEvents
  getInput iptctl