-- | Strict evaluation of diagrams with time and size limit
{-# LANGUAGE ScopedTypeVariables #-}
module Graphics.Diagrams.Escape 
    ( escapeDiagram
    , numberErrors
    ) where

import Graphics.Diagrams
import Graphics.Diagrams.Types
import Graphics.Diagrams.Colors (toRGB)

import Data.Data.GenRep.Functions (getErrorIndex)
import Control.Exception.Pure (catchPureErrorsSafe)
import System.SimpleTimeout.Limits
import System.IO.Parallel (twoParallel)

import Control.Monad (liftM2)
import Control.DeepSeq (NFData, deepseq)
import Control.Concurrent.MVar (MVar, newMVar, modifyMVar)
import Control.Monad.State (State, runState)

-------------------------

-- | Strict evaluation of diagrams with time and size limit
escapeDiagram :: TimeLimit -> SizeLimit -> Diagram -> IO Diagram
escapeDiagram t s x = do
    ch <- newBudget t s
    idv <- newMVar 1        -- to generate identifiers for groups
    escapeDiagram' ch idv x

escapeDiagram' :: Budget -> MVar Int -> Diagram -> IO Diagram
escapeDiagram' ch idv = fff where

    fff :: Diagram -> IO Diagram
    fff x = do
        m <- checkBudget ch 1 
                (return . return . err . timeError) 
                (return . return $ err "size limit exceeded")
                $ fmap (either err id) (catchPureErrorsSafe x) >>= check
        m
      where
        timeError d
            = "timeout at " ++ show (round $ 100 * d :: Int) ++ "%" 

    err s = Error s EmptyDiagram

    m1 :: NFData a => (a -> Diagram) -> a -> IO (IO Diagram)
    m1 f a = fmap (return . either err f) $ catchPureErrorsSafe $ a `deepseq` a

    m2 :: NFData a => (a -> Diagram -> Diagram) -> a -> Diagram -> IO (IO Diagram)
    m2 f a b 
        = fmap (either (\s -> fmap (Error s) $ fff b) 
                       (\a -> fmap (f a) $ fff b)) 
            $ catchPureErrorsSafe $ a `deepseq` a

    checkColor :: Color -> Color
    checkColor (Color s) = case toRGB s of
        Just _  -> Color s
        _   -> error $ "Not an SVG color: " ++ s
    checkColor (RGB r g b) = RGB (f r) (f g) (f b)  where
        f x = 0 `max` (1 `min` x)

    check :: Diagram -> IO (IO Diagram)

    check EmptyDiagram      = return $ return EmptyDiagram
    check (Circle r)        = m1 Circle (if r >= 0 then r else error "negative radius") 
    check (Text p s)        = m1 (uncurry Text) (p, s)
    check (Rect a b)        = m1 (uncurry Rect) (a, b)
    check (Polyline loop l) = fmap g $ m1 (uncurry Polyline) (loop, l) where
        g m = m >>= \x -> case x of
            Polyline loop l -> do
                (b, l) <- decSizeBudget ch $ \x -> case splitAt (x+2) l of
                        (a,[]) -> (min x (x+2-length a), (True, a))
                        (a,_)  -> (0, (False, a))
                return $ if b 
                    then Polyline loop l
                    else Error "size limit exceeded" $ Polyline False l
            _ -> return x

    check (Link s x)        = m2 Link s x
    check (FontFamily s x)  = m2 FontFamily s x
    check (Move p x)        = m2 Move p x
    check (Scale t x)       = m2 Scale t x
    check (ScaleXY x y d)   = m2 (uncurry ScaleXY) (x,y) d
    check (Rotate t x)      = m2 Rotate t x
    check (Fill t x)        = m2 Fill (checkColor t) x
    check (Stroke t x)      = m2 Stroke (checkColor t) x
    check (StrokeWidth t x) = m2 StrokeWidth (if t >= 0 then t else error "negative stroke width") x
    check (Clip a b x)      = m2 (uncurry Clip) (a, b) x
    check (Error s x)       = m2 Error s x
    check (TransformMatrix a b c d e f x)
        = m2 (\(a,b,c,d,e,f) -> TransformMatrix a b c d e f) (a,b,c,d,e,f) x

    check (Pack x f) = return $ do
        i <- modifyMVar idv (\i -> return (i+1,i))
        fmap (\(x,y)-> Group x i y) $ twoParallel (fff x) (fff (f (Ref i)))

    check (Overlay a b) = return $ fmap (uncurry Overlay) $ twoParallel (fff a) (fff b)

    check (Ref i)       = return $ return $ Ref i
    check (Group _ _ _) = error "check: Group not possible"

-- | Error message extraction and numbering
numberErrors 
    :: Diagram
    -> (Diagram, [(String, String)])
numberErrors d = (res, reverse $ map swap errs)
 where
    swap (a,b) = (b,a)

    (res, (_, errs)) = runState (replace d) (0, [])

    m2 f x = fmap f $ replace x

    replace :: Diagram -> State (Int, [(String, String)]) Diagram
    replace (Link s x)        = m2 (Link s) x
    replace (FontFamily s x)  = m2 (FontFamily s) x
    replace (Move p x)        = m2 (Move p) x
    replace (Scale t x)       = m2 (Scale t) x
    replace (Rotate t x)      = m2 (Rotate t) x
    replace (Fill t x)        = m2 (Fill t) x
    replace (Stroke t x)      = m2 (Stroke t) x
    replace (StrokeWidth t x) = m2 (StrokeWidth t) x
    replace (Clip a b x)      = m2 (Clip a b) x
    replace (TransformMatrix a b c d e f x)
        = m2 (TransformMatrix a b c d e f) x
    replace (Group a i b)   = liftM2 (\a b -> Group a i b) (replace a) (replace b)
    replace (Overlay a b) = liftM2 Overlay (replace a) (replace b)
    replace (Error e x) = do
        i <- getErrorIndex e
        x <- replace x
        return $ Overlay x (i `textAt` (0,0) `fill` red)
    replace x = return x