{-# LANGUAGE RecordWildCards #-}
module Affection
    ( withAffection
    , getAffection
    , putAffection
    -- , withWindow
    -- , withDefaultWindow
    , delaySec
    , get
    , put
    , module A
    ) where

import qualified SDL
import qualified SDL.Internal.Numbered as SDL (toNumber)
import qualified SDL.Raw as Raw
import qualified GEGL as G

import Data.Maybe
import Data.IORef

import System.Clock

import Control.Monad.Loops
import Control.Monad.State

import Foreign.C.Types (CInt(..))
import Foreign.Storable (peek)

import Debug.Trace

import Affection.Types as A
import Affection.Draw as A
import Affection.Particle as A

import qualified BABL as B

-- | Main function which bootstraps everything else.
withAffection
  :: AffectionConfig us -- ^ Configuration of the Game and its engine.
  -> IO ()
withAffection AffectionConfig{..} = do
  -- intialiaze SDL
  case initComponents of
    All ->
      SDL.initializeAll
    Only is ->
      SDL.initialize is
  G.gegl_init
  -- give SDL render quality
  SDL.HintRenderScaleQuality SDL.$= SDL.ScaleLinear
  -- just checking…
  do
    renderQuality <- SDL.get SDL.HintRenderScaleQuality
    when (renderQuality /= SDL.ScaleLinear) $
      putStrLn "Warning: Linear texture filtering not enabled!"
  -- get current time
  execTime <- newIORef =<< getTime Monotonic
  -- construct window
  window <- SDL.createWindow windowTitle windowConfig
  SDL.showWindow window
  -- create renderer
  renderer <- SDL.createRenderer
    window
    (-1)
    SDL.defaultRenderer
  -- make draw surface
  surface <- SDL.createRGBSurface
    (SDL.windowInitialSize windowConfig)
    SDL.ABGR8888
  let (SDL.Surface ptr _) = surface
  pixelFormat <- liftIO $ peek . Raw.surfaceFormat =<< peek ptr
  SDL.V2 (CInt rw) (CInt rh) <- liftIO $ SDL.surfaceDimensions surface
  let (w, h) = (fromIntegral rw, fromIntegral rh)
      stride = (fromIntegral $ Raw.pixelFormatBytesPerPixel pixelFormat) * w
  pixels <- SDL.surfacePixels $ surface
  let bablFormat = B.PixelFormat B.RGBA B.CFu8
      cpp        = B.babl_components_per_pixel bablFormat
  format <- B.babl_format bablFormat
  initContainer <- (\x -> AffectionData
    { quitEvent       = False
    , userState       = x
    , drawWindow      = window
    , windowRenderer  = renderer
    , drawSurface     = surface
    , drawFormat      = format
    , drawPixels      = pixels
    , drawDimensions  = (w, h)
    , drawStride      = stride
    , drawCPP         = cpp
    , drawStack       = []
    , elapsedTime     = 0
    }) <$> loadState surface
  (_, nState) <- runStateT ( A.runState $ do
    preLoop
    whileM_ (do
        current <- get
        return $ not $ A.quitEvent current
      )
      (do
        -- Measure time difference form last run
        now      <- liftIO $ getTime Monotonic
        lastTime <- liftIO $ readIORef execTime
        -- get state
        ad <- get
        -- clean draw requests from last run
        mapM_ (invalidateDrawRequest (drawPixels ad) (drawStride ad) (drawCPP ad)) (drawStack ad)
        -- clean the renderer form last time
        SDL.clear renderer
        -- compute dt and update elapsedTime
        let dt = (fromIntegral $ toNanoSecs $ diffTimeSpec lastTime now) / (fromIntegral 10 ^ 9)
        put $ ad
          { drawStack = []
          , elapsedTime = elapsedTime ad + dt
          }
        -- poll events
        evs <- preHandleEvents =<< liftIO SDL.pollEvents
        -- execute user defined update loop
        updateLoop dt evs
        -- execute user defined draw loop
        drawLoop
        -- handle all new draw requests
        ad2 <- get
        clear <- catMaybes <$>
          mapM (handleDrawRequest (drawPixels ad) (drawStride ad) (drawCPP ad)) (drawStack ad2)
        -- save all draw requests to clear in next run
        put $ ad2
          { drawStack = clear }
        -- get texture from surface
        texture <- SDL.createTextureFromSurface (windowRenderer ad2) (drawSurface ad2)
        -- actual drawing
        SDL.copy (windowRenderer ad2) texture Nothing Nothing
        SDL.present (windowRenderer ad2)
        -- clean the texture
        SDL.destroyTexture texture
        -- save new time
        liftIO $ writeIORef execTime $ now
      )
    ) initContainer
  G.gegl_exit
  cleanUp $ userState nState
  SDL.destroyWindow window
  SDL.quit

getSurfaces :: SDL.Window -> IO (SDL.Surface, SDL.Surface)
getSurfaces window = do
  oldSurf@(SDL.Surface ptr _) <- SDL.getWindowSurface window
  rawSurfacePtr <- Raw.convertSurfaceFormat ptr (SDL.toNumber SDL.ABGR8888) 0
  let surface = (flip SDL.Surface Nothing) rawSurfacePtr
  return (oldSurf, surface)

-- Prehandle SDL events in case any window events occur
preHandleEvents :: [SDL.Event] -> Affection us [SDL.Event]
preHandleEvents evs =
  mapM handle evs
  where
    handle e =
      case SDL.eventPayload e of
        SDL.WindowMovedEvent _ -> do
          liftIO $ traceIO "I was moved"
          return e
        _ ->
          return e

-- | Return the userstate to the user
getAffection :: Affection us us
getAffection = do
  ad <- get
  return $ userState ad

-- | Put altered user state back
putAffection
  :: us              -- User state
  -> Affection us ()
putAffection us = do
  ad <- get
  put $ ad
    { userState = us }

-- | block a thread for a specified amount of time
delaySec
  :: Int -- ^ Number of seconds
  -> IO ()
delaySec dur = SDL.delay (fromIntegral $ dur * 1000)