-- #hide
module Graphics.HGL.Win32.WND
	( WND, mkWND, openWND, closeWND, redrawWND
	, handleEvents, closeAllHWNDs
	, beginGraphics, endGraphics
	, wndRect
	, getHWND
	, drawWND
	) where

import Graphics.HGL.Units (Point)
import Graphics.HGL.Internals.Event( Event(..) )
import Graphics.HGL.Internals.Draw (Draw, unDraw)
import Graphics.HGL.Internals.Events( Events, sendEvent, sendTick )
import Graphics.HGL.Internals.Utilities(safeTry, Exception)
import Graphics.HGL.Win32.Draw( DrawFun, setDefaults, withDC )
import Graphics.HGL.Win32.Types( Key(MkKey), toPoint )

import Control.Concurrent( yield )
import Control.Monad(liftM2,when)
import Data.Bits
import Data.IORef
import Data.Maybe(isJust)
import System.IO.Unsafe(unsafePerformIO)

import Graphics.Win32
import System.Win32 (getModuleHandle)

----------------------------------------------------------------
-- Once a window has been closed, we want to detect any further
-- operations on the window - so all access is via a mutable Maybe
----------------------------------------------------------------

newtype WND = MkWND (IORef (Maybe HWND))

closeWND :: WND -> IO ()
closeWND wnd@(MkWND hwndref) = do
  mb_hwnd <- readIORef hwndref
  writeIORef hwndref Nothing    -- mark it as closed
  case mb_hwnd of
    Just hwnd -> do
      removeHWND hwnd   -- added by Ulf Norell <ulfn@cs.chalmers.se>
      yield             -- added by Ulf
      destroyWindow hwnd
    Nothing   -> return ()

getHWND :: WND -> IO HWND
getHWND (MkWND hwndref) = do
  mb_hwnd <- readIORef hwndref
  case mb_hwnd of 
    Just hwnd -> return hwnd
    Nothing   -> ioError (userError "Attempted to act on closed window")

redrawWND :: WND -> IO ()
redrawWND wnd = do
  hwnd <- getHWND wnd
  invalidateRect (Just hwnd) Nothing False

drawWND :: WND -> Draw () -> IO ()
drawWND wnd p = do
  hwnd <- getHWND wnd
  withDC (Just hwnd) (\ hdc -> setDefaults hdc >> unDraw p hdc)

wndRect :: WND -> IO (Point, Point)
wndRect wnd = do
  hwnd <- getHWND wnd
  (l,t,r,b) <- getClientRect hwnd
  return (toPoint (l,t), toPoint (r,b))

mkWND :: HWND -> IO WND
mkWND hwnd = fmap MkWND (newIORef (Just hwnd))

openWND :: String -> Maybe POINT -> Maybe POINT 
           -> Events      -- where to send the events
           -> DrawFun     -- how to redraw the picture
           -> Maybe MilliSeconds  -- time between timer ticks
           -> IO WND
openWND name pos size events draw tickRate = do
  checkInitialised
  clAss <- newClass
  hwnd <- createWND name wndProc pos size wS_OVERLAPPEDWINDOW Nothing
  show hwnd False
  updateWindow hwnd
  maybe (return ())
	(\ rate -> setWinTimer hwnd 1 rate >> return ())
	tickRate	
  fmap MkWND (newIORef (Just hwnd))
 where
  wndProc hwnd msg wParam lParam = do
    -- print msg
    rs <- safeTry $ do
                 r <- windowProc 
		      (sendEvent events) 
		      draw
		      (\ wParam -> sendTick events)
		      hwnd msg wParam lParam
                 r `seq` return r  -- force it inside the try!
    case rs of
      Right a -> return a
      Left  e -> uncaughtError e >> return 0  -- Let's hope this works ok

  show hwnd iconified = 
    if iconified 
    then do  
      showWindow hwnd sW_SHOWNORMAL -- open "iconified"
      return ()
    else do 
      showWindow hwnd sW_RESTORE    -- open "restored" (ie normal size)
      bringWindowToTop hwnd

-- Note that this code uses a single (static) MSG throughout the whole
-- system - let's hope this isn't a problem
handleEvents :: IO Bool -> IO ()
handleEvents userQuit = do
  -- first wait for a window to be created or for the user prog to quit
  -- this avoids the race condition that we might quit (for lack of
  -- any windows) before the user's thread has even had a chance to run.
  safeTry $
    while (fmap not (liftM2 (||) userQuit (fmap not noMoreWindows)))
      yield   -- Ulf uses this instead of handleEvent
  -- then wait for all windows to be shut down or user to quit
  safeTry $
    while (fmap not (liftM2 (||) userQuit systemQuit)) 
      handleEvent
  return ()
 where
  while p s = do { c <- p; if c then s >> while p s else return () }

handleEvent :: IO ()
handleEvent = do
  yield  -- always yield before any blocking operation
  nowin <- noMoreWindows
  when (not nowin) $ allocaMessage $ \ lpmsg -> do
    getMessage lpmsg Nothing
    translateMessage lpmsg
    dispatchMessage lpmsg
    return ()


----------------------------------------------------------------
-- The grotty details - opening WNDs, creating classes, etc
----------------------------------------------------------------

className = mkClassName "Graphics.HGL.Win32.WND"

newClass :: IO ATOM
newClass = do
  icon         <- loadIcon   Nothing iDI_APPLICATION
  cursor       <- loadCursor Nothing iDC_ARROW
  whiteBrush   <- getStockBrush wHITE_BRUSH
  mainInstance <- getModuleHandle Nothing
  atom <- registerClass (
	(cS_HREDRAW .|. cS_VREDRAW), -- redraw if window size Changes
	mainInstance,
	(Just icon),
	(Just cursor),
	(Just whiteBrush),
	Nothing,
	className)
  --return atom
  return (maybe undefined id atom)

createWND :: String -> WindowClosure -> Maybe POINT -> Maybe POINT 
		    -> WindowStyle -> Maybe HMENU -> IO HWND
createWND name wndProc posn size style menu = do
  mainInstance <- getModuleHandle Nothing
  mbSize <- calcSize size
  hwnd <- createWindowEx 
	    0 -- Win32.wS_EX_TOPMOST 
	    className
	    name
	    style
	    (fmap (fromIntegral.fst) posn)    -- x
	    (fmap (fromIntegral.snd) posn)    -- y
	    (fmap (fromIntegral.fst) mbSize)  -- w
	    (fmap (fromIntegral.snd) mbSize)  -- h
	    Nothing                           -- parent
	    menu
	    mainInstance
	    wndProc
  addHWND hwnd
  return hwnd
 where
  calcSize :: Maybe POINT -> IO (Maybe POINT)
  calcSize = 
    maybe (return Nothing)
          (\ (width, height) -> do
             (l,t,r,b) <- adjustWindowRect (0,0,width,height) style (isJust menu)
             return $ Just (r-l, b-t))

windowProc :: (Event -> IO ()) ->   	-- Event Handler
              DrawFun ->		-- Picture redraw
	      (WPARAM -> IO ()) ->	-- tick
	      (HWND -> WindowMessage -> WPARAM -> LPARAM -> IO LRESULT)
windowProc send redraw tick hwnd msg wParam lParam
  | msg == wM_PAINT
  = paint
  | msg == wM_MOUSEMOVE
  = mouseMove lParam
  | msg == wM_LBUTTONDOWN || msg == wM_LBUTTONDBLCLK
  = button lParam True True
  | msg == wM_LBUTTONUP
  = button lParam True False
  | msg == wM_RBUTTONDOWN || msg == wM_RBUTTONDBLCLK
  = button lParam False True
  | msg == wM_RBUTTONUP
  = button lParam False False
  | msg == wM_KEYDOWN
  = key wParam True
  | msg == wM_KEYUP
  = key wParam False
  | msg == wM_CHAR
  = char wParam
  | msg == wM_TIMER
  = timer wParam
  | msg == wM_SIZE
  = resize
{-
  | msg == wM_MOUSEACTIVATE
  = do
      hwnd' <- setFocus hwnd
      if hwnd `eqHWND` hwnd' 
        then
          return mA_NOACTIVATE  -- already had input focus
        else
          return mA_ACTIVATEANDEAT
-}
  | msg == wM_DESTROY
  = destroy
  | otherwise
  = defWindowProc (Just hwnd) msg wParam lParam

 where
  paint :: IO LRESULT
  paint = paintWith hwnd (\hdc lpps -> do
	redraw hwnd hdc
	return 0
	)

  button :: LPARAM -> Bool -> Bool -> IO LRESULT
  button lParam isLeft isDown = do
	let (y,x) = lParam `divMod` 65536
	send (Button {pt = toPoint (x,y), isLeft=isLeft, isDown=isDown})
	return 0

  key :: WPARAM -> Bool -> IO LRESULT
  key wParam isDown = do
     	send (Key { keysym = MkKey wParam, isDown = isDown })
	-- by returning 1 we let it get translated into a char too
	return 1

  char :: WPARAM -> IO LRESULT
  char wParam = do
     	send (Char { char = toEnum (fromIntegral wParam) })
	return 0

  mouseMove :: LPARAM -> IO LRESULT
  mouseMove lParam = do
	let (y,x) = lParam `divMod` 65536
	send (MouseMove { pt = toPoint (x,y) })
	return 0

  timer :: WPARAM -> IO LRESULT
  timer wParam = do
	tick wParam
	return 0

  resize :: IO LRESULT
  resize = do
	-- don't send new size, it may be out of date by the time we
	-- get round to reading the event
	send Resize
	return 0

  destroy :: IO LRESULT
  destroy = do
        removeHWND hwnd
        send Closed
	return 0

paintWith :: HWND -> (HDC -> LPPAINTSTRUCT -> IO a) -> IO a
paintWith hwnd p =
  allocaPAINTSTRUCT $ \ lpps -> do
  hdc  <- beginPaint hwnd lpps
  a    <- p hdc lpps
  endPaint hwnd lpps
  return a

----------------------------------------------------------------
-- The open window list
----------------------------------------------------------------

-- It's very important that we close any windows - even if the 
-- Haskell application fails to do so (or aborts for some reason).
-- Therefore we keep a list of open windows and close them all at the
-- end.

-- persistent list of open windows
windows :: IORef [HWND]
windows = unsafePerformIO (newIORef [])

initialised :: IORef Bool
initialised = unsafePerformIO (newIORef False)

noMoreWindows :: IO Bool
noMoreWindows = fmap null (readIORef windows)

-- It's also important that we abort cleanly if an uncaught IOError
-- occurs - this flag keeps track of such things

hadUncaughtError :: IORef Bool
hadUncaughtError = unsafePerformIO (newIORef False)

-- We call this if an uncaught error has occured
uncaughtError :: Exception -> IO ()
uncaughtError e = do
  putStr "Uncaught Error: "
  print e  
  writeIORef hadUncaughtError True

systemQuit :: IO Bool
systemQuit = liftM2 (||) (readIORef hadUncaughtError) noMoreWindows

beginGraphics :: IO ()
beginGraphics = do
  closeAllHWNDs  -- just in case any are already open!
  writeIORef initialised True

checkInitialised :: IO ()
checkInitialised = do
  init <- readIORef initialised
  if init 
    then return ()
    else ioError (userError msg)
 where
  msg = "Graphics library uninitialised: perhaps you forgot to use runGraphics?"

endGraphics :: IO ()
endGraphics = do
  closeAllHWNDs
  writeIORef initialised False

closeAllHWNDs :: IO ()
closeAllHWNDs = do
  hwnds <- readIORef windows
  mapM_ destroyWindow hwnds
  writeIORef windows []
  writeIORef hadUncaughtError False -- clear the system


addHWND :: HWND -> IO ()
addHWND hwnd = do
  hwnds <- readIORef windows
  writeIORef windows (hwnd:hwnds)

-- remove a HWND from windows list
removeHWND :: HWND -> IO ()
removeHWND hwnd = do
  hwnds <- readIORef windows
  writeIORef windows (filter (/= hwnd) hwnds)