-- | A simple layout engine to render text (and later also mathematics).

{-# LANGUAGE BangPatterns, ScopedTypeVariables #-}
module Graphics.Rendering.MiniTypeset.Layout where

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

import Control.Monad
import Data.IORef

import qualified Data.Map as Map ; import Data.Map (Map)

import GHC.Float

import Graphics.Rendering.TrueType.STB

import Graphics.Rendering.OpenGL as GL hiding ( Height , translate )

import Graphics.Rendering.MiniTypeset.Common
import Graphics.Rendering.MiniTypeset.Box
import Graphics.Rendering.MiniTypeset.Document
import Graphics.Rendering.MiniTypeset.FontTexture
import Graphics.Rendering.MiniTypeset.MultiFont
import Graphics.Rendering.MiniTypeset.Render

--------------------------------------------------------------------------------
-- * Relative position and size constants (mathematical coordinate system!)

subSupSize =  0.65 :: Double          -- Subscript \/ superscript relative sizes
supPos     =  0.40 :: Double          -- Superscript relative position
subPos     = -0.20 :: Double          -- Subscript relative position

underlinePos     = -0.10 :: Double      -- Underline relative position
overlinePos      =  0.10 :: Double      -- Overline relative position
strikeThroughPos = 0.48  :: Double      -- Strike-through relative position
horizLineWidth   = 0.045 :: Double      -- Text decoration line width

abovePos  =  0.15 :: Double
belowPos  = -0.15 :: Double

--------------------------------------------------------------------------------
-- * Subscript size indexing

-- | 0 is the default size, 1 is smaller, 2 is even smaller, etc (each subscript 
newtype SizeIndex
  = SizeIndex Int
  deriving (Eq,Ord,Show)

succSizeIndex :: SizeIndex -> SizeIndex
succSizeIndex (SizeIndex n) = SizeIndex (n+1)

calculateSizeHeight :: Height -> SizeIndex -> Int
calculateSizeHeight (Height height) = go where
  go (SizeIndex n)
    | n == 0  = height
    | n == 1  = round (fromIntegral height * subSupSize)
    | n == 2  = round (fromIntegral height * subSupSize * subSupSize)
    | n == 3  = round (fromIntegral height * subSupSize * subSupSize * 0.8)
    | n >  3  = go (SizeIndex 3)

--------------------------------------------------------------------------------
-- * The layout data type

-- | This data type is the output of the layout engine. The ``identifying'' part 
-- is retained, because everything is still relative, and only during the rendering
-- will positions become absolute. See 'dryrunLayout'
data Layout ident style
  = LoutGlyph !Pos !style !Col !Char !MultiFontGlyph
  | LoutGroup !Box   [Layout ident style]
  | LoutBox   !Box   (Layout ident style)
  | LoutOfs   !Pos   (Layout ident style)
  | LoutIdent !ident (Layout ident style)
  | LoutDecor !LayoutDecoration (Layout ident style)
  | LoutEmpty
  deriving (Show)

data LayoutDecoration
  = HorizLine
      { _hlineCol       :: !Col
      , _hlineVAlign    :: !VAlign
      , _hlineVPos      :: !Double
      , _hlineLineWidth :: !Double
      }
  deriving (Show)

instance Translate (Layout ident style) where
  translate = translateLayout

translateLayout :: Pos -> Layout ident style -> Layout ident style
translateLayout ofs layout = case layout of
  LoutGlyph ofs0 sty col ch mfg -> LoutGlyph (ofs0+ofs) sty col ch mfg   -- minor optimization
  LoutOfs   ofs0 lout           -> LoutOfs   (ofs0+ofs) lout             -- minor optimization
  _                             -> LoutOfs   ofs layout

reboxLayout :: Box -> Layout ident style -> Layout ident style
reboxLayout box layout = case layout of
  LoutGroup _ louts -> LoutGroup box louts
  LoutBox   _ lout  -> LoutBox   box lout
  _                 -> LoutBox   box layout

--------------------------------------------------------------------------------
-- * The box of a glyph

mfgRelBox :: MultiFontGlyph -> Box
mfgRelBox (MFG ftex bufloc) = Box (width,height) outer bound gap where
  vm  = _ftexVM ftex
  hm  = _locHM  bufloc

  top     = - float2Double (ascent  vm)          -- ascent is normally positive
  bottom  = - float2Double (descent vm)          -- descent is normally negative
  lgap    = float2Double (lineGap vm) * lgapfactor
  width   = float2Double (advanceWidth hm)
  height  = 0
  lgapfactor = _ftexLGapFactor ftex

  outer = Quad (0,top) (width,bottom)
  gap   = Quad (0,top) (width,bottom+lgap)

  bound = Quad (fi ofsx , fi ofsy) (fi (ofsx+sizx) , fi (ofsy+sizy))

  (ofsx,ofsy) = _locBufOfs bufloc
  (sizx,sizy) = _locBufSiz bufloc

  fi = fromIntegral :: Int -> Double

--------------------------------------------------------------------------------
-- * Rendering a layout

-- | Renders the layout to the OpenGL framebuffer.
--
-- Note: you should set up the OpenGL coordinate transformation matrices
-- so that the coordinate system is screen-space, measured in pixels. For
-- example something like
--
-- > matrixMode $= Projection
-- > loadIdentity
-- > ortho 0 xres yres 0 (-1) 1 
-- > matrixMode $= Modelview 0
-- > loadIdentity
--
-- should do.
--
renderLayout :: Ord ident => Layout ident style -> Pos -> IO ()
renderLayout lout pos = void (renderLayout' False lout pos)

-- | Does not actually render, but computes the bounding boxes of the identified 
-- parts of the layout.
dryrunLayout :: Ord ident => Layout ident style -> Pos -> IO (Map ident AbsBox)
dryrunLayout lout pos = renderLayout' True lout pos

renderLayout'
  :: forall ident style. Ord ident
  => Bool                     -- ^ @True@ = dryrun (do not render); @False@ = do the rendering
  -> Layout ident style
  -> Pos
  -> IO (Map ident AbsBox )
renderLayout' dryrun layout pos0 =
  do
    table <- newIORef Map.empty
    _ <- go table pos0 layout
    readIORef table

  where
    go :: IORef (Map ident AbsBox) -> Pos -> Layout ident style -> IO AbsBox
    go !table !pos layout = case layout of

      LoutGlyph ofs _ col _ mfg -> do
        unless dryrun $ renderMFG (pos+ofs) col mfg
        return (AbsBox pos $ mfgRelBox mfg)

      LoutDecor decor lout -> do
        absbox <- go table pos lout
        unless dryrun $ renderLayoutDecoration absbox decor
        return absbox

      LoutGroup relbox louts -> do
        mapM_ (go table pos) louts
        return (AbsBox pos relbox)

      LoutBox relbox lout -> do
        go table pos lout
        return (AbsBox pos relbox)

      LoutOfs ofs lout -> go table (pos + ofs) lout

      LoutIdent ident lout -> do
        box <- go table pos lout
        modifyIORef table (Map.insert ident box)
        return box

      LoutEmpty -> return (AbsBox pos emptyBox)

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

renderLayoutDecoration :: AbsBox -> LayoutDecoration -> IO ()
renderLayoutDecoration absbox decor = case decor of

  HorizLine col valign vpos lwidth -> do
    let Quad (x1,y1) (x2,y2) = absboxBoundingQuad absbox
    let y = case valign of
              AlignTop    -> y1 - vpos
              AlignBottom -> y2 - vpos
    renderLine col (max 1 lwidth) (Pos x1 y) (Pos x2 y)

--------------------------------------------------------------------------------
-- * Creating a layouting

-- | Creates a layout from a document. Then you can render the resulting layout
-- with 'renderLayout'
createLayout
  :: forall fontfile style ident. (Ord fontfile, Ord ident)
  => MultiFont fontfile style
  -> Height
  -> Document ident
  -> IO (Layout ident style)
createLayout multifont height doc = do
  (box0,lout0) <- createLayout' multifont height doc
  -- The layout origin is not the top-left corner, but the baseline of the first line.
  -- Normally you want to shift it down so that the first line is visible, too.
  let outer@(Quad (l,t) _) = boxOuterQuad box0
  let ofs = Pos (-l) (-t)
  return $ translate ofs lout0

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

-- | A type used by 'createLayout'
data Cfg = Cfg
  { _currentSize  :: !SizeIndex
  , _currentStyle :: !BasicStyle
  , _currentColor :: !Col
  }

defaultCfg :: Cfg
defaultCfg = Cfg
  { _currentSize  = SizeIndex 0
  , _currentStyle = Regular
  , _currentColor = Col 0 0 0
  }

-- Note: the layout origin is not the top-left corner, but the baseline of the first line.
-- The function 'createLayout' does the necessary shift for you.
createLayout'
  :: forall fontfile style ident. (Ord fontfile, Ord ident)
  => MultiFont fontfile style
  -> Height
  -> Document ident
  -> IO (Box, Layout ident style)
createLayout' multifont height doc = go initialCfg doc where

  styleMap =  (_ufcStyleMap . _mfUserConfig) multifont

  initialCfg = defaultCfg

  sizeHeight :: SizeIndex -> Int
  sizeHeight = calculateSizeHeight height

  fsizeHeight :: SizeIndex -> Double
  fsizeHeight = fromIntegral . sizeHeight

  relativePlacement :: ([Box] -> (Box,[AbsBox])) -> Cfg -> [Document ident] -> IO (Box, Layout ident style)
  relativePlacement place !cfg docs  = case docs of
    []  -> return (emptyBox, LoutEmpty)
    [d] -> go cfg d
    _   -> do
      bls <- mapM (go cfg) docs
      let (boxes,louts) = unzip bls
          (box,aboxes)  = place boxes
          offsets       = map _aboxOffset aboxes
      return (box, LoutGroup box (zipWith translate offsets louts))

  go :: Cfg -> Document ident -> IO (Box, Layout ident style)
  go cfg doc = case doc of

    EmptyDoc           -> return (emptyBox, LoutEmpty)

    WithColor  col doc -> go cfg' doc where cfg' = cfg { _currentColor = col }
    WithStyle  sty doc -> go cfg' doc where cfg' = cfg { _currentStyle = sty }

    Decorated decor doc -> do
      (box,lout) <- go cfg doc
      let col    =              _currentColor cfg
          ht     = fsizeHeight (_currentSize  cfg)
          lwidth = horizLineWidth * ht
          ldecor = case decor of
            Underline     -> HorizLine col AlignBottom (underlinePos     * ht) lwidth
            Overline      -> HorizLine col AlignTop    (overlinePos      * ht) lwidth
            StrikeThrough -> HorizLine col AlignBottom (strikeThroughPos * ht) lwidth
      return (box, LoutDecor ldecor lout)

    SubSupScript subsup doc -> do
      let (doc1,doc2) = subSupDocs subsup
      (box0,lout0) <- go cfg  doc
      let cfg' = cfg { _currentSize = succSizeIndex (_currentSize cfg) }
      (box1,lout1) <- go cfg' doc1
      (box2,lout2) <- go cfg' doc2
      let ht     = fsizeHeight (_currentSize  cfg)
          ofs1   = - subPos * ht
          ofs2   = - supPos * ht
      let (box,(pos1,pos2)) = subSupScriptBox box0 (ofs1,box1) (ofs2,box2)
          lout = LoutGroup box [ lout0, translate pos1 lout1 , translate pos2 lout2 ]
      return (box,lout)

    AboveBelow aboveBelow doc -> do
      let (doc1,doc2) = aboveBelowDocs aboveBelow
      (box0,lout0) <- go cfg  doc
      let cfg' = cfg { _currentSize = succSizeIndex (_currentSize cfg) }
      (box1,lout1) <- go cfg' doc1
      (box2,lout2) <- go cfg' doc2
      let ht     = fsizeHeight (_currentSize  cfg)
          ofs1   = Pos 0 (- abovePos * ht)
          ofs2   = Pos 0 (- belowPos * ht)
      let (box,(pos1,pos2)) = aboveBelowBox box0 (ofs1,box1) (ofs2,box2)
          lout = LoutGroup box [ lout0, translate pos1 lout1 , translate pos2 lout2 ]
      return (box,lout)

    Realign which doc -> do
      (box , lout) <- go cfg doc
      let box' = realignBox which box
      return (box', reboxLayout box' lout)

    AddMargin margin doc -> do
      (Box siz outer bound gap , lout) <- go cfg doc
      let outer' = marginQuad margin outer      -- should we also extend the outer box or not??
          gap'   = marginQuad margin gap
          box' = Box siz outer' bound gap'
      return (box' , reboxLayout box' lout)

    Identified uid doc -> do
      (box, lout) <- go cfg doc
      return (box, LoutIdent uid lout)

    Symbol char -> do
      let Cfg size style0 col = cfg
          style = styleMap style0
      mfg@(MFG ftex bufloc) <- lkpMultiFont multifont (sizeHeight size) style char
      let relbox = mfgRelBox mfg
          lout = LoutGlyph (Pos 0 0) style col char mfg
      return (relbox, lout)

    Space -> go cfg (Symbol ' ')

    String chars -> go cfg (HorzCat AlignBottom $ map Symbol chars)

    HorzCat valign  docs -> relativePlacement (hcatBoxes   valign) cfg docs
    VertCat halign  docs -> relativePlacement (vcatBoxes   halign) cfg docs
    Overlay (ha,va) docs -> relativePlacement (overlayBoxes ha va) cfg docs

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