{-# LANGUAGE ExistentialQuantification, NamedFieldPuns, RecordWildCards #-}
{-# OPTIONS -Wall #-}
-- | For a higher-level API for textual OSDs using /Pango/, use "Graphics.Aosd.Pango".
module Graphics.Aosd(
    -- * Renderers
    AosdRenderer(..), GeneralRenderer(..), 
--     -- ** Simple combinators
--     HCatRenderer(..), VCatRenderer(..),
    -- * Options
    AosdOptions(..), Transparency(..), Position(..), XClassHint(..), defaultOpts,
    -- * Displaying
    aosdFlash,
    FlashDurations(..), symDurations,
    -- ** Low-level operations
    AosdPtr(..),aosdNew,reconfigure,
    aosdRender, aosdShow, aosdHide, aosdLoopOnce, aosdLoopFor,

    -- * Reexports
    module Graphics.Rendering.Cairo,
    Rectangle(..),
    CInt, CUInt

    
    
    ) where

import Control.Exception
import Control.Monad.IO.Class
import Control.Monad.Trans.Reader
import Foreign
import Foreign.C
import Graphics.Aosd.AOSD_H
import Graphics.Aosd.Util
import Graphics.Rendering.Cairo
import Graphics.Rendering.Cairo.Internal(runRender)
import Data.Functor
import Graphics.Rendering.Pango.Enums
import Graphics.X11.Xlib(openDisplay,closeDisplay,displayHeight,displayWidth,defaultScreen)

class AosdRenderer a where
    toGeneralRenderer :: a -> IO GeneralRenderer


data XClassHint = XClassHint { resName, resClass :: String }
    deriving(Show)

data Transparency = None | Fake | Composite
    deriving(Show)

data Position = Min -- ^ Left/top
              | Center 
              | Max -- ^ Right/bottom
    deriving(Show,Enum,Bounded)

data AosdOptions = AosdOptions {
    -- | 'Nothing' = use /libaosd/ default.
    classHint :: Maybe XClassHint,
    -- | 'Nothing' = use /libaosd/ default.
    transparency :: Maybe Transparency,
    xPos :: Position,
    yPos :: Position,
    -- | Positive values denote a rightwards respectively downwards offset (in pixels).
    offset :: (CInt,CInt),
    -- | 'Nothing' = use /libaosd/ default.
    hideUponMouseEvent :: Maybe Bool,
    -- | Mouse-click event handler.
    mouseEventCB :: Maybe (C'AosdMouseEvent -> IO ())
}

data GeneralRenderer = GeneralRenderer {
    grRender :: Render (),
    -- | Part of the surface that the renderer actually draws on (determines the window size).
    grInkExtent :: Rectangle,
    -- | Part of the surface whose...
    --
    -- * ... left edge is aligned to the left edge of the screen, if 'xPos' is 'Min'
    --
    -- * ... center is aligned to the center of the screen, if 'xPos' is 'Center'
    --
    -- * ... right edge is aligned to the right edge of the screen, if 'xPos' is 'Max'
    --
    -- (Likewise for the /y/ axis)
    grPositioningExtent :: Rectangle
}


-- -- | Horizontal concatenation
-- data HCatRenderer = forall r1 r2. (AosdRenderer r1, AosdRenderer r2) => HCat r1 r2
-- 
-- 
-- -- | Vertical concatenation
-- data VCatRenderer = forall r1 r2. (AosdRenderer r1, AosdRenderer r2) => VCat r1 r2


instance AosdRenderer GeneralRenderer where
    toGeneralRenderer = return

-- instance AosdRenderer HCatRenderer where
--     toGeneralRenderer (HCat r1 r2) = do
--         gr1 <- toGeneralRenderer r1
--         gr2 <- toGeneralRenderer r2
--         return GeneralRenderer {
--                     grWidth = ((+) `on` grWidth) gr1 gr2,
--                     grHeight = (max `on` grHeight) gr1 gr2,
--                     grRender = do
--                         grRender gr1
--                         translate (fromIntegral $ grWidth gr1) 0
--                         grRender gr2
--                }
-- 
-- instance AosdRenderer VCatRenderer where
--     toGeneralRenderer (VCat r1 r2) = do
--         gr1 <- toGeneralRenderer r1
--         gr2 <- toGeneralRenderer r2
--         return GeneralRenderer {
--                     grWidth = (max `on` grWidth) gr1 gr2,
--                     grHeight = ((+) `on` grHeight) gr1 gr2,
--                     grRender = do
--                         grRender gr1
--                         translate 0 (fromIntegral $ grHeight gr1)
--                         grRender gr2
--                }

toC'AosdRenderer :: Render () -> IO C'AosdRenderer
toC'AosdRenderer r = mk'AosdRenderer f
    where
        f cairo _ = runReaderT (runRender r) cairo


toAosdTransparency :: Transparency -> C'AosdTransparency
toAosdTransparency None = c'TRANSPARENCY_NONE
toAosdTransparency Fake = c'TRANSPARENCY_FAKE
toAosdTransparency Composite = c'TRANSPARENCY_COMPOSITE


-- toAosdCoordinate :: Position -> C'AosdCoordinate
-- toAosdCoordinate Min = c'COORDINATE_MINIMUM
-- toAosdCoordinate Center = c'COORDINATE_CENTER
-- toAosdCoordinate Max = c'COORDINATE_MAXIMUM


withAosd :: (Ptr C'Aosd -> IO a) -> IO a
withAosd k = do
    a <- c'aosd_new 
    finally (k a) (c'aosd_destroy a)



withConfiguredAosd :: AosdRenderer a => AosdOptions -> a -> (Ptr C'Aosd -> IO r) -> IO r
withConfiguredAosd opts x k = 
    withAosd (\a -> do
        reconfigure0 opts x a 
        k a
    )


data ScreenSize = ScreenSize { screenWidth, screenHeight :: Int }
    deriving(Show)



getScreenSize :: IO ScreenSize
getScreenSize = do
        display <- openDisplay ""

        let go = do
                -- Work around unsafe FFI declarations in the X11 bindings...
                screen <- evaluate $ defaultScreen display
                screenWidth <- evaluate . fromIntegral $ displayWidth display screen
                screenHeight <- evaluate . fromIntegral $ displayHeight display screen

                return ScreenSize{..}

        go `finally` closeDisplay display

reconfigure0 :: AosdRenderer a => AosdOptions -> a -> Ptr C'Aosd -> IO ()
reconfigure0 AosdOptions{..} renderer a = do
        GeneralRenderer{..} <- toGeneralRenderer renderer 
        maybeDo (setClassHint a) classHint
        
        maybeDo (c'aosd_set_transparency a . toAosdTransparency) transparency

        ScreenSize{..} <- getScreenSize


        let -- l=Left, t=Top, w=Width, h=Height 
            Rectangle li ti wi hi = grInkExtent
            Rectangle lp tp wp hp = grPositioningExtent

{- 
(These comments only look at the x dimension; the other is analogous) 

Consider the mapping "screenx" from grRender x coordinates to screen x coordinates

Since we translate grRender by -(li,ti), we have:

        screenx x = windowLeft + x - li 

If xPos is Min, we want: 

        screenx lp = 0
        <=>
        windowLeft + lp - li = 0
        <=>
        windowLeft = li - lp

If xPos is Center, we want:

        screenx (lp + wp/2) = screenWidth/2
        <=>
        windowLeft + (lp + wp/2) - li = screenWidth/2
        <=>
        windowLeft = li - lp + (screenWidth - wp)/2 

If xPos is Max, we want:

        screenx (lp+wp) = screenWidth
        <=>
        windowLeft + (lp+wp) - li = screenWidth
        <=>
        windowLeft = li - lp + screenWidth - wp

-}
            calculateOffsetAdjustment pos min_ink min_positioning size_positioning size_screen  = fromIntegral $ 
                        case pos of
                              Min -> min_ink - min_positioning
                              Center -> min_ink - min_positioning + div (size_screen - size_positioning) 2
                              Max -> min_ink - min_positioning + size_screen - size_positioning 

                      

            windowLeft = calculateOffsetAdjustment xPos li lp wp screenWidth  + fst offset
            windowTop  = calculateOffsetAdjustment yPos ti tp hp screenHeight + snd offset

            windowWidth = fromIntegral wi
            windowHeight = fromIntegral hi

            finalRenderer = do
                translate (fi . negate $ li) (fi . negate $ ti)
                grRender



        c'aosd_set_geometry a windowLeft windowTop windowWidth windowHeight
            

        rendererPtr <- toC'AosdRenderer finalRenderer
        c'aosd_set_renderer a rendererPtr nullPtr 

        maybeDo (setHideUponMouseEvent a) hideUponMouseEvent

        maybeDo (setMouseEventCB a) mouseEventCB

setClassHint :: Ptr C'Aosd -> XClassHint -> IO ()
setClassHint a XClassHint{ resName, resClass } = 
    withCString resName (\resName' ->
        withCString resClass (\resClass' ->
            c'aosd_set_names a resName' resClass'))

setHideUponMouseEvent :: Ptr C'Aosd -> Bool -> IO ()
setHideUponMouseEvent a b = c'aosd_set_hide_upon_mouse_event a (if b then 1 else 0)

setMouseEventCB :: Ptr C'Aosd -> (C'AosdMouseEvent -> IO ()) -> IO ()
setMouseEventCB a f = do
    fptr <- mk'AosdMouseEventCb f'
    c'aosd_set_mouse_event_cb a fptr nullPtr
  where
    f' :: Ptr C'AosdMouseEvent -> Ptr () -> IO () 
    f' p _ = do 
        mouseEvent <- peek p 
        f mouseEvent

-- | Non-'Nothing' defaults:
--
-- *       transparency = Just Composite,
--
-- *       xPos = Center, 
--
-- *       yPos = Center,
-- 
-- *       offset = (0,0),
--
-- *       hideUponMouseEvent = Just True
defaultOpts :: AosdOptions
defaultOpts =
    AosdOptions { 
        classHint = Nothing,
        transparency = Just Composite,
        xPos = Center, 
        yPos = Center,
        offset = (0,0),
        hideUponMouseEvent = Just True,
        mouseEventCB = Nothing
    }

data FlashDurations = FlashDurations {
    inMillis :: CUInt -- ^ Fade-in time in milliseconds 
  , fullMillis :: CUInt -- ^ Full display time in milliseconds
  , outMillis :: CUInt -- ^ Fade-out time in milliseconds
}
    deriving(Show)

-- | Construct a 'FlashDurations' with equal 'inMillis' and 'outMillis'.
symDurations :: 
        CUInt -- ^ 'inMillis' and 'outMillis'. 
     -> CUInt -- ^ 'fullMillis'.
     -> FlashDurations
symDurations fadeMillis fullMillis = FlashDurations fadeMillis fullMillis fadeMillis

-- | Main high-level displayer. Blocks. 
aosdFlash :: AosdRenderer a => AosdOptions -> a -> FlashDurations -> IO ()
aosdFlash opts renderer FlashDurations{..} = 
    withConfiguredAosd opts renderer (\p -> c'aosd_flash p inMillis fullMillis outMillis)

                               



newtype AosdPtr = AosdPtr { unAosdPtr :: ForeignPtr C'Aosd }

-- | Create an /Aosd/ object managed by the garbage collector.
aosdNew :: AosdRenderer a => AosdOptions -> a -> IO AosdPtr
aosdNew opts r = do
    p <- c'aosd_new
    reconfigure0 opts r p
    AosdPtr <$> Foreign.newForeignPtr p'aosd_destroy p


reconfigure :: AosdRenderer a => AosdOptions -> a -> AosdPtr -> IO ()
reconfigure opts r = (`withForeignPtr` reconfigure0 opts r) . unAosdPtr

aosdRender :: AosdPtr -> IO ()
aosdRender = (`withForeignPtr` c'aosd_render) . unAosdPtr

aosdShow :: AosdPtr -> IO ()
aosdShow = (`withForeignPtr` c'aosd_show) . unAosdPtr

aosdHide :: AosdPtr -> IO ()
aosdHide = (`withForeignPtr` c'aosd_hide) . unAosdPtr

aosdLoopOnce :: AosdPtr -> IO ()
aosdLoopOnce = (`withForeignPtr` c'aosd_loop_once) . unAosdPtr

aosdLoopFor :: AosdPtr 
            -> CUInt -- ^ Time in milliseconds.
            -> IO ()
aosdLoopFor (AosdPtr fp) millis = (fp `withForeignPtr` (`c'aosd_loop_for` millis))