{-# OPTIONS_GHC -Wall #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE CPP #-}
#if ( __GLASGOW_HASKELL__ < 820 )
{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-}
#endif

-- | In making a chart, there are three main size domains you have to be concerned about:
--
-- - the range of the data being charted. This range is often projected onto chart elements such as axes and labels. A data range in two dimensions is a 'Rect' a.
--
-- - the scale of various chart primitives and elements.  The overall dimensions of the chart canvas - the rectangular shape on which the data is represented - is referred to as an 'Aspect' in the api, and is a wrapped 'Rect' to distinguish aspects from rect ranges.  The default chart options tend to be callibrated to Aspects around widths of one.
--
-- - the size of the chart rendered as an image. Backends tend to shrink charts to fit the rectangle shape specified in the render function, and a loose sympathy is expected between the aspect and a chart's ultimate physical size.
--
-- Jumping ahead a bit, the code snippet below draws vertical lines using a data range of "Rect 0 12 0 0.2" (slightly different to the actual data range), using a widescreen (3:1) aspect, and renders the chart as a 300 by 120 pixel svg:
--
-- > fileSvg "other/scaleExample.svg" (300,120) $
-- >   withHud (hudAspect_ .~ widescreen $ hudRange_ .~ Just (Rect 0 12 0 0.2) $ def)
-- >   (lineChart (repeat def)) ((\x -> [Pair x 0, Pair x (x/100)]) <$> [0..10])
--
-- ![scale example](other/scaleExample.svg)
--
module Chart.Core
  ( -- * Chart types
    Chart
  , UChart(..)
  , combine

    -- * Scaling
  , range
  , projectss
  , Aspect(..)
  , aspect
  , asquare
  , sixbyfour
  , golden
  , widescreen
  , skinny
  , AlignH(..)
  , AlignV(..)
  , alignHU
  , alignHTU
  , alignVU

    -- * Combinators
    --
    -- | The concept of a point on a chart is the polymorphic 'R2' from the 'linear' library.  Diagrams most often uses 'Point', which is a wrapped 'V2'.  The 'Pair' type from 'numhask-range' is often used as a point reference.
  , positioned
  , p_
  , r_
  , stack
  , vert
  , hori
  , sepVert
  , sepHori

    -- * IO
  , fileSvg

    -- * Color
    --
    -- | chart-unit exposes the 'colour' and 'palette' libraries for color combinators
  , ucolor
  , ublue
  , ugrey

    -- * Compatability
  , scaleX
  , scaleY
  , scale
  ) where

import Diagrams.Backend.SVG (SVG, renderSVG)
import Diagrams.Prelude
       hiding (Color, D, aspect, project, scale, scaleX, scaleY, zero)
import qualified Diagrams.Prelude as Diagrams
import NumHask.Pair
import NumHask.Prelude
import NumHask.Rect
import NumHask.Space

-- | A Chart is simply a type synonym for a typical Diagrams object.  A close relation to this type is 'Diagram' 'B', but this usage tends to force a single backend (B comes from the backend libraries), so making Chart b's maintains backend polymorphism.
--
-- Just about everything - text, circles, lines, triangles, charts, axes, titles, legends etc - are 'Chart's, which means that most things are amenable to full use of the combinatorially-inclined diagrams-lib.
type Chart b
   = (Renderable (Path V2 Double) b) =>
       QDiagram b V2 Double Any

-- | a UChart provides a late binding of a chart Aspect so multiple charts can be rendered using the same range.
data UChart a b = UChart
  { uchartRenderer :: () =>
                        Aspect -> Rect Double -> a -> Chart b
  , uchartRenderRange :: Rect Double
  , uchartData :: a
  }

-- | render a list of charts, taking into account each of their ranges
combine :: Aspect -> [UChart a b] -> Chart b
combine asp qcs = mconcat $ (\(UChart c _ x) -> c asp rall x) <$> qcs
  where
    rall = fold $ (\(UChart _ r1 _) -> r1) <$> qcs

-- | project a double-containered set of data to a new Rect range
projectss ::
     (Functor f, Functor g)
  => Rect Double
  -> Rect Double
  -> g (f (Pair Double))
  -> g (f (Pair Double))
projectss r0 r1 xyss = map (project r0 r1) <$> xyss

-- | determine the range of a double-containered set of data
range :: (Foldable f, Foldable g) => g (f (Pair Double)) -> Rect Double
range xyss = foldMap space xyss

-- | a wrapped Rect specifying the shape od the chart canvas.
--
-- The Aspect tends to be:
--
-- - independent of the data range
-- - expressed in terms around a width magnitude of one.  chart default options are callibrated to this convention.
newtype Aspect = Aspect (Rect Double)

-- | the rendering aspect of a chart expressed as a ratio of x-plane : y-plane.
aspect :: Double -> Aspect
aspect a = Aspect $ Ranges ((a *) <$> one) one

-- | a 1:1 aspect
asquare :: Aspect
asquare = aspect 1

-- | a 1.5:1 aspect
sixbyfour :: Aspect
sixbyfour = aspect 1.5

-- | golden ratio
golden :: Aspect
golden = aspect 1.61803398875

-- | a 3:1 aspect
widescreen :: Aspect
widescreen = aspect 3

-- | a skinny 5:1 aspect
skinny :: Aspect
skinny = aspect 5

-- | horizontal alignment
data AlignH
  = AlignLeft
  | AlignCenter
  | AlignRight

-- | vertical alignment
data AlignV
  = AlignTop
  | AlignMid
  | AlignBottom

-- | conversion of horizontal alignment to (one :: Range Double) limits
alignHU :: AlignH -> Double
alignHU a =
  case a of
    AlignLeft -> 0.5
    AlignCenter -> 0
    AlignRight -> -0.5

-- | svg text is forced to be lower left (-0.5) by default
alignHTU :: AlignH -> Double
alignHTU a =
  case a of
    AlignLeft -> 0
    AlignCenter -> -0.5
    AlignRight -> -1

-- | conversion of vertical alignment to (one :: Range Double) limits
alignVU :: AlignV -> Double
alignVU a =
  case a of
    AlignTop -> -0.5
    AlignMid -> 0
    AlignBottom -> 0.5

-- | position an element at a point
positioned :: (R2 r) => r Double -> Chart b -> Chart b
positioned p = moveTo (p_ p)

-- | convert an R2 to a diagrams Point
p_ :: (R2 r) => r Double -> Point V2 Double
p_ r = curry p2 (r ^. _x) (r ^. _y)

-- | convert an R2 to a V2
r_ :: R2 r => r a -> V2 a
r_ r = V2 (r ^. _x) (r ^. _y)

-- | foldMap for beside; stacking chart elements in a direction, with a premap
stack ::
     (R2 r, V a ~ V2, Foldable t, Juxtaposable a, Semigroup a, N a ~ Double, Monoid a)
  => r Double
  -> (b -> a)
  -> t b
  -> a
stack dir f xs = foldr (\a x -> beside (r_ dir) (f a) x) mempty xs

-- | combine elements vertically, with a premap
vert ::
     (V a ~ V2, Foldable t, Juxtaposable a, Semigroup a, N a ~ Double, Monoid a)
  => (b -> a)
  -> t b
  -> a
vert = stack (Pair 0 -1)

-- | combine elements horizontally, with a premap
hori ::
     (V a ~ V2, Foldable t, Juxtaposable a, Semigroup a, N a ~ Double, Monoid a)
  => (b -> a)
  -> t b
  -> a
hori = stack (Pair 1 0)

-- | horizontal separator
sepHori :: Double -> Chart b -> Chart b
sepHori s x = beside (r2 (0, -1)) x (strutX s)

-- | vertical separator
sepVert :: Double -> Chart b -> Chart b
sepVert s x = beside (r2 (1, 0)) x (strutY s)

-- | write an svg to file
fileSvg :: FilePath -> (Double, Double) -> Diagram SVG -> IO ()
fileSvg f s = renderSVG f (mkSizeSpec (Just <$> r2 s))

-- | convert an rgba spec to an AlphaColour
ucolor :: (Floating a, Ord a) => a -> a -> a -> a -> AlphaColour a
ucolor r g b o = withOpacity (sRGB r g b) o

-- | the official chart-unit blue
ublue :: AlphaColour Double
ublue = ucolor 0.365 0.647 0.855 0.5

-- | the official chart-unit grey
ugrey :: AlphaColour Double
ugrey = ucolor 0.4 0.4 0.4 1

-- | These are difficult to avoid
instance R1 Pair where
  _x f (Pair a b) = (`Pair` b) <$> f a

instance R2 Pair where
  _y f (Pair a b) = Pair a <$> f b
  _xy f p = fmap (\(V2 a b) -> Pair a b) . f . (\(Pair a b) -> V2 a b) $ p

eps :: N [Point V2 Double]
eps = 1e-8

-- | the diagrams scaleX with a zero divide guard to avoid error throws
scaleX ::
     (N t ~ Double, Transformable t, R2 (V t), Diagrams.Additive (V t))
  => Double
  -> t
  -> t
scaleX s =
  Diagrams.scaleX
    (if s == zero
       then eps
       else s)

-- | the diagrams scaleY with a zero divide guard to avoid error throws
scaleY ::
     (N t ~ Double, Transformable t, R2 (V t), Diagrams.Additive (V t))
  => Double
  -> t
  -> t
scaleY s =
  Diagrams.scaleY
    (if s == zero
       then eps
       else s)

-- | the diagrams scale with a zero divide guard to avoid error throws
scale ::
     (N t ~ Double, Transformable t, R2 (V t), Diagrams.Additive (V t))
  => Double
  -> t
  -> t
scale s =
  Diagrams.scale
    (if s == zero
       then eps
       else s)