{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# OPTIONS_GHC -Wall #-}

module Chart.Svg
  ( svg,
    svgt,
    chartDef,
    chartDefs,
    styleBox,
    styleBoxes,
    styleBoxText,
    styleBoxGlyph,
  )
where

import Chart.Color
import Chart.Types
import Control.Category (id)
import Control.Lens hiding (transform)
import Data.Generics.Labels ()
import Data.Maybe
import Data.Monoid
import qualified Data.Text as Text
import qualified Lucid
import Lucid.Svg hiding (z)
import NumHask.Space as NH hiding (Element)
import Protolude hiding (writeFile)
import Text.HTML.TagSoup hiding (Attribute)

-- | the extra area from text styling
styleBoxText ::
  TextStyle ->
  Text.Text ->
  Point Double ->
  Rect Double
styleBoxText o t p = move (p + p') $ maybe flat (`rotateRect` flat) (o ^. #rotation)
  where
    flat = Rect ((- x' / 2.0) + x' * a') (x' / 2 + x' * a') ((- y' / 2) - n1') (y' / 2 - n1')
    s = o ^. #size
    h = o ^. #hsize
    v = o ^. #vsize
    n1 = o ^. #nudge1
    x' = s * h * fromIntegral (Protolude.sum $ maybe 0 Text.length . maybeTagText <$> parseTags t)
    y' = s * v
    n1' = s * n1
    a' = case o ^. #anchor of
      AnchorStart -> 0.5
      AnchorEnd -> -0.5
      AnchorMiddle -> 0.0
    p' = fromMaybe (Point 0.0 0.0) (o ^. #translate)

-- | the extra area from glyph styling
styleBoxGlyph :: GlyphStyle -> Rect Double
styleBoxGlyph s = move p' $ sw $ case sh of
  EllipseGlyph a -> NH.scale (Point sz (a * sz)) unitRect
  RectSharpGlyph a -> NH.scale (Point sz (a * sz)) unitRect
  RectRoundedGlyph a _ _ -> NH.scale (Point sz (a * sz)) unitRect
  VLineGlyph _ -> NH.scale (Point ((s ^. #borderSize) * sz) sz) unitRect
  HLineGlyph _ -> NH.scale (Point sz ((s ^. #borderSize) * sz)) unitRect
  TriangleGlyph a b c -> (sz *) <$> sconcat (toRect . SpotPoint <$> (a :| [b, c]) :: NonEmpty (Rect Double))
  _ -> (sz *) <$> unitRect
  where
    sh = s ^. #shape
    sz = s ^. #size
    sw = padRect (0.5 * s ^. #borderSize)
    p' = fromMaybe (Point 0.0 0.0) (s ^. #translate)

-- | the geometric dimensions of a Chart inclusive of style geometry
styleBox :: Chart Double -> Maybe (Rect Double)
styleBox (Chart (TextA s ts) xs) = foldRect $ zipWith (\t x -> styleBoxText s t (toPoint x)) ts xs
styleBox (Chart (GlyphA s) xs) = foldRect $ (\x -> move (toPoint x) (styleBoxGlyph s)) <$> xs
styleBox (Chart (RectA s) xs) = foldRect (padRect (0.5 * s ^. #borderSize) . toRect <$> xs)
styleBox (Chart (LineA s) xs) = foldRect (padRect (0.5 * s ^. #width) . toRect <$> xs)
styleBox (Chart BlankA xs) = foldRect (toRect <$> xs)
styleBox (Chart (PixelA s) xs) = foldRect (padRect (0.5 * s ^. #pixelRectStyle . #borderSize) . toRect <$> xs)

-- | the extra geometric dimensions of a [Chart]
styleBoxes :: [Chart Double] -> Maybe (Rect Double)
styleBoxes xss = foldRect $ catMaybes (styleBox <$> xss)

-- | calculate the linear gradient to shove in defs
-- FIXME: Only works for #pixelGradient = 0 or pi//2. Can do much better with something like https://stackoverflow.com/questions/9025678/how-to-get-a-rotated-linear-gradient-svg-for-use-as-a-background-image
lgPixel :: PixelStyle -> Svg ()
lgPixel o =
  linearGradient_
    [ id_ (o ^. #pixelTextureId),
      x1_ (show x0),
      y1_ (show y0),
      x2_ (show x1),
      y2_ (show y1)
    ]
    ( mconcat
        [ stop_
            [ stop_opacity_ (show $ opac $ o ^. #pixelColorMin),
              stop_color_ (toHex (o ^. #pixelColorMin)),
              offset_ "0"
            ],
          stop_
            [ stop_opacity_ (show $ opac $ o ^. #pixelColorMax),
              stop_color_ (toHex (o ^. #pixelColorMax)),
              offset_ "1"
            ]
        ]
    )
  where
    x0 = min 0 (cos (o ^. #pixelGradient))
    x1 = max 0 (cos (o ^. #pixelGradient))
    y0 = max 0 (sin (o ^. #pixelGradient))
    y1 = min 0 (sin (o ^. #pixelGradient))

-- | get chart definitions
chartDefs :: [Chart a] -> Svg ()
chartDefs cs = bool (defs_ (mconcat ds)) mempty (0 == length ds)
  where
    ds = mconcat $ chartDef <$> cs

chartDef :: Chart a -> [Svg ()]
chartDef c = case c of
  (Chart (PixelA s) _) -> [lgPixel s]
  _ -> []

-- | Rectangle svg
svgRect :: Rect Double -> Svg ()
svgRect (Rect x z y w) =
  rect_
    [ width_ (show $ z - x),
      height_ (show $ w - y),
      x_ (show x),
      y_ (show $ - w)
    ]

-- | Text svg
svgText :: TextStyle -> Text -> Point Double -> Svg ()
svgText s t p@(Point x y) =
  bool id (g_ [class_ "hasmathjax"]) (s ^. #hasMathjax) $
    text_
      ( [ x_ (show x),
          y_ (show $ - y)
        ]
          <> maybe [] (\x' -> [transform_ (toRotateText x' p)]) (s ^. #rotation)
      )
      (toHtmlRaw t)

-- | line svg
svgLine :: [Point Double] -> Svg ()
svgLine xs = polyline_ [points_ (toPointsText xs)]
  where
    toPointsText xs' = Text.intercalate "\n" $ (\(Point x y) -> show x <> "," <> show (- y)) <$> xs'

-- | GlyphShape to svg Tree
svgShape :: GlyphShape -> Double -> Point Double -> Svg ()
svgShape CircleGlyph s (Point x y) =
  circle_
    [ cx_ (show x),
      cy_ (show $ - y),
      r_ (show $ 0.5 * s)
    ]
svgShape SquareGlyph s p =
  svgRect (move p ((s *) <$> unitRect))
svgShape (RectSharpGlyph x') s p =
  svgRect (move p (NH.scale (Point s (x' * s)) unitRect))
svgShape (RectRoundedGlyph x' rx ry) s p =
  rect_
    [ width_ (show $ z - x),
      height_ (show $ w - y),
      x_ (show x),
      y_ (show $ - w),
      rx_ (show rx),
      ry_ (show ry)
    ]
  where
    (Rect x z y w) = move p (NH.scale (Point s (x' * s)) unitRect)
svgShape (TriangleGlyph (Point xa ya) (Point xb yb) (Point xc yc)) s p =
  polygon_
    [ transform_ (toTranslateText p),
      points_ (show (s * xa) <> "," <> show (- (s * ya)) <> " " <> show (s * xb) <> "," <> show (- (s * yb)) <> " " <> show (s * xc) <> "," <> show (- (s * yc)))
    ]
svgShape (EllipseGlyph x') s (Point x y) =
  ellipse_
    [ cx_ (show x),
      cy_ (show $ - y),
      rx_ (show $ 0.5 * s),
      ry_ (show $ 0.5 * s * x')
    ]
svgShape (VLineGlyph _) s (Point x y) =
  polyline_ [points_ (show x <> "," <> show (- (y - s / 2)) <> "\n" <> show x  <> "," <> show (- (y + s / 2)))]
svgShape (HLineGlyph _) s (Point x y) =
  polyline_ [points_ (show (x - s / 2) <> "," <> show (- y) <> "\n" <> show (x + s / 2) <> "," <> show (- y))]
svgShape (PathGlyph path) _ p =
  path_ [d_ path, transform_ (toTranslateText p)]

-- | GlyphStyle to svg Tree
svgGlyph :: GlyphStyle -> Point Double -> Svg ()
svgGlyph s p =
  svgShape (s ^. #shape) (s ^. #size) (realToFrac <$> p)
    & maybe id (\r -> g_ [transform_ (toRotateText r p)]) (s ^. #rotation)

-- | convert a Chart to svg
svg :: Chart Double -> Svg ()
svg (Chart (TextA s ts) xs) =
  g_ (attsText s) (mconcat $ zipWith (\t p -> svgText s t (toPoint p)) ts xs)
svg (Chart (GlyphA s) xs) =
  g_ (attsGlyph s) (mconcat $ svgGlyph s . toPoint <$> xs)
svg (Chart (LineA s) xs) =
  g_ (attsLine s) (svgLine $ toPoint <$> xs)
svg (Chart (RectA s) xs) =
  g_ (attsRect s) (mconcat $ svgRect . toRect <$> xs)
svg (Chart (PixelA s) xs) =
  g_ (attsPixel s) (mconcat $ svgRect . toRect <$> xs)
svg (Chart BlankA _) = mempty

-- | add a tooltip to a chart
svgt :: Chart Double -> (TextStyle, Text) -> Svg ()
svgt (Chart (TextA s ts) xs) (s', ts') =
  g_ (attsText s) (title_ (attsText s') (Lucid.toHtml ts') <> mconcat (zipWith (\t p -> svgText s t (toPoint p)) ts xs))
svgt (Chart (GlyphA s) xs) (s', ts') =
  g_ (attsGlyph s) (title_ (attsText s') (Lucid.toHtml ts') <> mconcat (svgGlyph s . toPoint <$> xs))
svgt (Chart (LineA s) xs) (s', ts') =
  g_ (attsLine s) (title_ (attsText s') (Lucid.toHtml ts') <> svgLine (toPoint <$> xs))
svgt (Chart (RectA s) xs) (s', ts') =
  g_ (attsRect s) (title_ (attsText s') (Lucid.toHtml ts') <> mconcat (svgRect . toRect <$> xs))
svgt (Chart (PixelA s) xs) (s', ts') =
  g_ (attsPixel s) (title_ (attsText s') (Lucid.toHtml ts') <> mconcat (svgRect . toRect <$> xs))
svgt (Chart BlankA _) _ = mempty

-- * Style to Attributes

attsRect :: RectStyle -> [Attribute]
attsRect o =
  [ stroke_width_ (show $ o ^. #borderSize),
    stroke_ (hex $ o ^. #borderColor),
    stroke_opacity_ (show $ opac $ o ^. #borderColor),
    fill_ (hex $ o ^. #color),
    fill_opacity_ (show $ opac $ o ^. #color)
  ]

attsPixel :: PixelStyle -> [Attribute]
attsPixel o =
  [ stroke_width_ (show $ o ^. #pixelRectStyle . #borderSize),
    stroke_ (toHex $ o ^. #pixelRectStyle . #borderColor),
    stroke_opacity_ (show $ opac $ o ^. #pixelRectStyle . #borderColor),
    fill_ ("url(#" <> (o ^. #pixelTextureId) <> ")")
  ]

attsText :: TextStyle -> [Attribute]
attsText o =
  [ stroke_width_ "0.0",
    stroke_ "none",
    fill_ (toHex $ o ^. #color),
    fill_opacity_ (show $ opac $ o ^. #color),
    font_size_ (show $ o ^. #size),
    text_anchor_ (toTextAnchor $ o ^. #anchor)
  ]
    <> maybe [] ((: []) . transform_ . toTranslateText) (o ^. #translate)
  where
    toTextAnchor :: Anchor -> Text
    toTextAnchor AnchorMiddle = "middle"
    toTextAnchor AnchorStart = "start"
    toTextAnchor AnchorEnd = "end"

attsGlyph :: GlyphStyle -> [Attribute]
attsGlyph o =
  [ stroke_width_ (show $ o ^. #borderSize),
    stroke_ (toHex $ o ^. #borderColor),
    stroke_opacity_ (show $ opac $ o ^. #borderColor),
    fill_ (toHex $ o ^. #color),
    fill_opacity_ (show $ opac $ o ^. #color)
  ]
    <> maybe [] ((: []) . transform_ . toTranslateText) (o ^. #translate)

attsLine :: LineStyle -> [Attribute]
attsLine o =
  [ stroke_width_ (show $ o ^. #width),
    stroke_ (toHex $ o ^. #color),
    stroke_opacity_ (show $ opac $ o ^. #color),
    fill_ "none"
  ]

toTranslateText :: Point Double -> Text
toTranslateText (Point x y) =
  "translate(" <> show x <> ", " <> show (- y) <> ")"

toRotateText :: Double -> Point Double -> Text
toRotateText r (Point x y) =
  "rotate(" <> show r <> ", " <> show x <> ", " <> show (- y) <> ")"