{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
---------------------------------------------------------
-- |
-- Copyright   : (c) 2006-2016, alpheccar.org
-- License     : BSD-style
--
-- Maintainer  : misc@NOSPAMalpheccar.org
-- Stability   : experimental
-- Portability : portable
--
-- Box
---------------------------------------------------------
-- #hide
module Graphics.PDF.Typesetting.Layout (

   Container(..)
 , Width
 , Height
 , VBox(..)
 , ParagraphStyle(..)
 , VerState(..)
 , vglue
 , addTo
 , isOverfull
 , mkContainer
 , strokeVBoxes
 , containerX
 , containerY
 , containerWidth
 , containerHeight
 , containerContentHeight
 , containerContentRightBorder
 , containerContentLeftBorder
 , containerCurrentHeight
 , containerContentRectangle
 , containerParaTolerance
 ) where

import Graphics.PDF.LowLevel.Types
import Graphics.PDF.Typesetting.Breaking
import Graphics.PDF.Draw
import Graphics.PDF.Coordinates
import Graphics.PDF.Shapes(Rectangle(..))
import Graphics.PDF.Typesetting.Box
import Data.List(foldl')
import Data.Maybe(isJust,fromJust)


data VerState s = VerState { baselineskip :: !(PDFFloat,PDFFloat,PDFFloat) -- ^ Default value (12,0.17,0.0)
                           , lineskip :: !(PDFFloat,PDFFloat,PDFFloat) -- ^ Default value (3.0,0.33,0.0)
                           , lineskiplimit :: !PDFFloat -- ^ Default value 2
                           , currentParagraphStyle :: !s
                           }

data VBox ps s = Paragraph Int [Letter s] !(Maybe ps) !BRState
               | VBox !PDFFloat !PDFFloat !PDFFloat ![VBox ps s] !(Maybe ps)
               | VGlue !PDFFloat !PDFFloat !PDFFloat !(Maybe (PDFFloat,PDFFloat)) !(Maybe ps)
               | SomeVBox !PDFFloat !BoxDimension !AnyBox !(Maybe ps)

notGlue :: VBox ps s -> Bool
notGlue (VGlue _ _ _ _ _) = False
notGlue (Paragraph _ _ _ _) = False
notGlue _ = True

vglue :: Maybe ps
     -> PDFFloat -- ^ Glue height
     -> PDFFloat -- ^ Glue dilatation factor
     -> PDFFloat -- ^ Glue compression factor
     -> PDFFloat -- ^ Glue width
     -> PDFFloat -- ^ Glue delta
     -> VBox ps s
vglue s h y z width delta = VGlue h width delta (Just(y,z)) s

instance Show (VBox ps s) where
 show (VBox _ a _ l _) = "(VBox " ++ show a ++ " " ++ show l ++ ")"
 show (VGlue a _ _ _ _) = "(VGlue " ++ show a ++ ")"
 show (Paragraph _ _ _ _) = "(Paragraph)"
 show (SomeVBox _ d t _) = "(SomeVBox " ++ show (boxHeight d) ++ " " ++ show t ++ ")"

instance MaybeGlue (VBox ps s) where
  glueSizeWithRatio (VGlue w _ _ (Just(y,z)) _) r = glueSize w y z r
  glueSizeWithRatio a _ = boxHeight a

  glueY (VGlue _ _ _ (Just(y,_)) _)  = y
  glueY _ = 0
  glueZ (VGlue _ _ _ (Just(_,z)) _)  = z
  glueZ _ = 0

instance Box (VBox ps s) where
   boxWidth (Paragraph _ _ _ _) = 0
   boxWidth (VBox w _ _ _ _) = w
   boxWidth (SomeVBox _ d _ _)  = boxWidth d
   boxWidth (VGlue _ w _ _ _)  = w

   boxHeight (Paragraph _ _ _ _) = 0
   boxHeight (VBox _ h _ _ _) = h
   boxHeight (SomeVBox _ d _ _) = boxHeight d
   boxHeight (VGlue h _ _ _ _) = h

   boxDescent (Paragraph _ _ _ _) = 0
   boxDescent (VBox _ _ d _ _) = d
   boxDescent (SomeVBox _ d _ _) = boxDescent d
   boxDescent (VGlue _ _ _ _ _) = 0

instance (ParagraphStyle ps s) => DisplayableBox (VBox ps s) where
        strokeBox (Paragraph _ _ _ _) _ _ = return ()
        strokeBox b@(VBox _ _ _ l _) x y'' = strokeVBoxes l x y'
          where
              y' = y'' - boxHeight b
        strokeBox (VGlue h w delta _ (Just style)) x y =
            if (isJust . interline $ style)
                then
                    (fromJust . interline $ style) $ Rectangle ((x+delta) :+ (y-h)) ((x+w+delta) :+ y)
                else
                   return()
        strokeBox (VGlue _ _ _ _ _) _ _ = return ()

        strokeBox (SomeVBox delta _ a _) x y = strokeBox a (x+delta) y

type Width = PDFFloat
type Height = PDFFloat

-- | Container for vboxes (x,y,width,maxheight,height,currenty,current z, tolerance para)
-- tolerance para means a paragraph is not started if too close from the bottom edge of the box
data Container ps s = Container PDFFloat PDFFloat Width PDFFloat PDFFloat PDFFloat PDFFloat PDFFloat [VBox ps s]

-- | Create a empty container to constraint the amount of line that can be displayed
mkContainer :: PDFFloat -- ^ x
            -> PDFFloat -- ^ y
            -> PDFFloat -- ^ width
            -> PDFFloat -- ^ height
            -> PDFFloat -- ^ Pargraph tolerance
            -> Container ps s -- ^ New container
mkContainer x y width height tol = Container x y width height 0 0 0 tol []

-- | Get the width of the container
containerWidth :: Container ps s -> PDFFloat
containerWidth (Container _ _ w _ _ _ _ _ _) = w

-- | Get the width of the container
containerParaTolerance :: Container ps s -> PDFFloat
containerParaTolerance (Container _ _ _ _ _ _ _ t _) = t

-- | Get the height of the container
containerHeight :: Container ps s -> PDFFloat
containerHeight (Container _ _ _ h _ _ _ _ _) = h

-- | Get the current height of the container without glue dilatation
containerCurrentHeight :: Container ps s -> PDFFloat
containerCurrentHeight (Container _ _ _ _ ch _ _ _ _) = ch

-- | Get the content height of the container with glue dilatation
containerContentHeight :: Container ps s -> PDFFloat
containerContentHeight (Container _ _ _ maxh h y z _ _) = let r = min (dilatationRatio maxh h y z) 2.0 in
 glueSize h y z r

-- | Get the minimum left border of the container content
containerContentLeftBorder :: Container ps s -> PDFFloat
containerContentLeftBorder (Container _ _ _ _ _ _ _ _ []) = 0.0
containerContentLeftBorder (Container _ _ _ _ _ _ _ _ l) = minimum . map getBoxDelta $ l

-- | Get the maximum right border of the container content (maybe bigger than container width due to overfull lines)
containerContentRightBorder :: Container ps s -> PDFFloat
containerContentRightBorder (Container _ _ _ _ _ _ _ _ []) = 0.0
containerContentRightBorder (Container _ _ _ _ _ _ _ _ l) =
 let xmax = maximum . map rightBorder $ l
     rightBorder x = getBoxDelta x + boxWidth x
 in
  xmax

-- | Container horizontal position
containerX :: Container ps s -> PDFFloat
containerX (Container x _ _ _ _ _ _ _ _) = x

-- | Container vertical position
containerY :: Container ps s -> PDFFloat
containerY (Container _ y _ _ _ _ _ _ _) = y

-- | Return the rectangle containing the text after formatting and glue dilatation
containerContentRectangle :: Container ps s -> Rectangle
containerContentRectangle c = Rectangle ((x+l) :+ (y-th)) ((x+r) :+ y)
 where
    x = containerX c
    y = containerY c
    th = containerContentHeight c
    l = containerContentLeftBorder c
    r = containerContentRightBorder c


-- | Get the required style for the interline glue
getInterlineStyle :: ComparableStyle ps => VBox ps s -> VBox ps s -> Maybe ps
getInterlineStyle (VBox _ _ _ _ (Just s)) (SomeVBox _ _ _ (Just s')) | s `isSameStyleAs` s' = Just s
                                                                     | otherwise = Nothing

getInterlineStyle (VBox _ _ _ _ (Just s)) (VBox _ _ _ _ (Just s')) |  s `isSameStyleAs`  s' = Just s
                                                                   | otherwise = Nothing

getInterlineStyle (SomeVBox _ _ _ (Just s)) (SomeVBox _ _ _ (Just s')) |  s `isSameStyleAs`  s' = Just s
                                                                       | otherwise = Nothing

getInterlineStyle (SomeVBox _ _ _ (Just s)) (VBox _ _ _ _ (Just s')) |  s `isSameStyleAs`  s' = Just s
                                                                     | otherwise = Nothing

getInterlineStyle _ _ = Nothing

-- | Interline glue required
interlineGlue :: ComparableStyle ps => VerState ps -> VBox ps s -> VBox ps s -> Maybe (VBox ps s, PDFFloat, PDFFloat)
interlineGlue settings a b | notGlue a && notGlue b =
    let p = boxDescent a
        h = boxHeight b - boxDescent b
        (ba,by,bz) = baselineskip settings
        (lw,ly,lz) = lineskip settings
        li = lineskiplimit settings
        istyle = getInterlineStyle a b
        theWidth = boxWidth a
        theDelta = getBoxDelta a
    in
    if p <= -1000
        then
            Nothing
        else
            if ba - p - h >= li
                then
                    Just $ (vglue istyle (ba-p-h) by bz theWidth theDelta,by,bz)
                else
                    Just $ (vglue istyle lw ly lz theWidth theDelta,ly,lz)
                                  | otherwise = Nothing

addTo :: ComparableStyle ps => VerState ps -> VBox ps s -> Container ps s -> Container ps s
addTo _ line (Container px py w maxh h y z t []) = Container px py w maxh ((boxHeight line)+h) y z t [line]
addTo settings line (Container px py w maxh h y z t l@(a:_)) =
    case interlineGlue settings a line of
        Nothing ->
          let h' = boxHeight line + h
              y' = y + glueY line
              z' = z + glueZ line
          in
          Container px py w maxh h' y' z' t (line:l)
        Just (v,ny,nz) ->
          let h' = boxHeight line + h + boxHeight v
              y' = y + ny + glueY line
              z' = z + nz + glueZ line
          in
          Container px py w maxh h' y' z' t (line:v:l)

isOverfull :: Container ps s -> Bool
isOverfull (Container _ _ _ maxh h y z _ _) = let r = dilatationRatio maxh h y z
 in
   if r >= bigAdjustRatio then h > maxh else r <= -1



-- | Paragraph style
class (ComparableStyle a, Style s) => ParagraphStyle a s | a -> s where
    -- | Width of the line of the paragraph
    lineWidth :: a -- ^ The style
               -> PDFFloat -- ^ Width of the text area used by the typesetting algorithm
               -> Int -- ^ Line number
               -> PDFFloat -- ^ Line width

    -- | Horizontal shift of the line position relatively to the left egde of the paragraph bounding box
    linePosition :: a -- ^ The style 
                  -> PDFFloat -- ^ Width of the text area used by the typesetting algorithm
                  -> Int -- ^ Line number 
                  -> PDFFloat -- ^ Horizontal offset from the left edge of the text area

    -- | How to style the interline glues added in a paragraph by the line breaking algorithm
    interline :: a -- ^ The style 
              -> Maybe (Rectangle -> Draw ()) -- ^ Function used to style interline glues
    interline _ = Nothing
    lineWidth _ w _ = w
    linePosition _ _ = const 0.0

    -- | Change the content of a paragraph before the line breaking algorithm is run. It may also change the style
    paragraphChange :: a -- ^ The style
               -> Int -- ^ Line offset different from 0 when a paragraph has been broken
               -> [Letter s] -- ^ List of letters in the paragraph
               -> (a,[Letter s]) -- ^ Update style and list of letters
    paragraphChange a _ l = (a,l)

    -- | Get the paragraph bounding box and the paragraph draw command to apply additional effects
    paragraphStyle :: a -- ^ The style 
                   -> Maybe (Rectangle -> Draw b -> Draw ()) -- ^ Function used to style a paragraph
    paragraphStyle _ = Nothing


-- | Get the delta used to position a box with non rectangular shapes
getBoxDelta :: VBox ps s -> PDFFloat
getBoxDelta (Paragraph _ _ _ _) = 0.0
getBoxDelta (VBox _ _ _ _ _) = 0.0
getBoxDelta (VGlue _ _ delta _ _) = delta
getBoxDelta (SomeVBox delta _ _ _) = delta




isSameParaStyle :: ComparableStyle ps => ps -> VBox ps s -> Bool
isSameParaStyle s (Paragraph _ _ (Just s') _) =  s `isSameStyleAs`  s'
isSameParaStyle s (VBox _ _ _ _ (Just s')) =  s `isSameStyleAs`  s'
isSameParaStyle s (VGlue _ _ _ _ (Just s')) =  s `isSameStyleAs`  s'
isSameParaStyle s (SomeVBox _ _ _ (Just s'))  =  s `isSameStyleAs`  s'
isSameParaStyle _ _ = False

recurseStrokeVBoxes :: (ParagraphStyle ps s) => Int -> [VBox ps s] -> PDFFloat -> PDFFloat -> Draw ()
recurseStrokeVBoxes _ [] _ _  = return ()
recurseStrokeVBoxes _ (Paragraph _ _ _ _:_) _ _ = return ()
recurseStrokeVBoxes nb (a@(VGlue _ _ _ _ _):l) xa y  = do
    let h = boxHeight a
    strokeBox a xa y
    recurseStrokeVBoxes nb l xa (y-h)

recurseStrokeVBoxes nb (a:l) xa y = do
    let h = boxHeight a
    strokeBox a xa y
    recurseStrokeVBoxes (nb+1) l xa (y-h)

drawWithParaStyle :: (ParagraphStyle ps s) => ps -> [VBox ps s] -> PDFFloat -> PDFFloat -> Draw ()
drawWithParaStyle style b xa y' = do
    let  (l',l'') = span (isSameParaStyle style) b
         h' = foldl' (\x' ny -> x' + boxHeight ny) 0.0 l'
    if (isJust . paragraphStyle $ style)
      then do
          let xleft = (minimum $ 100000:map getBoxDelta l' ) + xa
              xright = (maximum $ 0:(map (\x -> boxWidth x + getBoxDelta x) l')) + xa
          (fromJust . paragraphStyle $ style) (Rectangle (xleft :+ (y'- h')) (xright :+ y')) (recurseStrokeVBoxes 1 l' xa y')
      else
         recurseStrokeVBoxes 1 l' xa y'
    strokeVBoxes l'' xa (y' - h')

-- | Stroke the VBoxes
strokeVBoxes :: (ParagraphStyle ps s) => [VBox ps s] -- ^ List of boxes
             -> PDFFloat -- ^ X pos
             -> PDFFloat -- ^ Y pos
             -> Draw ()
strokeVBoxes [] _ _ = return ()
strokeVBoxes b@((Paragraph _ _ (Just s') _):_) xa y = drawWithParaStyle s' b xa y
strokeVBoxes b@((VBox _ _ _ _ (Just s')):_) xa y = drawWithParaStyle s' b xa y
strokeVBoxes b@((VGlue _ _ _ _ (Just s')):_) xa y = drawWithParaStyle s' b xa y
strokeVBoxes b@((SomeVBox _ _ _ (Just s')):_) xa y = drawWithParaStyle s' b xa y
strokeVBoxes (a:l) xa y =
    do
        let h = boxHeight a
        strokeBox a xa y
        strokeVBoxes l xa (y-h)