{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE NegativeLiterals #-}
{-# 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:
--
-- > scaleExample :: IO ()
-- > scaleExample =
-- >     fileSvg "other/scaleExample.svg" (#size .~ Pair 300 120 $ def) $
-- >     withHud
-- >       def
-- >       widescreen
-- >       (Rect 0 12 0 0.2)
-- >       (lineChart (repeat def))
-- >       (vlineOneD ((0.01*) <$> [0..10]))
--
-- ![scale example](other/scaleExample.svg)
--

module Chart.Core
  ( -- * Chart types
    Chart
    -- * Scaling
  , range
  , projectss
  , aspect
  , asquare
  , sixbyfour
  , golden
  , widescreen
  , skinny
  , AlignH(..)
  , AlignV(..)
  , alignHU
  , alignHTU
  , alignVU
  , alignVTU
    -- * Types
  , Orientation(..)
  , Place(..)
    -- * 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

    -- * Color
    --
    -- | chart-unit exposes the 'colour' and 'palette' libraries for color combinators
  , ucolor
  , ublue
  , ugrey
    -- * Compatability
  , scaleX
  , scaleY
  , scale
  ) where

import Diagrams.Prelude
       hiding (Color, D, aspect, project, scale, scaleX, scaleY, zero)
import qualified Diagrams.Prelude as Diagrams
import qualified Diagrams.TwoD.Text
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
  , Renderable (Diagrams.TwoD.Text.Text Double) b) =>
       QDiagram b V2 Double Any

-- | 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

-- | the aspect of a chart expressed as a ratio of x-plane : y-plane.
aspect :: (BoundedField a, Ord a, Multiplicative a, FromInteger a) => a -> Rect a
aspect a = Ranges ((a *) <$> one) one

-- | a 1:1 aspect
asquare :: Rect Double
asquare = aspect 1

-- | a 1.5:1 aspect
sixbyfour :: Rect Double
sixbyfour = aspect 1.5

-- | golden ratio
golden :: Rect Double
golden = aspect 1.61803398875

-- | a 3:1 aspect
widescreen :: Rect Double
widescreen = aspect 3

-- | a skinny 5:1 aspect
skinny :: Rect Double
skinny = aspect 5

-- | horizontal alignment
data AlignH
  = AlignLeft
  | AlignCenter
  | AlignRight
  deriving (Eq, Show)

-- | vertical alignment
data AlignV
  = AlignTop
  | AlignMid
  | AlignBottom
  deriving (Eq, Show)

-- | 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

-- | svg text is lower by default
alignVTU :: AlignV -> Double
alignVTU a =
  case a of
    AlignTop -> 0.5
    AlignMid -> 0
    AlignBottom -> -0.5

-- | Orientation for an element.  Watch this space for curvature!
data Orientation
  = Hori
  | Vert
  deriving (Eq, Show)

-- | Placement of elements around (what is implicity but maybe shouldn't just be) a rectangular canvas
data Place
  = PlaceLeft
  | PlaceRight
  | PlaceTop
  | PlaceBottom
  deriving (Eq, Show)

-- | 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)

-- | 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)