-----------------------------------------------------------------------------
-- |
-- Module      :  Graphics.Rendering.Diagrams.Engine
-- Copyright   :  (c) Brent Yorgey 2008
-- License     :  BSD-style (see LICENSE)
-- Maintainer  :  byorgey@gmail.com
-- Stability   :  experimental
-- Portability :  portable
--
-- The core rendering engine for "Graphics.Rendering.Diagrams", an
-- embedded domain-specific language (EDSL) for creating simple
-- diagrams.
--
-----------------------------------------------------------------------------

module Graphics.Rendering.Diagrams.Engine
  (
    -- * Preprocessing
    -- $preproc

    sizeAndPos

    -- * Rendering
    -- $render

    -- ** User interface

  , compose
  , writePng

    -- ** Internals

  , atomic
  , render

  ) where

import Graphics.Rendering.Diagrams.Types
import Graphics.Rendering.Diagrams.Attributes
import Graphics.Rendering.Diagrams.Layouts

import qualified Graphics.Rendering.Cairo as C
import Control.Arrow ((***))
import Control.Monad.Reader
import Data.List (foldl1')

-- $preproc
-- These functions take a user-generated 'Diagram' object and
-- preprocess it in preparation for final rendering.  The
-- preprocessing includes calculating diagram sizes and positioning
-- diagrams by the addition of appropriate translate annotations.

-- | Given a 'Diagram', compute its total size, and produce a new
--   version of the 'Diagram' with all sub-'Diagram's positioned
--   properly.
sizeAndPos :: Diagram -> (Point,Diagram)

-- the empty diagram takes up no space.
sizeAndPos Empty = ((0,0), Empty)

sizeAndPos d@(Prim (Shape s)) = (shapeSize s, d)

-- ignore the size calculated by the recursive call, and use the given
-- size instead.
sizeAndPos (Sized s d) = (s, snd $ sizeAndPos d)

-- attributes may affect the size of a diagram.
sizeAndPos (Ann a@(Attr attr) d) = (attrSize attr s, (Ann a d'))
  where (s, d') = sizeAndPos d

sizeAndPos (Compound (Layout l ds)) = (s, Union ds')
  where (s, ds') = layoutSizeAndPos l (fmap sizeAndPos ds)

-- $render
-- The rendering code takes a 'Diagram' and turns it into
-- actual graphics output, using the Cairo library to perform the low-level
-- drawing operations.

-- | Given a target width and height and a user-constructed 'Diagram',
--   render it using the Cairo rendering library.  Note that 'compose'
--   takes care of all the rendering details, including preprocessing
--   of the 'Diagram', and scaling/translating the final output so
--   that it fits within the given width and height.
--
--   The background of the output diagram will be opaque white.
--
--   In order to produce a physical output, the output of 'compose'
--   must be given as input to an output adapter such as 'writePng'.
compose :: Double      -- ^ output width
        -> Double      -- ^ output height
        -> Diagram     -- ^ 'Diagram' to render
        -> C.Render ()
compose w h d = do

  -- Preprocess the diagram, and use the global bounding box size to
  -- scale and translate the output so that it fits within the target
  -- output width and height.
  let ((x,y), d') = sizeAndPos d
      s           = min (w/x) (w/y)
  C.scale s s
  C.translate (x/2) (y/2)

  -- Set the output background to opaque white.
  C.save
  C.setSourceRGBA 1 1 1 1
  C.paint
  C.restore

  -- render the final diagram.
  flip runDiaRenderM defaultDiaRenderEnv . render $ d'

-- | Given a rendered diagram, output it to a .png file with the given
--   width and height.
writePng :: String -> Int -> Int -> C.Render () -> IO ()
writePng fileName w h render =
  C.withImageSurface C.FormatARGB32 w h $ \surface -> do
    C.renderWith surface render
    C.surfaceWriteToPNG surface fileName

-- | Perform a rendering operation atomically, by saving the state and
--   restoring it afterwards.
atomic :: DiaRenderM () -> DiaRenderM ()
atomic render = (c C.save) >> render >> (c C.restore)

-- | Render a diagram.
render :: Diagram -> DiaRenderM ()
render Empty = return ()
render (Prim (Shape s)) = renderShape s
render (Ann (Attr a) d) = atomic $ renderAttr a >>= flip local (render d)
render (Union ds) = mapM_ render ds
render (Sized _ d) = render d