{-# LANGUAGE TupleSections #-}
{-# LANGUAGE CPP #-}
module Graphics.Rasterific.Svg.RasterificTextRendering
        ( renderText ) where

#if !MIN_VERSION_base(4,8,0)
import Control.Applicative( (<*>), (<$>) )
import Data.Monoid( mappend, mempty )
#endif

import Control.Monad( foldM )
import Control.Monad.IO.Class( liftIO )
import Control.Monad.Identity( Identity )
import Control.Monad.Trans.State.Strict( execState
                                       , StateT
                                       , modify
                                       , gets )
import Control.Applicative( (<|>) )
import Control.Lens( at, (?=) )
import qualified Control.Lens as L
import Codec.Picture( PixelRGBA8( .. ) )
import qualified Data.Foldable as F
import Data.Monoid( (<>), Last( .. ), First( .. ) )
import Data.Maybe( fromMaybe )
import qualified Data.Text as T
import Graphics.Rasterific.Linear( (^+^), (^-^) )
import Graphics.Rasterific hiding ( Path, Line, Texture, transform )
import qualified Graphics.Rasterific as R
import qualified Graphics.Rasterific.Outline as RO
import Graphics.Rasterific.Immediate
import qualified Graphics.Rasterific.Transformations as RT
import Graphics.Rasterific.PathWalker
import Graphics.Text.TrueType
import Graphics.Svg.Types
import Graphics.Rasterific.Svg.RenderContext
import Graphics.Rasterific.Svg.PathConverter
{-import Graphics.Svg.XmlParser-}

{-import Debug.Trace-}
{-import Text.Printf-}

loadFont :: FilePath -> IODraw (Maybe Font)
loadFont fontPath = do
  loaded <- L.use $ loadedFonts . at fontPath
  case loaded of
    Just v -> return $ Just v
    Nothing -> do
      file <- liftIO $ loadFontFile fontPath
      case file of
        Left _ -> return Nothing
        Right f -> do
          loadedFonts . at fontPath ?= f
          return $ Just f

data RenderableString px = RenderableString
    { _renderableAttributes :: !DrawAttributes
    , _renderableSize       :: !Float
    , _renderableFont       :: !Font
    , _renderableString     :: ![(Char, CharInfo px)]
    }

data CharInfo px = CharInfo
  { _charX  :: Maybe Number
  , _charY  :: Maybe Number
  , _charDx :: Maybe Number
  , _charDy :: Maybe Number
  , _charRotate :: Maybe Float
  , _charStroke :: Maybe (Float, R.Texture px, R.Join, (R.Cap, R.Cap))
  }

emptyCharInfo :: CharInfo px
emptyCharInfo = CharInfo
  { _charX      = Nothing
  , _charY      = Nothing
  , _charDx     = Nothing
  , _charDy     = Nothing
  , _charRotate = Nothing
  , _charStroke = Nothing
  }

propagateTextInfo :: TextInfo -> TextInfo -> TextInfo
propagateTextInfo parent current = TextInfo
  { _textInfoX = combine _textInfoX
  , _textInfoY = combine _textInfoY
  , _textInfoDX = combine _textInfoDX
  , _textInfoDY = combine _textInfoDY
  , _textInfoRotate = combine _textInfoRotate
  , _textInfoLength = _textInfoLength current
  }
  where
    combine f = case f current of
      [] -> f parent
      lst -> lst

textInfoRests :: TextInfo -> TextInfo -> TextInfo
              -> TextInfo
textInfoRests this parent sub = TextInfo
    { _textInfoX      = decideWith _textInfoX
    , _textInfoY      = decideWith _textInfoY
    , _textInfoDX     = decideWith _textInfoDX
    , _textInfoDY     = decideWith _textInfoDY
    , _textInfoRotate = decideWith _textInfoRotate
    , _textInfoLength = _textInfoLength parent
    }
  where
    decideWith f = decide (f this) (f parent) (f sub)

    decide []   _ ssub = ssub
    decide  _ top    _ = top

unconsTextInfo :: RenderContext -> DrawAttributes -> TextInfo
               -> IODraw (CharInfo PixelRGBA8, TextInfo)
unconsTextInfo ctxt attr nfo = do
  texture <- textureOf ctxt attr _strokeColor _strokeOpacity
  return (charInfo texture, restText)
 where
  unconsInf lst = case lst of
     []     -> (Nothing, [])
     (x:xs) -> (Just x, xs)

  (xC, xRest) = unconsInf $ _textInfoX nfo
  (yC, yRest) = unconsInf $ _textInfoY nfo
  (dxC, dxRest) = unconsInf $ _textInfoDX nfo
  (dyC, dyRest) = unconsInf $ _textInfoDY nfo
  (rotateC, rotateRest) = unconsInf $ _textInfoRotate nfo

  restText = TextInfo
    { _textInfoX      = xRest
    , _textInfoY      = yRest
    , _textInfoDX     = dxRest
    , _textInfoDY     = dyRest
    , _textInfoRotate = rotateRest
    , _textInfoLength = _textInfoLength nfo
    }

  sWidth =
     lineariseLength ctxt attr <$> getLast (_strokeWidth attr)

  charInfo tex = CharInfo
    { _charX = xC
    , _charY = yC
    , _charDx = dxC
    , _charDy = dyC
    , _charRotate = realToFrac <$> rotateC
    , _charStroke =
        (,, joinOfSvg attr, capOfSvg attr) <$> sWidth <*> tex
    }

repeatLast :: [a] -> [a]
repeatLast = go where
  go lst = case lst of
    [] -> []
    [x] -> repeat x
    (x:xs) -> x : go xs

infinitizeTextInfo :: TextInfo -> TextInfo
infinitizeTextInfo nfo =
    nfo { _textInfoRotate = repeatLast $ _textInfoRotate nfo }


-- | Monadic version of mapAccumL

mapAccumLM :: Monad m
            => (acc -> x -> m (acc, y)) -- ^ combining funcction

            -> acc                      -- ^ initial state

            -> [x]                      -- ^ inputs

            -> m (acc, [y])             -- ^ final state, outputs

mapAccumLM _ s []     = return (s, [])
mapAccumLM f s (x:xs) = do
    (s1, x')  <- f s x
    (s2, xs') <- mapAccumLM f s1 xs
    return    (s2, x' : xs')

mixWithRenderInfo :: RenderContext -> DrawAttributes
                  -> TextInfo -> String
                  -> IODraw (TextInfo, [(Char, CharInfo PixelRGBA8)])
mixWithRenderInfo ctxt attr = mapAccumLM go where
  go info c = do
    (thisInfo, rest) <- unconsTextInfo ctxt attr info
    return (rest, (c, thisInfo))


data LetterTransformerState = LetterTransformerState
    { _charactersInfos      :: ![CharInfo PixelRGBA8]
    , _characterCurrent     :: !(CharInfo PixelRGBA8)
    , _currentCharDelta     :: !R.Point
    , _currentAbsoluteDelta :: !R.Point
    , _currentDrawing       :: Drawing PixelRGBA8 ()
    , _stringBounds         :: !PlaneBound
    }

type GlyphPlacer = StateT LetterTransformerState Identity

unconsCurrentLetter :: GlyphPlacer ()
unconsCurrentLetter = modify $ \s ->
  case _charactersInfos s of
    [] -> s
    (x:xs) -> s { _charactersInfos = xs
                , _characterCurrent = x
                }

prepareCharRotation :: CharInfo px -> R.PlaneBound -> RT.Transformation
prepareCharRotation info bounds = case _charRotate info of
  Nothing -> mempty
  Just angle -> RT.rotateCenter (toRadian angle) lowerLeftCorner
      where
        lowerLeftCorner = boundLowerLeftCorner bounds

prepareCharTranslation :: RenderContext -> CharInfo px -> R.PlaneBound
                       -> R.Point -> R.Point
                       -> (R.Point, R.Point, RT.Transformation)
prepareCharTranslation ctxt info bounds prevDelta prevAbsolute = go where
  lowerLeftCorner = boundLowerLeftCorner bounds
  toRPoint a b = linearisePoint ctxt mempty (a, b)
  mzero = Just $ Num 0
  V2 pmx pmy = Just . Num . realToFrac <$> prevAbsolute

  mayForcedPoint = case (_charX info, _charY info) of
    (Nothing, Nothing) -> Nothing
    (mx, my) -> toRPoint <$> (mx <|> pmx) <*> (my <|> pmy)

  delta = fromMaybe 0 $
    toRPoint <$> (_charDx info <|> mzero)
             <*> (_charDy info <|> mzero)

  go = case mayForcedPoint of
    Nothing ->
      let newDelta = prevDelta ^+^ delta
          trans = RT.translate $ newDelta ^+^ prevAbsolute in
      (newDelta, prevAbsolute, trans)

    Just p ->
      let newDelta = prevDelta ^+^ delta
          positionDelta = (realToFrac <$> p) ^-^ lowerLeftCorner
          trans = RT.translate $ positionDelta ^+^ newDelta in
      (newDelta, positionDelta, trans)

transformPlaceGlyph :: RenderContext
                    -> RT.Transformation
                    -> R.PlaneBound
                    -> DrawOrder PixelRGBA8
                    -> GlyphPlacer ()
transformPlaceGlyph ctxt pathTransformation bounds order = do
  unconsCurrentLetter
  info <- gets _characterCurrent
  delta <- gets _currentCharDelta
  absoluteDelta <- gets _currentAbsoluteDelta
  let rotateTrans = prepareCharRotation info bounds
      (newDelta, newAbsolute, placement) =
        prepareCharTranslation ctxt info bounds delta absoluteDelta
      finalTrans = pathTransformation <> placement <> rotateTrans
      newGeometry =
          R.transform (RT.applyTransformation finalTrans) $ _orderPrimitives order
      newOrder = order { _orderPrimitives = newGeometry }


      stroking Nothing = return ()
      stroking (Just (w, texture, rjoin, cap)) =
          orderToDrawing $ newOrder {
            _orderPrimitives = stroker <$> _orderPrimitives newOrder,
            _orderTexture = texture
          }
         where
           stroker = RO.strokize w rjoin cap

  modify $ \s -> s
    { _currentCharDelta = newDelta
    , _currentAbsoluteDelta = newAbsolute
    , _stringBounds = _stringBounds s <> bounds
    , _currentDrawing = do
        _currentDrawing s
        orderToDrawing newOrder
        stroking $ _charStroke info
    }

prepareFontFamilies :: DrawAttributes -> [String]
prepareFontFamilies = (++ defaultFont)
                    . fmap replaceDefault
                    . fromMaybe []
                    . getLast
                    . _fontFamily
  where
    defaultFont = ["Arial"]
    -- using "safe" web font, hoping they are present on

    -- the system.

    replaceDefault s = case s of
      "monospace" -> "Courier New"
      "sans-serif" -> "Arial"
      "serif" -> "Times New Roman"
      _ -> s

fontOfAttributes :: FontCache -> DrawAttributes -> IODraw (Maybe Font)
fontOfAttributes fontCache attr = case fontFilename of
  Nothing -> return Nothing
  Just fn -> loadFont fn
  where
    fontFilename =
      getFirst . F.foldMap fontFinder $ prepareFontFamilies attr
    noStyle = FontStyle
            { _fontStyleBold = False
            , _fontStyleItalic = False }

    italic = noStyle { _fontStyleItalic = True }

    style = case getLast $ _fontStyle attr of
      Nothing -> noStyle
      Just FontStyleNormal -> noStyle
      Just FontStyleItalic -> italic
      Just FontStyleOblique -> italic

    fontFinder ff =
         First $ findFontInCache fontCache descriptor
      where descriptor = FontDescriptor
                 { _descriptorFamilyName = T.pack ff
                 , _descriptorStyle = style }


prepareRenderableString :: RenderContext -> DrawAttributes -> Text
                        -> IODraw [RenderableString PixelRGBA8]
prepareRenderableString ctxt ini_attr root =
    fst <$> everySpan ini_attr mempty (_textRoot root) where

  everySpan attr originalInfo tspan =
      foldM (everyContent subAttr) (mempty, nfo) $ _spanContent tspan
    where
      subAttr = attr <> _spanDrawAttributes tspan
      nfo = propagateTextInfo originalInfo
          . infinitizeTextInfo
          $ _spanInfo tspan

  everyContent _attr (acc, info) (SpanTextRef _) = return (acc, info)
  everyContent attr (acc, info) (SpanSub thisSpan) = do
      let thisTextInfo = _spanInfo thisSpan
      (drawn, newInfo) <- everySpan attr info thisSpan
      return (acc <> drawn, textInfoRests thisTextInfo info newInfo)
  everyContent attr (acc, info) (SpanText txt) = do
    font <- fontOfAttributes (_fontCache ctxt) attr
    case font of
      Nothing -> return (acc, info)
      Just f -> do
        (info', str) <- mixWithRenderInfo ctxt attr info $ T.unpack txt
        let finalStr = RenderableString attr size f str
        return (acc <> [finalStr], info')

     where
       size = case getLast $ _fontSize attr of
          Just v -> lineariseLength ctxt attr v
          Nothing -> 16


anchorStringRendering :: TextAnchor -> LetterTransformerState
                      -> Drawing PixelRGBA8 ()
anchorStringRendering anchor st = case anchor of
    TextAnchorStart -> _currentDrawing st
    TextAnchorMiddle ->
        withTransformation (RT.translate (V2 (negate $ stringWidth / 2) 0)) $
            _currentDrawing st
    TextAnchorEnd ->
        withTransformation (RT.translate (V2 (- stringWidth) 0)) $ _currentDrawing st
  where
    stringWidth = boundWidth $ _stringBounds st

notWhiteSpace :: (Char, a) -> Bool
notWhiteSpace (c, _) = c /= ' ' && c /= '\t'

initialLetterTransformerState :: [RenderableString PixelRGBA8] -> LetterTransformerState
initialLetterTransformerState str = LetterTransformerState
  { _charactersInfos   =
      fmap snd . filter notWhiteSpace . concat $ _renderableString <$> str
  , _characterCurrent  = emptyCharInfo
  , _currentCharDelta  = V2 0 0
  , _currentAbsoluteDelta = V2 0 0
  , _currentDrawing    = mempty
  , _stringBounds = mempty
  }

executePlacer :: Monad m => PathDrawer m px -> [DrawOrder px] -> m ()
executePlacer placer = F.mapM_ exec where
  exec order | bounds == mempty = return ()
             | otherwise = placer mempty bounds order
    where
      bounds = F.foldMap (F.foldMap planeBounds)
             $ _orderPrimitives order

textureOf :: RenderContext
          -> DrawAttributes
          -> (DrawAttributes -> Last Texture)
          -> (DrawAttributes -> Maybe Float)
          -> IODraw (Maybe (R.Texture PixelRGBA8))
textureOf ctxt attr colorAccessor opacityAccessor =
  case getLast $ colorAccessor attr of
    Nothing -> return Nothing
    Just svgTexture ->
        prepareTexture ctxt attr svgTexture opacity []
      where opacity = fromMaybe 1.0 $ opacityAccessor attr

renderString :: RenderContext -> Maybe (Float, R.Path) -> TextAnchor
             -> [RenderableString PixelRGBA8]
             -> IODraw (Drawing PixelRGBA8 ())
renderString ctxt mayPath anchor str = do
  textRanges <- mapM toFillTextRange str

  case mayPath of
    Just (offset, tPath) ->
        return . pathPlacer offset tPath $ fillOrders textRanges
    Nothing -> return . linePlacer $ fillOrders textRanges
  where
    fillOrders =
      drawOrdersOfDrawing swidth sheight (_renderDpi ctxt) background
        . printTextRanges 0

    pixelToPt s = pixelSizeInPointAtDpi s $ _renderDpi ctxt
    (mini, maxi) = _renderViewBox ctxt
    V2 swidth sheight = floor <$> (maxi ^-^ mini)
    background = PixelRGBA8 0 0 0 0

    pathPlacer offset tPath =
        anchorStringRendering anchor
            . flip execState (initialLetterTransformerState str)
            . drawOrdersOnPath (transformPlaceGlyph ctxt) offset 0 tPath

    linePlacer =
        anchorStringRendering anchor
            . flip execState (initialLetterTransformerState str)
            . executePlacer (transformPlaceGlyph ctxt)

    toFillTextRange renderable = do
      mayTexture <- textureOf ctxt (_renderableAttributes renderable)
                        _fillColor _fillOpacity
      return TextRange
        { _textFont = _renderableFont renderable
        , _textSize = pixelToPt $ _renderableSize renderable
        , _text     = fst <$> _renderableString renderable
        , _textTexture = mayTexture
        }

startOffsetOfPath :: RenderContext -> DrawAttributes -> R.Path -> Number
                  -> Float
startOffsetOfPath _ _ _ (Num i) = realToFrac i
startOffsetOfPath _ attr _ (Em i) = emTransform attr $ realToFrac i
startOffsetOfPath _ _ tPath (Percent p) =
    realToFrac p * RO.approximatePathLength tPath
startOffsetOfPath ctxt attr tPath num =
    startOffsetOfPath ctxt attr tPath $ stripUnits ctxt num

renderText :: RenderContext
           -> DrawAttributes
           -> Maybe TextPath
           -> Text
           -> IODraw (Drawing PixelRGBA8 ())
renderText ctxt attr ppath stext =
  prepareRenderableString ctxt attr stext >>= renderString ctxt pathInfo anchor
  where
    renderPath =
      svgPathToRasterificPath False . _textPathData <$> ppath

    offset = do
      rpath <- renderPath
      mayOffset <- _textPathStartOffset <$> ppath
      return $ startOffsetOfPath ctxt attr rpath mayOffset

    pathInfo = (,) <$> (offset <|> return 0) <*> renderPath

    anchor = fromMaybe TextAnchorStart
           . getLast
           . _textAnchor
           . mappend attr
           . _spanDrawAttributes $ _textRoot stext