{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}

module Text.Noise.Renderer (render) where

import           Prelude hiding ((!!))
import           Data.List hiding ((!!))
import           Data.Monoid
import           Data.Function
import           Data.Maybe
import           Control.Monad
import           Control.Applicative
import qualified Numeric
import qualified Data.List as List
import qualified Data.ByteString as ByteString
import qualified Crypto.Hash.SHA1 as SHA1
import qualified Text.Blaze.Internal as Blaze
import qualified Text.Blaze.Svg11 as SVG
import qualified Text.Blaze.Svg11.Attributes as SVG.At
import           Text.Blaze.Svg11 ((!), Svg)
import qualified Text.Blaze.Svg.Renderer.Pretty as Pretty
import qualified Text.Blaze.Svg.Renderer.Utf8 as Utf8
import qualified Text.Noise.Renderer.SVG.Attributes as At
import qualified Text.Noise.Compiler.Document as D
import qualified Text.Noise.Compiler.Document.Color as Color

class Renderable a where
  renderToInlineSvg :: a -> InlineSvg
  renderToSvg :: a -> Svg
  render :: a -> String
  renderToInlineSvg = InlineSvg [] . renderToSvg
  renderToSvg = uninline . renderToInlineSvg
  render = Pretty.renderSvg . renderToSvg

instance (Renderable a) => Renderable [a] where
  renderToInlineSvg = mconcat . map renderToInlineSvg

data InlineSvg = InlineSvg [(String, Svg)] Svg

data InlineAttribute = InlineAttribute (Maybe (String, Svg)) SVG.Attribute

instance Monoid InlineSvg where
  mempty = InlineSvg [] mempty
  mappend (InlineSvg defs svg) (InlineSvg defs' svg') =
    InlineSvg (List.unionBy ((==) `on` fst) defs defs') (svg <> svg')

(?) :: InlineSvg -> InlineAttribute -> InlineSvg
(InlineSvg defs svg) ? (InlineAttribute attrDef attr) = InlineSvg defs' svg'
  where svg' = svg ! attr
        defs' = case attrDef of
          Just def -> def : defs
          Nothing -> defs

(!!) :: Svg -> [SVG.Attribute] -> Svg
svg !! attrs = foldl' (!) svg attrs

(??) :: InlineSvg -> [InlineAttribute] -> InlineSvg
inlineSvg ?? attrs = foldl' (?) inlineSvg attrs

inline :: Svg -> InlineSvg
inline = InlineSvg []

uninline :: InlineSvg -> Svg
uninline (InlineSvg defs main) = SVG.defs (mconcat $ map snd defs) <> main

instance Blaze.Attributable InlineSvg where
  (!) (InlineSvg defs main) attr = InlineSvg defs (main ! attr)

instance Renderable D.Document where
  renderToSvg (D.Document elems) =
    SVG.docTypeSvg $ uninline $ mconcat $ map renderToInlineSvg elems

instance Renderable D.Element where
  renderToInlineSvg (D.Rectangle x y w h radius fill stroke) = inline SVG.rect
    ! At.x x
    ! At.y y
    ! At.width w
    ! At.height h
    ! At.rx radius
    ?? fillAttrs fill
    ?? strokeAttrs stroke

  renderToInlineSvg (D.Circle cx cy r fill stroke) = inline SVG.circle
    ! At.cx cx
    ! At.cy cy
    ! At.r r
    ?? fillAttrs fill
    ?? strokeAttrs stroke

  renderToInlineSvg (D.Image x y w h file) = inline SVG.image
    ! At.x x
    ! At.y y
    ! At.width w
    ! At.height h
    ! At.xlinkHref file
    ! At.preserveaspectratio "none"

  renderToInlineSvg (D.Path fill stroke commands) = inline SVG.path
    ! At.d (concatMap renderPathCommand commands)
    ?? fillAttrs fill
    ?? strokeAttrs stroke

  renderToInlineSvg (D.Group members) = InlineSvg defs (SVG.g innerSvg)
    where InlineSvg defs innerSvg = renderToInlineSvg members

instance Renderable D.Gradient where
  renderToSvg gradient = svgGradient $ forM_ (D.stops gradient) $ \(offset, color) ->
    SVG.stop
      ! At.offset offset
      !! stopColorAttrs color
    where
      svgGradient = case gradient of
        (D.RadialGradient _ ) -> SVG.radialgradient
        (D.LinearGradient angle _ ) ->
          let radians = angle * pi / 180
          in SVG.lineargradient
            ! At.x2 (cos radians)
            ! At.y2 (sin radians)

colorValue :: D.Color -> SVG.AttributeValue
colorValue = Blaze.stringValue . ('#' :) . Color.toRGBHex

svgAttr :: (Renderable a) => (SVG.AttributeValue -> SVG.Attribute) -> a -> InlineAttribute
svgAttr attrFn x = InlineAttribute (Just (uniqueId, svg')) $ attrFn (Blaze.stringValue funcIRI)
  where svg = renderToSvg x
        svg' = svg ! At.id uniqueId
        funcIRI = D.showFuncIRI (D.localIRIForId uniqueId)
        uniqueId = List.foldl' (flip Numeric.showHex) "" $ ByteString.unpack sha
        sha = SHA1.hashlazy (Utf8.renderSvg svg)

paintAttrs :: (SVG.AttributeValue -> SVG.Attribute)
           -> (D.OpacityValue -> SVG.Attribute)
           -> D.Paint
           -> [InlineAttribute]
paintAttrs paintServerAttrFn opacityAttrFn paint = case paint of
  D.GradientPaint gradient -> [ svgAttr paintServerAttrFn gradient ]
  D.ColorPaint color       -> map (InlineAttribute Nothing) (paintServerAttr : maybeToList opacityAttr)
    where opacityAttr = opacityAttrFn <$> Color.alpha color
          paintServerAttr = paintServerAttrFn (colorValue color)

fillAttrs :: D.Paint -> [InlineAttribute]
fillAttrs = paintAttrs SVG.At.fill At.fillOpacity

strokeAttrs :: D.Paint -> [InlineAttribute]
strokeAttrs = paintAttrs SVG.At.stroke At.strokeOpacity

stopColorAttrs :: D.Color -> [SVG.Attribute]
stopColorAttrs color = stopColorAttr : maybeToList stopOpacityAttr
  where stopOpacityAttr = At.stopOpacity <$> Color.alpha color
        stopColorAttr = SVG.At.stopColor (colorValue color)

renderPathCommand :: D.PathCommand -> String
renderPathCommand command = unwords $ case command of
  D.Move dx dy -> "m" : map show [dx, dy]
  D.Line dx dy -> "l" : map show [dx, dy]
  D.Arc x y rx ry rotation ->
    ["a", show rx, show ry, show rotation, "0", "0", show x, show y]
