-- |
-- Module      : Main
-- Copyright   : [2013] Manuel M T Chakravarty
-- License     : BSD3
--
-- Maintainer  : Manuel M T Chakravarty <chak@cse.unsw.edu.au>
-- Portability : haskell2011

module Graphics.Gloss.Game (

    -- * Reexport some basic Gloss datatypes
  module Graphics.Gloss.Data.Color,
  module Graphics.Gloss.Data.Display,
  module Graphics.Gloss.Data.Picture,
  module Graphics.Gloss.Interface.Pure.Game,
  
    -- * Geometry
  Size, Rect,

    -- * Load sprites into pictures
  bmp, png, jpg,
  
    -- * Query pictures
  boundingBox,
  
    -- * More convenient game play
  play, playInScene,
  
    -- * Game scenes
  Animation, animation, noAnimation, animationPicture,
  Scene, picture, picturing, animating, translating, rotating, scaling, scenes,
  drawScene,
) where

  -- standard libraries
import Data.IORef
import Data.Maybe
import System.IO.Unsafe (unsafePerformIO)

  -- packages
import Graphics.Gloss.Data.Color
import Graphics.Gloss.Data.Display
import Graphics.Gloss.Data.Picture        hiding (Picture(..))
import Graphics.Gloss.Data.Picture        (Picture)             -- keep 'Picture' abstract
import Graphics.Gloss.Interface.Pure.Game (Event(..), Key(..), SpecialKey(..), MouseButton(..), KeyState(..))
import Graphics.Gloss.Juicy
import qualified Graphics.Gloss                   as G
import qualified Graphics.Gloss.Interface.IO.Game as G


-- Geometry
-- --------

type Size = (Float, Float)    -- ^width & height

type Rect = (Point, Size)     -- ^origin & extent, where the origin is at the centre


-- On-the-fly image loading
-- ------------------------

-- |Turn a bitmap file into a picture.
--
-- NB: Define loaded pictures on the toplevel to avoid reloading.
--
bmp :: FilePath -> Picture
bmp fname = unsafePerformIO $ loadBMP fname

-- |Turn a PNG file into a picture.
--
-- NB: Define loaded pictures on the toplevel to avoid reloading.
--
png :: FilePath -> Picture
png fname = maybe (text "PNG ERROR") id (unsafePerformIO $ loadJuicyPNG fname)

-- |Turn a JPEG file into a picture.
--
-- NB: Define loaded pictures on the toplevel to avoid reloading.
--
jpg :: FilePath -> Picture
jpg fname = maybe (text "JPEG ERROR") id (unsafePerformIO $ loadJuicyJPG fname)


-- Query pictures
-- --------------

-- |Determine the bounding box of a picture.
--
-- FIXME: Current implementation is incomplete!
--
boundingBox :: Picture -> Rect
boundingBox G.Blank                    = ((0, 0), (0, 0))
boundingBox (G.Polygon _)              = error "Graphics.Gloss.Game.boundingbox: Polygon not implemented yet"
boundingBox (G.Line _)                 = error "Graphics.Gloss.Game.boundingbox: Line not implemented yet"
boundingBox (G.Circle r)               = ((0, 0), (2 * r, 2 * r))
boundingBox (G.ThickCircle t r)        = ((0, 0), (2 * r + t, 2 * r + t))
boundingBox (G.Arc _ _ _)              = error "Graphics.Gloss.Game.boundingbox: Arc not implemented yet"
boundingBox (G.ThickArc _ _ _ _)       = error "Graphics.Gloss.Game.boundingbox: ThickArc not implemented yet"
boundingBox (G.Text _)                 = error "Graphics.Gloss.Game.boundingbox: Text not implemented yet"
boundingBox (G.Bitmap w h _ _)         = ((0, 0), (fromIntegral w, fromIntegral h))
boundingBox (G.Color _ p)              = boundingBox p
boundingBox (G.Translate dx dy p)      = let ((x, y), size) = boundingBox p in ((x + dx, y + dy), size)
boundingBox (G.Rotate _ang _p)         = error "Graphics.Gloss.Game.boundingbox: Rotate not implemented yet"
boundingBox (G.Scale xf yf p)          = let (origin, (w, h)) = boundingBox p in (origin, (w * xf, h * yf))
boundingBox (G.Pictures _ps)           = error "Graphics.Gloss.Game.boundingbox: Pictures not implemented yet"


-- Extended play function
-- ----------------------

-- |Play a game.
--
play :: Display                      -- ^Display mode
     -> Color                        -- ^Background color
     -> Int                          -- ^Number of simulation steps to take for each second of real time
     -> world                        -- ^The initial world state
     -> (world -> Picture)           -- ^A function to convert the world to a picture
     -> (Event -> world -> world)    -- ^A function to handle individual input events
     -> [Float -> world -> world]    -- ^Set of functions invoked once per iteration —
                                     --  first argument is the period of time (in seconds) needing to be advanced
     -> IO ()
play display bg fps world draw handler steppers
  = G.play display bg fps world draw handler (perform steppers)
  where
    perform []                 _time world = world
    perform (stepper:steppers) time  world = perform steppers time (stepper time world)

-- Global variable to keep track of the time since we started playing (there can only always be one game anyway).
--
currentTime :: IORef Float
{-# NOINLINE currentTime #-}
currentTime = unsafePerformIO $ newIORef 0

-- |Play a game in a scene.
--
playInScene :: Display                               -- ^Display mode
            -> Color                                 -- ^Background color
            -> Int                                   -- ^Number of simulation steps to take for each second of real time
            -> world                                 -- ^The initial world state
            -> Scene world                           -- ^A scene parameterised by the world
            -> (Float -> Event -> world -> world)    -- ^A function to handle individual input events
                                                     --  * first argument is the absolute time (in seconds)
            -> [Float -> Float -> world -> world]    -- ^Set of functions invoked once per iteration —
                                                     --  * first argument is the absolute time (in seconds)
                                                     --  * second argument is the period of time needing to be advanced
            -> IO ()
playInScene display bg fps world scene handler steppers
  = G.playIO display bg fps world drawSceneNow performHandler (advanceTimeAndPerform steppers)
  where
    drawSceneNow world
      = do
        { now <- readIORef currentTime
        ; return $ drawScene scene now world
        }
        
    performHandler event world 
      = do
        { now <- readIORef currentTime
        ; return $ handler now event world
        }

    advanceTimeAndPerform steppers deltaT world
      = do 
        { now <- readIORef currentTime
        ; let future = now + deltaT
        ; writeIORef currentTime future
        ; perform steppers future deltaT world
        }

    perform []                 _now _deltaT world = return world
    perform (stepper:steppers) now  deltaT  world = perform steppers now deltaT (stepper now deltaT world)


-- Scenes are parameterised pictures
-- ---------------------------------

-- |An abstract representation of an animation.
--
data Animation = Animation [Picture] Float Float

-- |Construct a new animation with a list of pictures for the animation, the time between animation frames, and a given
-- (absolute) start time.
--
animation :: [Picture] -> Float -> Float -> Animation
animation = Animation

-- |An empty animation.
--
noAnimation :: Animation
noAnimation = animation [] 1 0

animationPicture :: Float -> Animation -> Maybe Picture
animationPicture now (Animation pics delay start)
  | start > now      = Nothing
  | i >= length pics = Nothing
  | otherwise        = Just $ pics !! i
  where
    i = round ((now - start) / delay)

-- |A scene describes the rendering of a world state — i.e., which picture should be draw depending on the current time
-- and of the state of the world.
--
data Scene world
  = Picturing   (Float -> world -> Picture)
  | Translating (         world -> Point)          (Scene world)
  | Rotating    (         world -> Float)          (Scene world)
  | Scaling     (         world -> (Float, Float)) (Scene world)
  | Scenes                                         [Scene world]

-- |Turn a static picture into a scene.
--
picture :: Picture -> Scene world
picture p = picturing (const p)

-- |Turn a world-dependent picture into a scene.
--
picturing :: (world -> Picture) -> Scene world
picturing worldToPic = Picturing (const worldToPic)

-- |Animate a world-dependent animation. The default picture is displayed while no animation is running.
--
animating :: (world -> Animation) -> Picture -> Scene world
animating anim defaultPic
  = Picturing (\currentTime world -> fromMaybe defaultPic $ animationPicture currentTime (anim world))

-- |Move a scene in dependences on a world-dependent location.
--
translating :: (world -> Point) -> Scene world -> Scene world
translating = Translating

-- |Rotate a scene in dependences on a world-dependent angle.
--
rotating :: (world -> Float) -> Scene world -> Scene world
rotating = Rotating

-- |Scale a scene in dependences on world-dependent scaling factors.
--
scaling :: (world -> (Float, Float)) -> Scene world -> Scene world
scaling = Scaling

-- |Compose a scene from a list of scenes.
--
scenes :: [Scene world] -> Scene world
scenes = Scenes

-- |Render a scene on the basis of time since playing started and the specific world state.
--
drawScene :: Scene world -> Float -> world -> Picture
drawScene scene time world = drawS scene
  where
    drawS (Picturing draw)             = draw time world
    drawS (Translating movement scene) = let (x, y) = movement world in translate x y (drawS scene)
    drawS (Rotating rotation scene)    = rotate (rotation world) (drawS scene)
    drawS (Scaling scaling scene)      = let (xf, yf) = scaling world in scale xf yf (drawS scene)
    drawS (Scenes scenes)              = pictures $ map drawS scenes


-- -- Game objects
-- -- ------------
-- 
-- data Object objClass =