-- | Contains the SDL implementation 2D graphics rendering implementation.
--
-- The SDL engine uses Cairo for its 2D vector graphics, which is hardware accelerated
-- and generally pretty fast.
module Helm.Engine.SDL.Graphics2D (render) where

import           Data.Foldable (forM_)
import           Foreign.C.Types (CInt)
import           Foreign.Ptr (castPtr)

import qualified Data.Text as T
import qualified Graphics.Rendering.Cairo as Cairo
import qualified Graphics.Rendering.Pango as Pango
import           Graphics.Rendering.Cairo.Matrix (Matrix(..))
import           Linear.V2 (V2(V2))
import           Linear.V3 (V3(V3))
import qualified SDL.Video.Renderer as Renderer

import           Helm.Color (Color(..), Gradient(..))
import           Helm.Engine.SDL.Asset (Image(..))
import           Helm.Engine.SDL.Engine (SDLEngine)
import           Helm.Graphics2D
import           Helm.Graphics2D.Text

-- | Render a 2D element to an SDL texture (with a width and height).
render :: Renderer.Texture -> V2 CInt -> Collage SDLEngine -> IO ()
render tex (V2 w h) coll = do
  (pixels, pitch) <- Renderer.lockTexture tex Nothing

  Cairo.withImageSurfaceForData (castPtr pixels) Cairo.FormatARGB32 (fromIntegral w) (fromIntegral h) (fromIntegral pitch) $ \surface ->
    Cairo.renderWith surface $ do
      Cairo.setSourceRGB 0 0 0
      Cairo.rectangle 0 0 (fromIntegral w) (fromIntegral h)
      Cairo.fill

      renderCollage coll

  Renderer.unlockTexture tex

-- | Render a collage (a group of forms with context).
renderCollage :: Collage SDLEngine -> Cairo.Render ()
renderCollage Collage { .. } = do
  Cairo.save

  forM_ collageDims $ \(V2 w h) -> do
    Cairo.rectangle 0 0 w h
    Cairo.clip

  forM_ collageCenter $ \(V2 x y) -> Cairo.translate x y
  mapM_ renderForm collageForms

  Cairo.restore

-- | Map a 'FontWeight' to a Pango font weight.
mapFontWeight :: FontWeight -> Pango.Weight
mapFontWeight weight = case weight of
  LightWeight  -> Pango.WeightLight
  NormalWeight -> Pango.WeightNormal
  BoldWeight   -> Pango.WeightBold

-- | Map a 'FontStyle' variant to a Pango font style.
mapFontStyle :: FontStyle -> Pango.FontStyle
mapFontStyle style = case style of
  NormalStyle  -> Pango.StyleNormal
  ObliqueStyle -> Pango.StyleOblique
  ItalicStyle  -> Pango.StyleItalic

-- | Setup a transformation state, render something with it, and then restore the old state.
withTransform
  :: Double           -- ^ The x and y scale factor of the state.
  -> Double           -- ^ The theta rotation of the state, in radians.
  -> Double           -- ^ The x translation value for the state.
  -> Double           -- ^ The y translation value for the state.
  -> Cairo.Render ()  -- ^ The render monad to run with the transformation state.
  -> Cairo.Render ()  -- ^ The final render monad.
withTransform s t x y f = do
  Cairo.save
  Cairo.scale s s
  Cairo.translate x y
  Cairo.rotate t
  f
  Cairo.restore

-- | Set the Cairo line cap from a 'LineCap'.
setLineCap :: LineCap -> Cairo.Render ()
setLineCap cap = case cap of
  FlatCap   -> Cairo.setLineCap Cairo.LineCapButt
  RoundCap  -> Cairo.setLineCap Cairo.LineCapRound
  PaddedCap -> Cairo.setLineCap Cairo.LineCapSquare

-- | Set the Cairo line join from a 'LineJoin'.
setLineJoin :: LineJoin -> Cairo.Render ()
setLineJoin join = case join of
  SmoothJoin    -> Cairo.setLineJoin Cairo.LineJoinRound
  ClippedJoin   -> Cairo.setLineJoin Cairo.LineJoinBevel
  SharpJoin lim -> do Cairo.setLineJoin Cairo.LineJoinMiter
                      Cairo.setMiterLimit lim

-- | Set up all the necessary settings with Cairo
-- to render with a line style (and then stroke the line). Assumes
-- that all drawing paths have already been setup before being called.
setLineStyle :: LineStyle -> Cairo.Render ()
setLineStyle LineStyle { lineColor = Color r g b a, .. } = do
  Cairo.setSourceRGBA r g b a
  setLineCap lineCap
  setLineJoin lineJoin
  Cairo.setLineWidth lineWidth
  Cairo.setDash lineDashing lineDashOffset
  Cairo.stroke

-- | Set up all the necessary settings with Cairo
-- to render with a fill style (and then fill the line). Assumes
-- that all drawing paths have already been setup before being called.
setFillStyle :: FillStyle SDLEngine -> Cairo.Render ()
setFillStyle (Solid (Color r g b a)) = do
  Cairo.setSourceRGBA r g b a
  Cairo.fill

setFillStyle (Texture SDLImage { cairoSurface }) = do
  Cairo.setSourceSurface cairoSurface 0 0
  Cairo.getSource >>= flip Cairo.patternSetExtend Cairo.ExtendRepeat
  Cairo.fill

setFillStyle (Gradient (Linear (sx, sy) (ex, ey) points)) =
  Cairo.withLinearPattern sx sy ex ey $ \ptn ->
    setGradientFill ptn points

setFillStyle (Gradient (Radial (sx, sy) sr (ex, ey) er points)) =
  Cairo.withRadialPattern sx sy sr ex ey er $ \ptn ->
    setGradientFill ptn points

-- | Add color stops to a pattern and then fill it.
setGradientFill :: Cairo.Pattern -> [(Double, Color)] -> Cairo.Render ()
setGradientFill ptn points = do
  Cairo.setSource ptn
  mapM_ (\(o, Color r g b a) -> Cairo.patternAddColorStopRGBA ptn o r g b a) points
  Cairo.fill

-- | Render a form.
renderForm :: Form SDLEngine -> Cairo.Render ()
renderForm Form { formPos = V2 x y, .. } = withTransform formScale formTheta x y $ do
  Cairo.save

  case formStyle of
    PathForm style (Path (~ps @ (V2 hx hy : _))) -> do
      Cairo.newPath
      Cairo.moveTo hx hy
      mapM_ (\(V2 lx ly) -> Cairo.lineTo lx ly) ps
      setLineStyle style

    ShapeForm style shape -> do
      Cairo.newPath

      case shape of
        PolygonShape (Path (~ps @ (V2 hx hy : _))) -> do
          Cairo.moveTo hx hy
          mapM_ (\(V2 lx ly) -> Cairo.lineTo lx ly) ps

        RectangleShape (V2 w h) ->
          Cairo.rectangle (-w / 2) (-h / 2) w h

        ArcShape (V2 cx cy) a1 a2 r (V2 sx sy) -> do
          Cairo.scale sx sy
          Cairo.arc cx cy r a1 a2

      case style of
        OutlinedShape ls -> setLineStyle ls
        FilledShape fs -> setFillStyle fs

    TextForm Text { textColor = Color r g b a, .. } -> do
      layout <- Pango.createLayout textString

      Cairo.liftIO $ Pango.layoutSetAttributes layout
        [ Pango.AttrFamily { paStart = i, paEnd = j, paFamily = T.pack textTypeface }
        , Pango.AttrWeight { paStart = i, paEnd = j, paWeight = mapFontWeight textWeight }
        , Pango.AttrStyle  { paStart = i, paEnd = j, paStyle = mapFontStyle textStyle }
        , Pango.AttrSize   { paStart = i, paEnd = j, paSize = textHeight }
        ]

      Pango.PangoRectangle tx ty w h <- fmap snd $ Cairo.liftIO $ Pango.layoutGetExtents layout

      Cairo.translate ((-w / 2) - tx) ((-h / 2) - ty)
      Cairo.setSourceRGBA r g b a
      Pango.showLayout layout

      where
        i = 0
        j = length textString

    ImageForm SDLImage { imageDims = V2 w h, .. } (V2 sx sy) (V2 sw sh) stretch -> do
      Cairo.translate (-sx) (-sy)

      if stretch then
        Cairo.scale (sw / fromIntegral w)
                    (sh / fromIntegral h)
      else
        Cairo.scale 1 1

      Cairo.setSourceSurface cairoSurface 0 0
      Cairo.translate sx sy
      Cairo.rectangle 0 0 sw sh

      if stretch then
        Cairo.paint
      else
        Cairo.fill

    GroupForm (Transform (V3 (V3 a b lx) (V3 c d ly) _)) forms -> do
      Cairo.transform $ Matrix a b c d lx ly
      mapM_ renderForm forms

    CollageForm coll -> renderCollage coll

  Cairo.restore