packages feed

brillo-2.0.0: Brillo/Internals/Interface/Backend/GLFW.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# OPTIONS_HADDOCK hide #-}

-- | Support for using GLFW as the window manager backend.
module Brillo.Internals.Interface.Backend.GLFW (GLFWState)
where

import Control.Concurrent (threadDelay)
import Control.Exception qualified as X
import Control.Monad (unless, when)
import Data.Functor ((<&>))
import Data.IORef (IORef, modifyIORef', readIORef, writeIORef)
import Data.Text qualified as T
import GHC.Desugar ((>>>))
import Graphics.Rendering.OpenGL (($=))
import Graphics.Rendering.OpenGL qualified as GL
import Graphics.UI.GLFW qualified as GLFW

import Brillo.Data.Cursor (CursorShape (..))
import Brillo.Data.FileDialog (FileDialog (..), SelectionMode (..))
import Brillo.Internals.Interface.Backend.Types (
  Backend (..),
  Callback (..),
  Display (..),
  Key (..),
  KeyState (..),
  Modifiers (..),
  MouseButton (..),
  SpecialKey (..),
 )
import Brillo.Internals.TinyFileDialogs as TinyFileDialogs
import Data.List (singleton)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map


-- | State of the GLFW backend library.
data GLFWState
  = GLFWState
  { modifiers :: Modifiers
  -- ^ Status of Ctrl, Alt or Shift (Up or Down?)
  , mousePosition :: (Int, Int)
  -- ^ Latest mouse position
  , mouseWheelPos :: Int
  -- ^ Latest mousewheel position
  , dirtyScreen :: Bool
  -- ^ Does the screen need to be redrawn?
  , display :: IO ()
  -- ^ Action that draws on the screen
  , idle :: IO ()
  -- ^ Action perforrmed when idling
  , optWinHdl :: Maybe GLFW.Window
  -- ^ The Window Handle
  , cursorCache :: Map CursorShape GLFW.Cursor
  -- ^ Cache of created cursors
  }


-- | Initial GLFW state.
glfwStateInit :: GLFWState
glfwStateInit =
  GLFWState
    { modifiers = Modifiers Up Up Up
    , mousePosition = (0, 0)
    , mouseWheelPos = 0
    , dirtyScreen = True
    , display = return ()
    , idle = return ()
    , optWinHdl = Nothing
    , cursorCache = Map.empty
    }


-- | Fetch the window handle from the state if it has been initialized.
winHdl :: GLFWState -> GLFW.Window
winHdl state =
  case optWinHdl state of
    Just handle -> handle
    Nothing -> error "GLFW backend: requested uninitialized window handle"


instance Backend GLFWState where
  initBackendState = glfwStateInit
  initializeBackend = initializeGLFW
  exitBackend = exitGLFW
  openWindow = openWindowGLFW
  dumpBackendState = dumpStateGLFW
  installDisplayCallback = installDisplayCallbackGLFW
  installWindowCloseCallback = installWindowCloseCallbackGLFW
  installReshapeCallback = installReshapeCallbackGLFW
  installRefreshCallback = installRefreshCallbackGLFW
  installKeyMouseCallback = installKeyMouseCallbackGLFW
  installMotionCallback = installMotionCallbackGLFW
  installDropCallback = installDropCallbackGLFW
  installIdleCallback = installIdleCallbackGLFW
  runMainLoop = runMainLoopGLFW
  postRedisplay = postRedisplayGLFW
  getWindowDimensions ref = windowHandle ref >>= \win -> GLFW.getWindowSize win
  getScreenSize = getScreenSizeGLFW
  openFileDialog = openFileDialogGLFW
  elapsedTime _ =
    GLFW.getTime >>= \mt -> case mt of
      Nothing -> error "GLFW.getTime returned Nothing. Is GLFW initialized?"
      Just t -> return t
  sleep _ sec = threadDelay (floor (sec * 1000000.0)) -- GLFW.sleep sec)
  setCursor = setCursorGLFW


-- Initialise -----------------------------------------------------------------

-- | Initialise the GLFW backend.
initializeGLFW :: IORef GLFWState -> Bool -> IO ()
initializeGLFW _ debug =
  do
    let simpleErrorCallback e s =
          putStrLn $ unwords ["GLFW backend: ", show e, show s]
    GLFW.setErrorCallback (Just simpleErrorCallback)

    _ <- GLFW.init
    glfwVersion <- GLFW.getVersion

    when debug $
      putStrLn $
        "  glfwVersion        = " ++ show glfwVersion


-- Exit -----------------------------------------------------------------------

-- | Tell the GLFW backend to close the window and exit.
exitGLFW :: IORef GLFWState -> IO ()
exitGLFW ref = do
  win <- windowHandle ref
  GLFW.setWindowShouldClose win True


-- Cursor ---------------------------------------------------------------------

-- | Set the cursor shape for the GLFW window.
setCursorGLFW :: IORef GLFWState -> CursorShape -> IO ()
setCursorGLFW stateRef shape = do
  state <- readIORef stateRef
  win <- windowHandle stateRef

  case shape of
    CursorHidden -> do
      -- Hide cursor using GLFW's cursor input mode
      GLFW.setCursorInputMode win GLFW.CursorInputMode'Hidden
    _ -> do
      -- Ensure cursor is visible
      GLFW.setCursorInputMode win GLFW.CursorInputMode'Normal

      -- Check if cursor is already cached
      case Map.lookup shape (cursorCache state) of
        Just cursor -> do
          GLFW.setCursor win cursor
        Nothing -> do
          -- Create and cache the cursor
          let stdCursor = cursorShapeToGLFW shape
          cursor <- GLFW.createStandardCursor stdCursor
          -- Cache the cursor for future use
          modifyIORef' stateRef $ \s ->
            s{cursorCache = Map.insert shape cursor (cursorCache s)}
          GLFW.setCursor win cursor


-- | Convert Brillo CursorShape to GLFW StandardCursorShape.
cursorShapeToGLFW :: CursorShape -> GLFW.StandardCursorShape
cursorShapeToGLFW shape = case shape of
  CursorArrow -> GLFW.StandardCursorShape'Arrow
  CursorIBeam -> GLFW.StandardCursorShape'IBeam
  CursorCrosshair -> GLFW.StandardCursorShape'Crosshair
  CursorHand -> GLFW.StandardCursorShape'Hand
  CursorResizeH -> GLFW.StandardCursorShape'HResize
  CursorResizeV -> GLFW.StandardCursorShape'VResize
  CursorHidden -> GLFW.StandardCursorShape'Arrow -- Not used, handled separately


-- Open Window ----------------------------------------------------------------

-- | Open a new window.
openWindowGLFW ::
  IORef GLFWState ->
  Display ->
  IO ()
openWindowGLFW ref (InWindow title (sizeX, sizeY) pos) =
  do
    win <-
      GLFW.createWindow
        sizeX
        sizeY
        (T.unpack title)
        Nothing
        Nothing

    case win of
      Nothing ->
        error $
          "GLFW.createWindow failed to create a "
            ++ show sizeX
            ++ "x"
            ++ show sizeY
            ++ " window."
            ++ " Check that your GPU drivers support OpenGL."
      Just w -> do
        modifyIORef' ref (\s -> s{optWinHdl = win})
        uncurry (GLFW.setWindowPos w) pos
        GLFW.makeContextCurrent win

        -- Try to enable sync-to-vertical-refresh by setting the number
        -- of buffer swaps per vertical refresh to 1.
        GLFW.swapInterval 1
openWindowGLFW ref FullScreen =
  do
    mon <- GLFW.getPrimaryMonitor
    case mon of
      Nothing ->
        error "GLFW.getPrimaryMonitor returned Nothing. No monitor detected."
      Just m -> do
        vmode <- GLFW.getVideoMode m
        case vmode of
          Nothing ->
            error "GLFW.getVideoMode returned Nothing for primary monitor."
          Just vm -> do
            let sizeX = GLFW.videoModeWidth vm
            let sizeY = GLFW.videoModeHeight vm

            win <-
              GLFW.createWindow
                sizeX
                sizeY
                ""
                mon
                Nothing

            case win of
              Nothing ->
                error "GLFW.createWindow failed to create a fullscreen window."
              Just w -> do
                modifyIORef' ref (\s -> s{optWinHdl = win})
                GLFW.makeContextCurrent win

                -- Try to enable sync-to-vertical-refresh by setting the number
                -- of buffer swaps per vertical refresh to 1.
                GLFW.swapInterval 1
                GLFW.setCursorInputMode w GLFW.CursorInputMode'Normal


windowHandle :: IORef GLFWState -> IO GLFW.Window
windowHandle ref =
  do
    s <- readIORef ref
    return $ winHdl s


getScreenSizeGLFW :: IORef GLFWState -> IO (Int, Int)
getScreenSizeGLFW _state = do
  monitor <- GLFW.getPrimaryMonitor
  case monitor of
    Nothing ->
      error "GLFW.getPrimaryMonitor returned Nothing. No monitor detected."
    Just m -> do
      vmode <- GLFW.getVideoMode m
      case vmode of
        Nothing ->
          error "GLFW.getVideoMode returned Nothing for primary monitor."
        Just vm -> do
          let sizeX = GLFW.videoModeWidth vm
          let sizeY = GLFW.videoModeHeight vm
          pure (sizeX, sizeY)


-- | Open a file dialog. Return `Nothing` if the user cancels the dialog.
openFileDialogGLFW :: IORef GLFWState -> FileDialog -> IO (Maybe [FilePath])
openFileDialogGLFW _state fileDialog = do
  case fileDialog.selectionMode of
    SingleDirectorySelect -> do
      dirPathMb <-
        TinyFileDialogs.selectFolderDialog
          fileDialog.title
          fileDialog.defaultPath

      return (dirPathMb <&> T.unpack >>> singleton)
    _ -> do
      filePathsMb <-
        TinyFileDialogs.openFileDialog
          fileDialog.title
          fileDialog.defaultPath
          fileDialog.filterPatterns
          fileDialog.filterDescription
          (fileDialog.selectionMode == MultiFileSelect)

      return $ filePathsMb <&> (<&> T.unpack)


-- Dump State -----------------------------------------------------------------

-- | Print out the internal GLFW state.
dumpStateGLFW :: IORef GLFWState -> IO ()
dumpStateGLFW ref =
  do
    win <- windowHandle ref
    (ww, wh) <- GLFW.getWindowSize win

    -- GLFW-b does not provide a general function to query windowHints
    -- could be added by adding additional getWindowHint which
    -- uses glfwGetWindowAttrib behind the scenes as has been done
    -- already for e.g. getWindowVisible which uses glfwGetWindowAttrib
    {-
            r           <- GLFW.getWindowHint NumRedBits
            g           <- GLFW.getWindowHint NumGreenBits
            b           <- GLFW.getWindowHint NumBlueBits
            a           <- GLFW.getWindowHint NumAlphaBits
            let rgbaBD  = [r,g,b,a]

            depthBD     <- GLFW.getWindowHint NumDepthBits

            ra          <- GLFW.getWindowHint NumAccumRedBits
            ga          <- GLFW.getWindowHint NumAccumGreenBits
            ba          <- GLFW.getWindowHint NumAccumBlueBits
            aa          <- GLFW.getWindowHint NumAccumAlphaBits
            let accumBD = [ra,ga,ba,aa]

            stencilBD   <- GLFW.getWindowHint NumStencilBits

            auxBuffers  <- GLFW.getWindowHint NumAuxBuffers

            fsaaSamples <- GLFW.getWindowHint NumFsaaSamples

            putStr  $ "* dumpGlfwState\n"
                    ++ " windowWidth  = " ++ show ww          ++ "\n"
                    ++ " windowHeight = " ++ show wh          ++ "\n"
                    ++ " depth rgba   = " ++ show rgbaBD      ++ "\n"
                    ++ " depth        = " ++ show depthBD     ++ "\n"
                    ++ " accum        = " ++ show accumBD     ++ "\n"
                    ++ " stencil      = " ++ show stencilBD   ++ "\n"
                    ++ " aux Buffers  = " ++ show auxBuffers  ++ "\n"
                    ++ " FSAA Samples = " ++ show fsaaSamples ++ "\n"
                    ++ "\n"
    -}

    putStr $
      "* dumpGlfwState\n"
        ++ " windowWidth  = "
        ++ show ww
        ++ "\n"
        ++ " windowHeight = "
        ++ show wh
        ++ "\n"
        ++ "\n"


-- Display Callback -----------------------------------------------------------

-- | Callback for when GLFW needs us to redraw the contents of the window.
installDisplayCallbackGLFW ::
  IORef GLFWState -> [Callback] -> IO ()
installDisplayCallbackGLFW stateRef callbacks =
  modifyIORef' stateRef $ \s ->
    s
      { display = callbackDisplay stateRef callbacks
      }


callbackDisplay ::
  IORef GLFWState ->
  [Callback] ->
  IO ()
callbackDisplay stateRef callbacks = do
  -- clear the display
  GL.clear [GL.ColorBuffer, GL.DepthBuffer]
  GL.color $ GL.Color4 0 0 0 (1 :: GL.GLfloat)

  -- set the OpenGL viewport to account for any HiDPI discrepancy
  (width, height) <- windowHandle stateRef >>= GLFW.getFramebufferSize
  GL.viewport
    $= ( GL.Position 0 0
       , GL.Size (fromIntegral width) (fromIntegral height)
       )

  -- get the display callbacks from the chain
  let funs = [f stateRef | (Display f) <- callbacks]
  sequence_ funs


-- Close Callback -------------------------------------------------------------

{-| Callback for when the user closes the window.
  We can do some cleanup here.
-}
installWindowCloseCallbackGLFW ::
  IORef GLFWState -> IO ()
installWindowCloseCallbackGLFW ref =
  do
    win <- windowHandle ref
    GLFW.setWindowCloseCallback win (Just winClosed)
  where
    winClosed :: GLFW.WindowCloseCallback
    winClosed _win = do
      return ()


-- Reshape --------------------------------------------------------------------

-- | Callback for when the user reshapes the window.
installReshapeCallbackGLFW ::
  IORef GLFWState -> [Callback] -> IO ()
installReshapeCallbackGLFW stateRef callbacks =
  do
    win <- windowHandle stateRef
    GLFW.setWindowSizeCallback win (Just $ callbackReshape stateRef callbacks)


callbackReshape ::
  IORef GLFWState ->
  [Callback] ->
  GLFW.WindowSizeCallback -- = Window -> Int -> Int -> IO ()
callbackReshape stateRef callbacks _win sizeX sizeY =
  -- Call all reshape callbacks (which will update world state, viewport, etc.)
  -- Note: The actual redraw during live resize is handled by callbackRefresh,
  -- since on macOS the window size callback doesn't fire during drag.
  mapM_
    (\f -> f (sizeX, sizeY))
    ([f stateRef | Reshape f <- callbacks])


-- Refresh -----------------------------------------------------------------------

-- | Callback for when the window needs to be refreshed (e.g., during live resize on macOS).
installRefreshCallbackGLFW ::
  IORef GLFWState -> [Callback] -> IO ()
installRefreshCallbackGLFW stateRef callbacks = do
  win <- windowHandle stateRef
  GLFW.setWindowRefreshCallback win (Just $ callbackRefresh stateRef callbacks)


callbackRefresh ::
  IORef GLFWState ->
  [Callback] ->
  GLFW.WindowRefreshCallback -- = Window -> IO ()
callbackRefresh stateRef callbacks win = do
  -- Get the current window size and fire reshape callbacks
  -- This ensures EventResize is sent during live resize on macOS
  (sizeX, sizeY) <- GLFW.getWindowSize win
  mapM_
    (\f -> f (sizeX, sizeY))
    ([f stateRef | Reshape f <- callbacks])

  -- Run idle callbacks to advance simulations/animations
  callbackIdle stateRef callbacks

  -- Redraw and present the frame
  callbackDisplay stateRef callbacks
  GLFW.swapBuffers win


-- KeyMouse -----------------------------------------------------------------------

{-| Callbacks for when the user presses a key or moves / clicks the mouse.
  This is a bit verbose because we have to do impedence matching between
  GLFW's event system, and the one use by Brillo which was originally
  based on GLUT. The main problem is that GLUT only provides a single callback
  slot for character keys, arrow keys, mouse buttons and mouse wheel movement,
  while GLFW provides a single slot for each.
-}
installKeyMouseCallbackGLFW ::
  IORef GLFWState ->
  [Callback] ->
  IO ()
installKeyMouseCallbackGLFW stateRef callbacks =
  do
    win <- windowHandle stateRef
    GLFW.setKeyCallback win (Just $ callbackKeyboard stateRef callbacks)
    GLFW.setCharCallback win (Just $ callbackChar stateRef callbacks)
    GLFW.setMouseButtonCallback win (Just $ callbackMouseButton stateRef callbacks)
    GLFW.setScrollCallback win (Just $ callbackMouseWheel stateRef callbacks)
    GLFW.setDropCallback win (Just $ callbackDrop stateRef callbacks)


-- GLFW calls this on a non-character keyboard action.
callbackKeyboard ::
  IORef GLFWState ->
  [Callback] ->
  GLFW.KeyCallback -- = Window -> Key -> Int -> KeyState -> ModifierKeys -> IO ()
  -- -> GLFW.Key -> Bool
  -- -> IO ()
callbackKeyboard _stateRef _callbacks _win _key _scancode GLFW.KeyState'Repeating _modifiers =
  return ()
callbackKeyboard stateRef callbacks _win key _scancode keystateglfw _modifiers =
  do
    let keystate = keystateglfw == GLFW.KeyState'Pressed
    (modsSet, GLFWState mods pos _ _ _ _ _ _) <-
      setModifiers stateRef key keystate
    let key' = fromGLFW key
    let keystate' = if keystate then Down else Up
    let isCharKey (Char _) = True
        isCharKey _ = False

    -- Only process key presses for keys that won't generate character events
    -- or process any key release (since char callback only handles presses)
    let shouldProcess = not keystate || not (isCharacterKey key)

    -- Call the Brillo KeyMouse actions with the new state.
    unless (modsSet || isCharKey key' && keystate || not shouldProcess) $
      mapM_
        (\f -> f key' keystate' mods pos)
        ([f stateRef | KeyMouse f <- callbacks])


{-| Check if a GLFW key will generate a character callback event
These are keys that produce printable characters and will trigger both
key and character callbacks, causing duplicate events
-}
isCharacterKey :: GLFW.Key -> Bool
isCharacterKey key = case key of
  -- Alphabetic keys
  GLFW.Key'A -> True
  GLFW.Key'B -> True
  GLFW.Key'C -> True
  GLFW.Key'D -> True
  GLFW.Key'E -> True
  GLFW.Key'F -> True
  GLFW.Key'G -> True
  GLFW.Key'H -> True
  GLFW.Key'I -> True
  GLFW.Key'J -> True
  GLFW.Key'K -> True
  GLFW.Key'L -> True
  GLFW.Key'M -> True
  GLFW.Key'N -> True
  GLFW.Key'O -> True
  GLFW.Key'P -> True
  GLFW.Key'Q -> True
  GLFW.Key'R -> True
  GLFW.Key'S -> True
  GLFW.Key'T -> True
  GLFW.Key'U -> True
  GLFW.Key'V -> True
  GLFW.Key'W -> True
  GLFW.Key'X -> True
  GLFW.Key'Y -> True
  GLFW.Key'Z -> True
  -- Number keys
  GLFW.Key'0 -> True
  GLFW.Key'1 -> True
  GLFW.Key'2 -> True
  GLFW.Key'3 -> True
  GLFW.Key'4 -> True
  GLFW.Key'5 -> True
  GLFW.Key'6 -> True
  GLFW.Key'7 -> True
  GLFW.Key'8 -> True
  GLFW.Key'9 -> True
  -- Printable symbols
  GLFW.Key'Space -> True
  GLFW.Key'Apostrophe -> True
  GLFW.Key'Comma -> True
  GLFW.Key'Minus -> True
  GLFW.Key'Period -> True
  GLFW.Key'Slash -> True
  GLFW.Key'Semicolon -> True
  GLFW.Key'Equal -> True
  GLFW.Key'LeftBracket -> True
  GLFW.Key'Backslash -> True
  GLFW.Key'RightBracket -> True
  GLFW.Key'GraveAccent -> True
  -- All other keys (function keys, arrows, etc.) don't generate char events
  _ -> False


setModifiers ::
  IORef GLFWState ->
  GLFW.Key ->
  Bool ->
  IO (Bool, GLFWState)
setModifiers stateRef key pressed =
  do
    glfwState <- readIORef stateRef
    let mods = modifiers glfwState
    let mods' = case key of
          GLFW.Key'LeftShift -> mods{shift = if pressed then Down else Up}
          GLFW.Key'LeftControl -> mods{ctrl = if pressed then Down else Up}
          GLFW.Key'LeftAlt -> mods{alt = if pressed then Down else Up}
          _ -> mods

    if mods' /= mods
      then do
        let glfwState' = glfwState{modifiers = mods'}
        writeIORef stateRef glfwState'
        return (True, glfwState')
      else return (False, glfwState)


-- GLFW calls this on a when the user presses or releases a character key.
callbackChar ::
  IORef GLFWState ->
  [Callback] ->
  GLFW.CharCallback
-- Window -> Char -> IO ()
-- -> Char -> Bool -> IO ()

callbackChar stateRef callbacks _win char -- keystate
  =
  do
    (GLFWState mods pos _ _ _ _ _ _) <- readIORef stateRef
    let key' = charToSpecial char
    -- TODO: is this correct? GLFW does not provide the keystate
    -- in a character callback, here we asume that its pressed
    let keystate = True

    -- Only key presses of characters are passed to this callback,
    -- character key releases are caught by the 'keyCallback'. This is an
    -- intentional feature of GLFW. What this means that a key press of
    -- the '>' char  (on a US Intl keyboard) is captured by this callback,
    -- but a release is captured as a '.' with the shift-modifier in the
    -- keyCallback.
    let keystate' = if keystate then Down else Up

    -- Call all the Brillo KeyMouse actions with the new state.
    mapM_
      (\f -> f key' keystate' mods pos)
      ([f stateRef | KeyMouse f <- callbacks])


-- GLFW calls on this when the user clicks or releases a mouse button.
callbackMouseButton ::
  IORef GLFWState ->
  [Callback] ->
  GLFW.MouseButtonCallback -- = Window -> MouseButton -> MouseButtonState -> ModifierKeys -> IO ()
callbackMouseButton stateRef callbacks _win key keystate _modifier =
  do
    (GLFWState mods pos _ _ _ _ _ _) <- readIORef stateRef
    let key' = fromGLFW key
    let keystate' = if keystate == GLFW.MouseButtonState'Pressed then Down else Up

    -- Call all the Brillo KeyMouse actions with the new state.
    mapM_
      (\f -> f key' keystate' mods pos)
      ([f stateRef | KeyMouse f <- callbacks])


-- GLFW calls on this when the user moves the mouse wheel.
callbackMouseWheel ::
  IORef GLFWState ->
  [Callback] ->
  GLFW.ScrollCallback
-- -> Int
-- -> IO ()
-- ScrollCallback = Window -> Double -> Double -> IO ()
callbackMouseWheel stateRef callbacks _win x _y =
  do
    (key, keystate) <- setMouseWheel stateRef (floor x)
    (GLFWState mods pos _ _ _ _ _ _) <- readIORef stateRef

    -- Call all the Brillo KeyMouse actions with the new state.
    mapM_
      (\f -> f key keystate mods pos)
      ([f stateRef | KeyMouse f <- callbacks])


setMouseWheel ::
  IORef GLFWState ->
  Int ->
  IO (Key, KeyState)
setMouseWheel stateRef w =
  do
    glfwState <- readIORef stateRef
    writeIORef stateRef $ glfwState{mouseWheelPos = w}
    case compare w (mouseWheelPos glfwState) of
      LT -> return (MouseButton WheelDown, Down)
      GT -> return (MouseButton WheelUp, Down)
      EQ -> return (SpecialKey KeyUnknown, Up)


-- | GLFW calls this when the user drops files/directories onto the window
callbackDrop ::
  IORef GLFWState ->
  [Callback] ->
  GLFW.DropCallback
callbackDrop stateRef callbacks _win paths = do
  mapM_
    (\f -> f paths)
    ([f stateRef | Drop f <- callbacks])


-- Motion Callback ------------------------------------------------------------

-- | Callback for when the user moves the mouse.
installMotionCallbackGLFW ::
  IORef GLFWState ->
  [Callback] ->
  IO ()
installMotionCallbackGLFW stateRef callbacks =
  do
    win <- windowHandle stateRef
    GLFW.setCursorPosCallback win (Just $ callbackMotion stateRef callbacks)


-- Drop Paths Callback ---------------------------------------------------------

-- | Callback for when the user drops files/directories onto the window.
installDropCallbackGLFW ::
  IORef GLFWState ->
  [Callback] ->
  IO ()
installDropCallbackGLFW stateRef callbacks = do
  win <- windowHandle stateRef
  GLFW.setDropCallback win (Just $ callbackDrop stateRef callbacks)


-- CursorPosCallback = Window -> Double -> Double -> IO ()

callbackMotion ::
  IORef GLFWState ->
  [Callback] ->
  GLFW.CursorPosCallback
callbackMotion stateRef callbacks _win x y =
  do
    pos <- setMousePos stateRef (floor x) (floor y)

    -- Call all the Brillo Motion actions with the new state.
    mapM_
      (\f -> f pos)
      ([f stateRef | Motion f <- callbacks])


setMousePos ::
  IORef GLFWState ->
  Int ->
  Int ->
  IO (Int, Int)
setMousePos stateRef x y =
  do
    let pos = (x, y)

    modifyIORef' stateRef $ \s ->
      s
        { mousePosition = pos
        }

    return pos


-- Idle Callback --------------------------------------------------------------

{-| Callback for when GLFW has finished its jobs and it's time for us to do
  something for our application.
-}
installIdleCallbackGLFW ::
  IORef GLFWState ->
  [Callback] ->
  IO ()
installIdleCallbackGLFW stateRef callbacks =
  modifyIORef' stateRef $ \s ->
    s
      { idle = callbackIdle stateRef callbacks
      }


callbackIdle ::
  IORef GLFWState ->
  [Callback] ->
  IO ()
callbackIdle stateRef callbacks =
  sequence_ $
    [f stateRef | Idle f <- callbacks]


-- Main Loop ------------------------------------------------------------------

runMainLoopGLFW :: IORef GLFWState -> IO ()
runMainLoopGLFW stateRef = do
  X.catch go handleException
  GLFW.destroyWindow =<< windowHandle stateRef
  GLFW.terminate
  where
    handleException :: X.SomeException -> IO ()
    handleException = print

    clearDirtyFlag :: IO ()
    clearDirtyFlag =
      modifyIORef'
        stateRef
        (\state -> state{dirtyScreen = False})

    display' :: IO ()
    display' = readIORef stateRef >>= display

    idle' :: IO ()
    idle' = readIORef stateRef >>= idle

    swapBuffers' :: IO ()
    swapBuffers' = windowHandle stateRef >>= GLFW.swapBuffers

    windowShouldClose :: IO Bool
    windowShouldClose = windowHandle stateRef >>= GLFW.windowShouldClose

    unlessM :: (Monad m) => m Bool -> m () -> m ()
    unlessM testAction action = do
      sentinel <- testAction
      unless sentinel action

    go :: IO ()
    go = do
      -- Perform drawing, clear the dirty flag, do idle processing
      display'
      clearDirtyFlag
      idle'

      -- Swap buffers. This swaps the GL buffers and will block
      -- until the next v-sync. In GLFW, this effectively pegs the
      -- maximum frame rate to 60fps, but will also stop the
      -- application from consuming 100% CPU.
      swapBuffers'

      -- Poll for GLFW events; quit if necessary.
      GLFW.pollEvents
      unlessM windowShouldClose go


-- Redisplay ------------------------------------------------------------------
postRedisplayGLFW ::
  IORef GLFWState ->
  IO ()
postRedisplayGLFW stateRef =
  modifyIORef' stateRef $ \s ->
    s
      { dirtyScreen = True
      }


-- Key Code Conversion --------------------------------------------------------
class GLFWKey a where
  fromGLFW :: a -> Key


instance GLFWKey GLFW.Key where
  fromGLFW key =
    case key of
      GLFW.Key'A -> charToSpecial 'a'
      GLFW.Key'B -> charToSpecial 'b'
      GLFW.Key'C -> charToSpecial 'c'
      GLFW.Key'D -> charToSpecial 'd'
      GLFW.Key'E -> charToSpecial 'e'
      GLFW.Key'F -> charToSpecial 'f'
      GLFW.Key'G -> charToSpecial 'g'
      GLFW.Key'H -> charToSpecial 'h'
      GLFW.Key'I -> charToSpecial 'i'
      GLFW.Key'J -> charToSpecial 'j'
      GLFW.Key'K -> charToSpecial 'k'
      GLFW.Key'L -> charToSpecial 'l'
      GLFW.Key'M -> charToSpecial 'm'
      GLFW.Key'N -> charToSpecial 'n'
      GLFW.Key'O -> charToSpecial 'o'
      GLFW.Key'P -> charToSpecial 'p'
      GLFW.Key'Q -> charToSpecial 'q'
      GLFW.Key'R -> charToSpecial 'r'
      GLFW.Key'S -> charToSpecial 's'
      GLFW.Key'T -> charToSpecial 't'
      GLFW.Key'U -> charToSpecial 'u'
      GLFW.Key'V -> charToSpecial 'v'
      GLFW.Key'W -> charToSpecial 'w'
      GLFW.Key'X -> charToSpecial 'x'
      GLFW.Key'Y -> charToSpecial 'y'
      GLFW.Key'Z -> charToSpecial 'z'
      GLFW.Key'Space -> SpecialKey KeySpace
      GLFW.Key'Escape -> SpecialKey KeyEsc
      GLFW.Key'F1 -> SpecialKey KeyF1
      GLFW.Key'F2 -> SpecialKey KeyF2
      GLFW.Key'F3 -> SpecialKey KeyF3
      GLFW.Key'F4 -> SpecialKey KeyF4
      GLFW.Key'F5 -> SpecialKey KeyF5
      GLFW.Key'F6 -> SpecialKey KeyF6
      GLFW.Key'F7 -> SpecialKey KeyF7
      GLFW.Key'F8 -> SpecialKey KeyF8
      GLFW.Key'F9 -> SpecialKey KeyF9
      GLFW.Key'F10 -> SpecialKey KeyF10
      GLFW.Key'F11 -> SpecialKey KeyF11
      GLFW.Key'F12 -> SpecialKey KeyF12
      GLFW.Key'F13 -> SpecialKey KeyF13
      GLFW.Key'F14 -> SpecialKey KeyF14
      GLFW.Key'F15 -> SpecialKey KeyF15
      GLFW.Key'F16 -> SpecialKey KeyF16
      GLFW.Key'F17 -> SpecialKey KeyF17
      GLFW.Key'F18 -> SpecialKey KeyF18
      GLFW.Key'F19 -> SpecialKey KeyF19
      GLFW.Key'F20 -> SpecialKey KeyF20
      GLFW.Key'F21 -> SpecialKey KeyF21
      GLFW.Key'F22 -> SpecialKey KeyF22
      GLFW.Key'F23 -> SpecialKey KeyF23
      GLFW.Key'F24 -> SpecialKey KeyF24
      GLFW.Key'F25 -> SpecialKey KeyF25
      GLFW.Key'Up -> SpecialKey KeyUp
      GLFW.Key'Down -> SpecialKey KeyDown
      GLFW.Key'Left -> SpecialKey KeyLeft
      GLFW.Key'Right -> SpecialKey KeyRight
      GLFW.Key'Tab -> SpecialKey KeyTab
      GLFW.Key'Enter -> SpecialKey KeyEnter
      GLFW.Key'Backspace -> SpecialKey KeyBackspace
      GLFW.Key'Insert -> SpecialKey KeyInsert
      GLFW.Key'Delete -> SpecialKey KeyDelete
      GLFW.Key'PageUp -> SpecialKey KeyPageUp
      GLFW.Key'PageDown -> SpecialKey KeyPageDown
      GLFW.Key'Home -> SpecialKey KeyHome
      GLFW.Key'End -> SpecialKey KeyEnd
      GLFW.Key'Pad0 -> SpecialKey KeyPad0
      GLFW.Key'Pad1 -> SpecialKey KeyPad1
      GLFW.Key'Pad2 -> SpecialKey KeyPad2
      GLFW.Key'Pad3 -> SpecialKey KeyPad3
      GLFW.Key'Pad4 -> SpecialKey KeyPad4
      GLFW.Key'Pad5 -> SpecialKey KeyPad5
      GLFW.Key'Pad6 -> SpecialKey KeyPad6
      GLFW.Key'Pad7 -> SpecialKey KeyPad7
      GLFW.Key'Pad8 -> SpecialKey KeyPad8
      GLFW.Key'Pad9 -> SpecialKey KeyPad9
      GLFW.Key'PadDivide -> SpecialKey KeyPadDivide
      GLFW.Key'PadMultiply -> SpecialKey KeyPadMultiply
      GLFW.Key'PadSubtract -> SpecialKey KeyPadSubtract
      GLFW.Key'PadAdd -> SpecialKey KeyPadAdd
      GLFW.Key'PadDecimal -> SpecialKey KeyPadDecimal
      GLFW.Key'PadEqual -> Char '='
      GLFW.Key'PadEnter -> SpecialKey KeyPadEnter
      _ -> SpecialKey KeyUnknown


{-| Convert char keys to special keys to work around a bug in
  GLFW 2.7. On OS X, GLFW sometimes registers special keys as char keys,
  so we convert them back here.
  GLFW 2.7 is current as of Nov 2011, and is shipped with the Hackage
  binding GLFW-b 0.2.*
-}
charToSpecial :: Char -> Key
charToSpecial c = case fromEnum c of
  32 -> SpecialKey KeySpace
  63232 -> SpecialKey KeyUp
  63233 -> SpecialKey KeyDown
  63234 -> SpecialKey KeyLeft
  63235 -> SpecialKey KeyRight
  63236 -> SpecialKey KeyF1
  63237 -> SpecialKey KeyF2
  63238 -> SpecialKey KeyF3
  63239 -> SpecialKey KeyF4
  63240 -> SpecialKey KeyF5
  63241 -> SpecialKey KeyF6
  63242 -> SpecialKey KeyF7
  63243 -> SpecialKey KeyF8
  63244 -> SpecialKey KeyF9
  63245 -> SpecialKey KeyF10
  63246 -> SpecialKey KeyF11
  63247 -> SpecialKey KeyF12
  63248 -> SpecialKey KeyF13
  63272 -> SpecialKey KeyDelete
  63273 -> SpecialKey KeyHome
  63275 -> SpecialKey KeyEnd
  63276 -> SpecialKey KeyPageUp
  63277 -> SpecialKey KeyPageDown
  _ -> Char c


instance GLFWKey GLFW.MouseButton where
  fromGLFW mouse =
    case mouse of
      GLFW.MouseButton'1 -> MouseButton LeftButton
      GLFW.MouseButton'2 -> MouseButton RightButton
      GLFW.MouseButton'3 -> MouseButton MiddleButton
      GLFW.MouseButton'4 -> MouseButton $ AdditionalButton 4
      GLFW.MouseButton'5 -> MouseButton $ AdditionalButton 5
      GLFW.MouseButton'6 -> MouseButton $ AdditionalButton 6
      GLFW.MouseButton'7 -> MouseButton $ AdditionalButton 7
      GLFW.MouseButton'8 -> MouseButton $ AdditionalButton 8