{-# LANGUAGE DeriveDataTypeable     #-}
{-# LANGUAGE FlexibleContexts       #-}
{-# LANGUAGE FlexibleInstances      #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GADTs                  #-}
{-# LANGUAGE MultiParamTypeClasses  #-}
{-# LANGUAGE RankNTypes             #-}
{-# LANGUAGE RecordWildCards        #-}
{-# LANGUAGE ScopedTypeVariables    #-}
{-# LANGUAGE TypeFamilies           #-}
{-# LANGUAGE TypeOperators          #-}
{-# LANGUAGE UndecidableInstances   #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Plots.Types.Scatter
-- Copyright   :  (C) 2015 Christopher Chalmers
-- License     :  BSD-style (see the file LICENSE)
-- Maintainer  :  Christopher Chalmers
-- Stability   :  experimental
-- Portability :  non-portable
--
-- A scatter plot is a type of mathematical diagram using Cartesian
-- coordinates to display values for typically two variables for a set
-- of data.
--
-- <<diagrams/src_Plots_Types_Scatter_scatterExample.svg#diagram=scatterExample&height=350>>
--
-- (see 'scatterPlot' example for code to make this plot)
--
----------------------------------------------------------------------------
module Plots.Types.Scatter
  ( -- * Scatter plot
    ScatterPlot

    -- * Scatter plot lenses
  , ScatterOptions
  , HasScatterOptions (..)
  , HasConnectingLine (..)

    -- * Basic scatter plot
    -- ** Add plots to the axis
  , scatterPlot
  , scatterPlot'
  , scatterPlotOf
  , scatterPlotOf'

    -- * Scatter options
  , scatterOptions

    -- * Bubble plots
  , bubblePlot
  , bubblePlot'
  , bubblePlotOf
  , bubblePlotOf'

    -- ** Bubble options
  , BubbleOptions
  , bubbleOptions
  , bubbleTransform
  , bubbleStyle

    -- ** General scatter plot
  , gscatterPlot
  , gscatterOptionsFor

    -- * Low level construction
  , mkScatterOptions
  ) where

import           Control.Lens                    hiding (lmap, transform, ( # ))
import           Control.Monad.State.Lazy

import qualified Data.Foldable                   as F
import           Data.Typeable

import           Diagrams.Coordinates.Isomorphic
import           Diagrams.Prelude                hiding (view)

import           Plots.Axis
import           Plots.Style
import           Plots.Types

import Diagrams.Types (mkQD, Prim (..))

-- config
-- > import Plots

------------------------------------------------------------------------
-- General scatter plot
------------------------------------------------------------------------

-- | A general data type for scatter plots. Allows storing different
--   types of data as well as allowing transforms depending on the data.
data ScatterPlot v where
  ScatterPlot :: Typeable a => ScatterOptions v a -> ScatterPlot v
  deriving Typeable

type instance V (ScatterPlot v) = v
type instance N (ScatterPlot v) = Double

-- | A general data type for scatter plots. Allows storing different
--   types of data as well as allowing transforms depending on the data.
data ScatterOptions v a = ScatterOptions
  { oData :: [a]
  , oPos  :: a -> Point v Double
  , oTr   :: a -> Transformation v Double
  , oSty  :: a -> Style v Double
  , oLine :: Bool
  } deriving Typeable

type instance V (ScatterOptions v a) = v
type instance N (ScatterOptions v a) = Double

instance Metric v => Enveloped (ScatterPlot v) where
  getEnvelope (ScatterPlot (ScatterOptions {..})) = getEnvelope (map oPos oData)

instance Plotable (ScatterPlot V2) where
  renderPlotable s sty (ScatterPlot (ScatterOptions {..})) =
    markers <> line
    where
      markers = F.foldMap mk oData # applyMarkerStyle sty
      --
      mk a = marker # transform (oTr a)
                    # applyStyle (oSty a)
                    # moveTo (specPoint s $ oPos a)
      marker = sty ^. plotMarker
      --
      line
        | not oLine = mempty
        | otherwise = fromVertices points # applyLineStyle sty
      points = map (specPoint s . oPos) oData

  defLegendPic sty (ScatterPlot (ScatterOptions {..})) =
    sty ^. plotMarker
      & applyMarkerStyle sty

strokeV3 :: Path V3 Double -> Diagram V3
strokeV3 path = mkQD (Prim path) (getEnvelope path) mempty mempty

instance Plotable (ScatterPlot V3) where
  renderPlotable s sty (ScatterPlot (ScatterOptions {..})) =
    markers <> line
    where
      markers = F.foldMap mk oData # applyMarkerStyle sty
      --
      mk a = marker # transform (oTr a)
                    # applyStyle (oSty a)
                    # moveTo (specPoint s $ oPos a)
      marker = sty ^. plotMarker
      --
      line
        | not oLine = mempty
        | otherwise = strokeV3 (fromVertices points) # applyLineStyle sty
      points = map (specPoint s . oPos) oData

  defLegendPic sty (ScatterPlot (ScatterOptions {..})) =
    sty ^. plotMarker
      & applyMarkerStyle sty

------------------------------------------------------------------------
-- Generating scatter options
------------------------------------------------------------------------

-- | Low level construction of 'ScatterOptions'.
mkScatterOptions
  :: (PointLike v Double p, F.Foldable f)
  => f a
  -> (a -> p)
  -> ScatterOptions v a
mkScatterOptions xs pf = ScatterOptions
  { oData = F.toList xs
  , oPos  = view unpointLike . pf
  , oTr   = mempty
  , oSty  = const mempty
  , oLine = False
  }

------------------------------------------------------------------------
-- Scatter plot lenses
------------------------------------------------------------------------

-- | Class of things that have a 'LensLike' for a 'ScatterPlot' \'s
--   connecting line.
class HasConnectingLine f a where
  -- | 'LensLike' onto whether the scatter plot should have a connecting
  --   line between points. If the line is present, it uses the
  --   'lineStyle' from the 'PlotStyle'.
  connectingLine :: Functor f => LensLike' f a Bool

instance HasConnectingLine f (ScatterOptions v a) where
  connectingLine = lens oLine (\o b -> o {oLine = b})

instance HasConnectingLine f (ScatterPlot v) where
  connectingLine f (ScatterPlot o@(ScatterOptions {..}))
    = f oLine <&> \b -> ScatterPlot o {oLine = b}

instance HasConnectingLine f p => HasConnectingLine f (Plot p) where
  connectingLine = rawPlot . connectingLine

instance (Applicative f, Typeable v)
    => HasConnectingLine f (DynamicPlot v) where
  connectingLine = (dynamicPlot :: Traversal' (DynamicPlot v) (Plot (ScatterPlot v)))
                 . connectingLine

instance (Applicative f, Typeable v)
    => HasConnectingLine f (StyledPlot v) where
  connectingLine = (styledPlot :: Traversal' (StyledPlot v) (ScatterPlot v))
                 . connectingLine

instance (Settable f, Typeable (BaseSpace c))
    => HasConnectingLine f (Axis c) where
  connectingLine = finalPlots . connectingLine

-- Options -------------------------------------------------------------

class HasScatterOptions f a d where
  -- | Lens onto the 'ScatterOptions' for a general scatter plot.
  gscatterOptions :: LensLike' f a (ScatterOptions (V a) d)

  -- | Apply a transform to the markers using the associated data.
  scatterTransform :: Functor f => LensLike' f a (d -> Transformation (V a) Double)
  scatterTransform = gscatterOptions . lens oTr (\o tr -> o {oTr = tr})

  -- | Apply a style to the markers using the associated data.
  scatterStyle :: Functor f => LensLike' f a (d -> Style (V a) Double)
  scatterStyle = gscatterOptions . lens oSty (\o sty -> o {oSty = sty})

  -- | Change the position of the markers depending on the data.
  scatterPosition :: Functor f => LensLike' f a (d -> Point (V a) Double)
  scatterPosition = gscatterOptions . lens oPos (\o pos -> o {oPos = pos})

instance d ~ d' => HasScatterOptions f (ScatterOptions v d) d' where
  gscatterOptions = id

instance (Applicative f, Typeable v, Typeable d)
    => HasScatterOptions f (ScatterPlot v) d where
  gscatterOptions f s@(ScatterPlot p) =
    case eq p of
      Just Refl -> ScatterPlot <$> f p
      Nothing   -> pure s
    where
      eq :: Typeable a => a -> Maybe (a :~: ScatterOptions v d)
      eq _ = eqT

instance (Functor f, HasScatterOptions f p a) => HasScatterOptions f (Plot p) a where
  gscatterOptions = rawPlot . gscatterOptions

instance (Applicative f, Typeable v, Typeable a)
    => HasScatterOptions f (DynamicPlot v) a where
  gscatterOptions = dynamicPlot . rawPlot

instance (Applicative f, Typeable (BaseSpace c), Typeable a)
    => HasScatterOptions f (Axis c) a where
  gscatterOptions = axisPlots . traverse . gscatterOptions


-- Pure scatter lenses -------------------------------------------------

-- | Lens onto a scatter plot of points.
scatterOptions :: (InSpace v Double a, HasScatterOptions f a (Point v Double))
               => LensLike' f a (ScatterOptions v (Point v Double))
scatterOptions = gscatterOptions

-- -- | Lens onto a transform of a scatter plot of points. This is a
-- --   specialised version of 'scatterTransform' for better type
-- --   inference.
-- _scatterTransform
--   :: (InSpace v a, PointLike v p, Functor f, HasScatterOptions f a (Point v))
--   => LensLike' f a (p -> Transformation v)
-- _scatterTransform = scatterTransform . lmapping unpointLike

-- -- | Lens onto a transform of a scatter plot of points. This is a
-- --   specialised version of 'scatterPosition' for better type inference.
-- _scatterPosition
--   :: (InSpace v a, PointLike v p, Functor f, HasScatterOptions f a (Point v))
--   => LensLike' f a (p -> p)
-- _scatterPosition = scatterPos . dimapping unpointLike pointLike

-- -- | Lens onto a style function of a scatter plot of points. This is a
-- --   specialised version of 'scatterStyle' for better type inference.
-- _scatterStyle
--   :: (InSpace v a, PointLike v p, Functor f, HasScatterOptions f a (Point v))
--   => LensLike' f a (p -> Style v)
-- _scatterStyle = scatterStyle . lmapping unpointLike

------------------------------------------------------------------------
-- Scatter plot
------------------------------------------------------------------------

-- $ scatter
-- Scatter plots display data as dots. There are several representations
-- for scatter plots for extra parameters. Scatter plots have the
-- following lenses:
--
-- * 'connectingLine' - line between points
-- *
--

-- | Add a 'ScatterPlot' to the 'AxisState' from a data set.
--
-- @
-- 'scatterPlot' :: [('Double', 'Double')] -> 'State' ('Plot' ('ScatterOptions' 'V2' ('P2' 'Double')) b) () -> 'State' ('Axis' 'V2') ()
-- 'scatterPlot' :: ['V2' 'Double']        -> 'State' ('Plot' ('ScatterOptions' 'V2' ('P2' 'Double')) b) () -> 'State' ('Axis' 'V2') ()
-- 'scatterPlot' :: ['P2' 'Double']        -> 'State' ('Plot' ('ScatterOptions' 'V2' ('P2' 'Double')) b) () -> 'State' ('Axis' 'V2') ()
-- @
--
-- === __Example__
--
-- <<diagrams/src_Plots_Types_Scatter_scatterExample.svg#diagram=scatterExample&height=350>>
--
-- > import Plots
-- > mydata1 = [(1,3), (2,5.5), (3.2, 6), (3.5, 6.1)]
-- > mydata2 = mydata1 & each . _1 *~ 0.5
-- > mydata3 = [V2 1.2 2.7, V2 2 5.1, V2 3.2 2.6, V2 3.5 5]
--
-- > scatterAxis :: Axis V2
-- > scatterAxis = r2Axis &~ do
-- >   scatterPlot mydata1 $ key "data 1"
-- >   scatterPlot mydata2 $ key "data 2"
-- >   scatterPlot mydata3 $ key "data 3"
--
-- > scatterExample = renderAxis scatterAxis
scatterPlot
  :: (BaseSpace c ~ v,
      PointLike v Double p,
      MonadState (Axis c) m,
      Plotable (ScatterPlot v),
      F.Foldable f)
  => f p  -- ^ points to plot
  -> State (Plot (ScatterOptions v (Point v Double))) ()
          -- ^ changes to plot options
  -> m () -- ^ add plot to 'Axis'
scatterPlot xs = gscatterPlot (xs ^.. folded . unpointLike) id

-- | Version of 'scatterPlot' without any changes to the
--   'ScatterOptions'.
--
-- @
-- 'scatterPlot'' :: [('Double', 'Double')] -> 'State' ('Axis' b 'V2' 'Double') ()
-- 'scatterPlot'' :: ['V2' 'Double']        -> 'State' ('Axis' b 'V2' 'Double') ()
-- 'scatterPlot'' :: ['P2' 'Double']        -> 'State' ('Axis' b 'V2' 'Double') ()
-- @
--
-- === __Example__
--
-- <<diagrams/src_Plots_Types_Scatter_scatterExample'.svg#diagram=scatterExample'&height=350>>
--
-- > import Plots
-- > mydata4 = [(1,3), (2,5.5), (3.2, 6), (3.5, 6.1)]
-- > mydata5 = mydata1 & each . _1 *~ 0.5
-- > mydata6 = [V2 1.2 2.7, V2 2 5.1, V2 3.2 2.6, V2 3.5 5]
--
-- > scatterAxis' :: Axis B V2 Double
-- > scatterAxis' = r2Axis &~ do
-- >   scatterPlot' mydata4
-- >   scatterPlot' mydata5
-- >   scatterPlot' mydata6
--
-- > scatterExample' = renderAxis scatterAxis'
scatterPlot'
  :: (BaseSpace c ~ v,
      PointLike v Double p,
      MonadState (Axis c) m,
      Plotable (ScatterPlot v),
      F.Foldable f)
  => f p  -- ^ points to plot
  -> m () -- ^ add plot to 'Axis'
scatterPlot' xs = scatterPlot xs (return ())

-- | Version of 'scatterPlot' that accepts a 'Fold' over the data.
scatterPlotOf
  :: (BaseSpace c ~ v,
      PointLike v Double p,
      Plotable (ScatterPlot v),
      MonadState (Axis c) m)
  => Fold s p -- ^ fold over points
  -> s        -- ^ data to fold
  -> State (Plot (ScatterOptions v (Point v Double))) () -- ^ changes to plot options
  -> m () -- ^ add plot to 'Axis'
scatterPlotOf f s = scatterPlot (toListOf f s)

-- | Version of 'scatterPlot' that accepts a 'Fold' over the data
--   without any changes to the 'ScatterOptions'.
scatterPlotOf'
  :: (BaseSpace c ~ v,
      PointLike v Double p,
      MonadState (Axis c) m,
      Plotable (ScatterPlot v))
  => Fold s p -- ^ fold over points
  -> s -- ^ data to fold
  -> m () -- ^ add plot to axis
scatterPlotOf' f s = scatterPlot' (toListOf f s)

------------------------------------------------------------------------
-- Bubble plot --
------------------------------------------------------------------------

-- | A bubble plot is a scatter plot using point together with a scalar.
type BubbleOptions v = ScatterOptions v (Double, Point v Double)

-- | Scatter plots with extra numeric parameter. By default the extra
--   parameter is the scale of the marker but this can be changed.
--
-- @
-- 'bubblePlot' :: [('Double', ('Double', 'Double'))] -> 'State' ('Plot' ('BubbleOptions' v) b) () -> 'State' ('Axis' b 'V2' 'Double') ()
-- 'bubblePlot' :: [('Double', 'V2' 'Double')]        -> 'State' ('Plot' ('BubbleOptions' v) b) () -> 'State' ('Axis' b 'V2' 'Double') ()
-- 'bubblePlot' :: [('Double', 'P2' 'Double')]        -> 'State' ('Plot' ('BubbleOptions' v) b) () -> 'State' ('Axis' b 'V2' 'Double') ()
-- @
--
-- === __Example__
--
-- <<diagrams/src_Plots_Types_Scatter_bubbleExample.svg#diagram=bubbleExample&height=350>>
--
-- > import Plots
-- > myweights = [2, 1.3, 1.8, 0.7]
-- > mydata7 = zip myweights [(1,3), (2,5.5), (3.2, 6), (3.5, 6.1)]
-- > mydata8 = mydata7 & each._2._2 *~ 0.5 & each._1 *~ 0.5
-- > mydata9 = [(1, V2 1.2 2.7), (3, V2 2 5.1), (0.9, V2 3.2 2.6), (2, V2 3.5 5)]
--
-- > bubbleAxis :: Axis B V2 Double
-- > bubbleAxis = r2Axis &~ do
-- >   bubblePlot mydata7 $ key "data 7"
-- >   bubblePlot mydata8 $ key "data 8"
-- >   bubblePlot mydata9 $ key "data 9"
--
-- > bubbleExample = renderAxis bubbleAxis
bubblePlot
  :: (BaseSpace c ~ v,
      PointLike v Double p,
      MonadState (Axis c) m,
      Plotable (ScatterPlot v),
      F.Foldable f)
  => f (Double, p) -- ^ fold over points with a size
  -> State (Plot (BubbleOptions v)) () -- ^ changes to the options
  -> m () -- ^ add plot to 'Axis'
bubblePlot xs s =
  gscatterPlot (xs ^.. folded . mapping unpointLike) snd $ do
    bubbleTransform .= scaling
    s

-- | Simple version of 'bubblePlot' without any changes to the 'Plot'.
--
-- @
-- 'bubblePlot'' :: [('Double', ('Double', 'Double'))] -> 'State' ('Axis' b 'V2' 'Double') ()
-- 'bubblePlot'' :: [('Double', 'V2' 'Double')]        -> 'State' ('Axis' b 'V2' 'Double') ()
-- @
--
bubblePlot'
  :: (v ~ BaseSpace c,
      PointLike v Double p,
      MonadState (Axis c) m,
      Plotable (ScatterPlot v),
      F.Foldable f)
  => f (Double, p) -- ^ fold over points with a size
  -> m () -- ^ add plot to 'Axis'
bubblePlot' xs = bubblePlot xs (return ())

-- | Version of 'bubblePlot' using a 'Fold' over the data.
bubblePlotOf
  :: (BaseSpace c ~ v,
      PointLike v Double p,
      Plotable (ScatterPlot v),
      MonadState (Axis c) m)
  => Fold s (Double,p) -- ^ fold over the data
  -> s            -- ^ data
  -> State (Plot (BubbleOptions v)) ()
                  -- ^ changes to the options
  -> m ()         -- ^ add plot to 'Axis'
bubblePlotOf f s = bubblePlot (toListOf f s)

-- | Version of 'bubblePlot' using a 'Fold' over the data without any
--   changes to the 'BubbleOptions'.
bubblePlotOf'
  :: (BaseSpace c ~ v,
      PointLike v Double p,
      MonadState (Axis c) m,
      Plotable (ScatterPlot v))
  => Fold s (Double,p) -- ^ fold over the data
  -> s            -- ^ data
  -> State (Plot (BubbleOptions v)) ()
                  -- ^ changes to the options
  -> m ()         -- ^ add plot to 'Axis'
bubblePlotOf' f s = bubblePlot (toListOf f s)

-- Bubble scatter lenses -----------------------------------------------

-- | LensLike onto into a 'ScatterOptions' made up of a scaler @n@, and
--   a point, @'Point' v@
--
-- @
-- 'bubbleOptions' :: 'Lens'' ('Plot' ('BubbleOptions' v) v) ('BubbleOptions' v)
-- @
bubbleOptions :: (InSpace v Double a, HasScatterOptions f a (Double, Point v Double))
              => LensLike' f a (BubbleOptions v)
bubbleOptions = gscatterOptions

-- | Setter over the transform function for a 'bubblePlot'. Default is 'scale'.
--
-- @
-- 'bubbleOptions' :: 'Setter'' ('Plot' ('BubbleOptions' v) v) (n -> 'Transformation' v)
-- @
--
--   Note that this is the less general version of @'bubblePlot' .
--   'scatterTransform'@, which would give a 'LensLike' onto @(n,
--   'Point' v) -> 'Transformation' v@.
--
bubbleTransform
  :: (InSpace v Double a, HasScatterOptions f a (Double, Point v Double), Settable f)
  => LensLike' f a (Double -> Transformation v Double)
bubbleTransform = bubbleOptions . scatterTransform . sets nOnly
  where nOnly f g (n,p) = f (\n' -> g (n', p)) n

-- | Setter over the style function for a 'bubblePlot'. Default is 'mempty'.
--
-- @
-- 'bubbleStyle' :: 'Setter'' ('Plot' ('BubbleOptions' v) v) (n -> 'Style' v)
-- @
--
--   Note that this is the less general version of @'bubblePlot' .
--   'scatterTransform'@, which would give a 'LensLike' onto @(n,
--   'Point' v) -> 'Style' v@.
--
bubbleStyle :: (InSpace v Double a, Settable f, HasScatterOptions f a (Double, Point v Double))
             => LensLike' f a (Double -> Style v Double)
bubbleStyle = bubbleOptions . scatterStyle . sets nOnly
  where nOnly f g (n,p) = f (\n' -> g (n', p)) n

------------------------------------------------------------------------
-- General scatter plot
------------------------------------------------------------------------

-- | A general scatter plot allow using any data type @d@ to determine
--   the 'scatterTransform' and 'scatterStyle'.
gscatterPlot
  :: (BaseSpace c ~ v,
      PointLike v Double p,
      MonadState (Axis c) m,
      Typeable d,
      Plotable (ScatterPlot v),
      F.Foldable f)
  => f d -- ^ data
  -> (d -> p) -- ^ extract point from data
  -> State (Plot (ScatterOptions v d)) ()
              -- ^ options for plot
  -> m ()     -- ^ add plot to 'Axis'
gscatterPlot xs pf s = addPlot $ over rawPlot ScatterPlot p1
  where
    p1 = execState s p0
    p0 = mkPlot $ mkScatterOptions xs (view unpointLike . pf)

-- | Helper to traverse over a general scatter plot where the type of d
--   is not infered.
gscatterOptionsFor
  :: (InSpace v Double a, HasScatterOptions f a d)
  => proxy d -> LensLike' f a (ScatterOptions v d)
gscatterOptionsFor _ = gscatterOptions