{-# LANGUAGE CPP                    #-}
{-# LANGUAGE DeriveDataTypeable     #-}
{-# LANGUAGE FlexibleContexts       #-}
{-# LANGUAGE FlexibleInstances      #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GADTs                  #-}
{-# LANGUAGE MultiParamTypeClasses  #-}
{-# LANGUAGE RankNTypes             #-}
{-# LANGUAGE ScopedTypeVariables    #-}
{-# LANGUAGE TemplateHaskell        #-}
{-# LANGUAGE TypeFamilies           #-}
{-# LANGUAGE TypeOperators          #-}
{-# LANGUAGE UndecidableInstances   #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Plots.Types
-- Copyright   :  (C) 2015 Christopher Chalmers
-- License     :  BSD-style (see the file LICENSE)
-- Maintainer  :  Christopher Chalmers
-- Stability   :  experimental
-- Portability :  non-portable
--
-- This module defines the various types for holding plots:
--
-- [@'PlotOptions' b v n@]
-- Generic options all plots have.
--
-- [@'PlotMods' b v n@]
-- Includes 'PlotOptions' along with modifications to the 'PlotStyle'.
--
-- [@'Plot' p b@]
-- A 'rawPlot' @p@ grouped with a 'PlotMods'.
--
-- [@'DynamicPlot' b v n@]
-- A wrapped up 'Plot' so it can be stored in an 'Axis'.
--
-- [@'StyledPlot' b v n@]
-- A 'DynamicPlot' with a concrete 'PlotStyle', ready to be rendered.
--
-- As well as other things like the 'Plotable' class, 'LegendEntries',
-- 'HasOrientation' and 'HasVisibility'.
--
----------------------------------------------------------------------------
module Plots.Types
  (

    -- * Plot options
    PlotOptions
  , HasPlotOptions (..)
  , key
  , addLegendEntry

    -- ** Plot modifications
  , PlotMods
  , plotMods

    -- * Plotable class
  , Plotable (..)

    -- * Plot types
    -- ** Parameterised plot
  , Plot
  , mkPlot
  , rawPlot

    -- ** Dynamic plot
  , DynamicPlot (..)
  , _DynamicPlot
  , dynamicPlot
  , dynamicPlotMods

    -- ** Styled plot
  , StyledPlot
  , styledPlot
  , styleDynamic
  , renderStyledPlot
  , singleStyledPlotLegend
  , styledPlotLegends

  -- * Miscellaneous
  -- ** Visibility
  , HasVisibility (..)
  , hide
  , display

  -- ** Orientation
  , Orientation (..)
  , HasOrientation (..)
  , orient
  , horizontal
  , vertical

  -- ** Legend entries
  , LegendEntry
  , LegendPic (..)
  , mkLegendEntry
  , legendPicture
  , legendText
  , legendPrecedence

    -- ** Axis spec
  , AxisSpec (..)
  , specTrans
  , specBounds
  , specScale
  , scaleNum
  , specPoint
  , specColourMap

  -- ** Positioning
  , Placement (..)
  , HasPlacement (..)
  , HasGap (..)
  , placeAgainst

  -- *** Common positions
  -- **** Inside positions
  , topLeft, top, topRight, left, right, bottomLeft, bottom
  , bottomRight

  -- **** Outside positions
  , leftAbove, leftTop, leftMid, leftBottom, leftBelow, midAbove, midBelow
  , rightAbove, rightTop, rightMid, rightBottom, rightBelow

  ) where

import           Control.Monad.State
import           Data.Bool
import           Data.List           (sortBy)
import           Data.Maybe          (fromMaybe)
import           Data.Ord            (comparing)
import           Data.Orphans        ()
import           Data.Typeable
import           Diagrams.Prelude    as D

import           Plots.Axis.Scale
import           Plots.Style
import           Plots.Util

-- Orientation ---------------------------------------------------------

data Orientation = Horizontal | Vertical
  deriving (Show, Eq, Ord, Typeable)

-- | Pick the first @a@ if the object has 'Horizontal' orientation and
--   the second @a@ if the object has a 'Vertical' orientation.
orient :: HasOrientation o => o -> a -> a -> a
orient o h v =
  case view orientation o of
    Horizontal -> h
    Vertical   -> v

-- | Class of things that have an orientation.
class HasOrientation a where
  -- | Lens onto the orientation of an object.
  orientation :: Lens' a Orientation

instance HasOrientation Orientation where
  orientation = id

-- | Lens onto whether an object's orientation is horizontal.
horizontal :: HasOrientation a => Lens' a Bool
horizontal = orientation . iso (==Horizontal) (bool Vertical Horizontal)

-- | Lens onto whether an object's orientation is vertical.
vertical :: HasOrientation a => Lens' a Bool
vertical = horizontal . involuted not

------------------------------------------------------------------------
-- Placement
------------------------------------------------------------------------

class HasGap a where
  -- | The value of the gap when rendering.
  gap :: Lens' a (N a)

-- | A 'Position' is a point on an axis together with an anchor and a
--   direction for the gap.
data Placement = Placement
  { pAt     :: V2 Rational
  , pAnchor :: V2 Rational
  , pGapDir :: Direction V2 Rational
  }
  deriving (Show, Read, Eq, Ord)
  -- In the future the axis position may be replaced by a reader-like
  -- anchor system where you can choose parts of the rendered axis as a
  -- position.
  -- we keep posGap and posGapDir as separate values to keep lens laws
  -- for 0 gaps
  -- I'm not sure this should work for a 3D axis

class HasPlacement a where
  placement :: Lens' a Placement

  -- | The position relative to the axis. @V2 0 0@ corresponds to the
  --   bottom left corner, @V2 1 1@ is the top right corner.
  placementAt :: Lens' a (V2 Rational)
  placementAt = placement . lens pAt (\p a -> p {pAt = a})

  -- | The anchor used for the object being positioned. @V2 0 0@
  --   corresponds to the bottom left corner, @V2 1 1@ is the top right
  --   corner.
  placementAnchor :: Lens' a (V2 Rational)
  placementAnchor = placement . lens pAnchor (\p a -> p {pAnchor = a})

  -- | The direction to extend the 'gap' when positioning.
  gapDirection :: Lens' a (Direction V2 Rational)
  gapDirection = placement . lens pGapDir (\p a -> p {pGapDir = a})

instance HasPlacement Placement where
  placement = id

-- Inside positions ----------------------------------------------------

-- Internal helper for all inside placements
pInside :: V2 Rational -> Placement
pInside v = Placement
  { pAt     = v
  , pAnchor = v
  , pGapDir = dirBetween' (P v) origin
  }

-- | @dirBetween p q@ returns the directions from @p@ to @q@
dirBetween' :: (Additive v, Num n) => Point v n -> Point v n -> Direction v n
dirBetween' p q = direction $ q .-. p


topLeft, top, topRight, left, right, bottomLeft, bottom, bottomRight :: Placement
topLeft     = pInside (V2 (-1)   1 )
top         = pInside (V2   0    1 )
topRight    = pInside (V2   1    1 )
left        = pInside (V2 (-1)   0 )
right       = pInside (V2 (-1)   0 )
bottomLeft  = pInside (V2 (-1) (-1))
bottom      = pInside (V2   0  (-1))
bottomRight = pInside (V2   1  (-1))

-- Outside positions ---------------------------------------------------

leftAbove, leftTop, leftMid, leftBottom, leftBelow, midAbove, midBelow,
  rightAbove, rightTop, rightMid, rightBottom, rightBelow :: Placement

leftAbove   = Placement (V2 (-1)   1 ) (V2 (-1) (-1)) (direction (V2   0    1 ))
leftTop     = Placement (V2 (-1)   1 ) (V2   1    1 ) (direction (V2 (-1)   0 ))
leftMid     = Placement (V2 (-1)   0 ) (V2   1    0 ) (direction (V2 (-1)   0 ))
leftBottom  = Placement (V2 (-1) (-1)) (V2   1  (-1)) (direction (V2 (-1)   0 ))
leftBelow   = Placement (V2 (-1) (-1)) (V2 (-1)   1 ) (direction (V2   0  (-1)))

midAbove    = Placement (V2   0    1 ) (V2   0  (-1)) (direction (V2   0    1 ))
midBelow    = Placement (V2   0  (-1)) (V2   0    1 ) (direction (V2   0  (-1)))

rightAbove  = Placement (V2   1    1 ) (V2   1  (-1)) (direction (V2   0    1 ))
rightTop    = Placement (V2   1    1 ) (V2 (-1)   1 ) (direction (V2   1    0 ))
rightMid    = Placement (V2   1    0 ) (V2 (-1)   0 ) (direction (V2   1    0 ))
rightBottom = Placement (V2   1  (-1)) (V2 (-1) (-1)) (direction (V2   1    0 ))
rightBelow  = Placement (V2   1  (-1)) (V2   1    1 ) (direction (V2   0  (-1)))

-- Using positions -----------------------------------------------------

-- | A tool for aligned one object to another. Positions @b@ around the
--   bounding box of @a@ by translating @b@.
placeAgainst
  :: (InSpace V2 n a, SameSpace a b, Enveloped a,
      HasOrigin b, Alignable b)
  => a -> Placement -> n -> b -> b
placeAgainst a (Placement (V2 px py) (V2 ax ay) d) n b
  = b # anchor
      # moveTo (pos .+^ n *^ fromDirection (fmap fromRational d))
  where
    pos    = mkP2 (lerp' px xu xl) (lerp' py yu yl)
    anchor = alignBy unitX (fromRational ax) . alignBy unitY (fromRational ay)
    (P (V2 xl yl), P (V2 xu yu)) = fromMaybe (origin, origin) (getCorners $ boundingBox a)

    lerp' z u v = fromRational alpha * u + (1 - fromRational alpha) * v
      where alpha = (z + 1) / 2

------------------------------------------------------------------------
-- Legend entries
------------------------------------------------------------------------

-- | Type allowing use of the default legend picture (depending on the
--   plot) or a custom legend picture with access to the 'PlotStyle'.
data LegendPic b v n
  = DefaultLegendPic
  | CustomLegendPic (PlotStyle b v n -> QDiagram b v n Any)

instance Default (LegendPic b v n) where
  def = DefaultLegendPic

-- | Data type for holding a legend entry.
data LegendEntry b v n = LegendEntry
  { lPic        :: LegendPic b v n
  , lText       :: String
  , lPrecedence :: n
  } deriving Typeable

-- | The picture used in the legend entry.
legendPicture :: Lens' (LegendEntry b v n) (LegendPic b v n)
legendPicture = lens lPic (\l pic -> l {lPic = pic})

-- | The text used in the legend entry.
legendText :: Lens' (LegendEntry b v n) String
legendText = lens lText (\l txt -> l {lText = txt})

-- | The order in which the legend entries are rendered. If precedences
--   are equal, the entries are put in the order they are added to the
--   axis.
--
--   Default is @0@.
legendPrecedence :: Lens' (LegendEntry b v n) n
legendPrecedence = lens lPrecedence (\l n -> l {lPrecedence = n})

type instance V (LegendEntry b v n) = v
type instance N (LegendEntry b v n) = n

-- | Make a legend entry with a default 'legendPicture' and
--   'legendPrecedence' 0 using the string as the 'legendText'.
mkLegendEntry :: Num n => String -> LegendEntry b v n
mkLegendEntry x = LegendEntry DefaultLegendPic x 0

------------------------------------------------------------------------
-- Plot attributes
------------------------------------------------------------------------

-- Generic Plot info

-- | Data type for holding information all plots must contain.
data PlotOptions b v n = PlotOptions
  { poName      :: Name
  , poClipPlot  :: Bool
  , poLegend    :: [LegendEntry b v n]
  , poVisible   :: Bool
  , poTransform :: Transformation v n
  -- , poPostPlotBoundingBox :: BoundingBox v n -> BoundingBox v n
  -- , poPlotPostProduction  :: QDiagram b v n Any -> QDiagram b v n Any
  } deriving Typeable

type instance V (PlotOptions b v n) = v
type instance N (PlotOptions b v n) = n

-- | Class of things that have 'PlotOptions'.
class HasPlotOptions f a b | a -> b where
  {-# MINIMAL plotOptions #-}
  -- | Lens onto the 'PlotOptions'.
  plotOptions :: LensLike' f a (PlotOptions b (V a) (N a))

  -- | The 'Name' applied to the plot. This gives a way to reference a
  --   specific plot in a rendered axis.
  --
  --   'Default' is 'mempty'.
  plotName :: Functor f => LensLike' f a Name
  plotName = plotOptions . lens poName (\g a -> g {poName = a})
  {-# INLINE plotName #-}

  -- | Whether the plot should be clipped to the bounds of the axes.
  --
  --   'Default' is 'True'.
  clipPlot :: Functor f => LensLike' f a Bool
  clipPlot = plotOptions . lens poClipPlot (\g a -> g {poClipPlot = a})
  {-# INLINE clipPlot #-}

  -- | The legend entries to be used for the current plot.
  --
  --   'Default' is 'mempty'.
  legendEntries :: Functor f => LensLike' f a [LegendEntry b (V a) (N a)]
  legendEntries = plotOptions . lens poLegend (\g a -> g {poLegend = a})
  {-# INLINE legendEntries #-}

  -- | The transform applied to the plot once it's in the axis
  --   coordinates.
  --
  --   'Default' is 'mempty'.
  plotTransform :: Functor f => LensLike' f a (Transformation (V a) (N a))
  plotTransform = plotOptions . lens poTransform (\g a -> g {poTransform = a})
  {-# INLINE plotTransform #-}

  -- | Whether or not the plot should be shown. The 'BoundingBox' of the
  --   plot will still affect the inferred axis bounds.
  --
  --   'Default' is 'True'.
  plotVisible :: Functor f => LensLike' f a Bool
  plotVisible = plotOptions . lens poVisible (\po b -> po {poVisible = b})
  {-# INLINE plotVisible #-}

instance (Additive v, Num n) => Default (PlotOptions b v n) where
  def = PlotOptions
    { poName                = mempty
    , poClipPlot            = True
    , poLegend              = []
    , poVisible             = True
    , poTransform           = mempty
    -- , poPostPlotBoundingBox = id
    -- , poPlotPostProduction  = id
    }

instance HasPlotOptions f (PlotOptions b v n) b where
  plotOptions = id
  {-# INLINE plotOptions #-}

instance (HasLinearMap v, Num n) => Transformable (PlotOptions b v n) where
  transform = over plotTransform . transform

-- instance HasBounds (PlotOptions b v n) v where
--   bounds = plotBounds

-- | Move origin by applying to @plotTransform@.
instance (Additive v, Num n) => HasOrigin (PlotOptions b v n) where
  moveOriginTo = over plotTransform . moveOriginTo

instance Qualifiable (PlotOptions b v n) where
  n .>> p = over plotName (n .>>) p

-- XXX template haskell getting in the way
-- instance HasVisibility (PlotOptions b v n) where
--   visible = plotVisible

-- | Add a 'LegendEntry' to something with 'PlotOptions' using the
--   'String' as the 'legendText' and a 'DefaultLegendPic'. Here are
--   some typical examples:
--
-- @
-- 'key' :: 'String' -> 'State' ('Plot' ('ScatterPlot' v n) b) ()
-- 'key' :: 'String' -> 'State' ('DynamicPlot' b v n) ()
-- 'key' :: 'String' -> 'State' ('PlotMods' b v n) ()
-- @
--
--  If you only care about the name of the legend, use 'key'.
key :: (HasPlotOptions Identity a b, MonadState a m, Num (N a)) => String -> m ()
key = addLegendEntry . mkLegendEntry

-- | Add a 'LegendEntry' to something with 'PlotOptions'. Here are some
--   typical examples:
--
-- @
-- 'addLegendEntry' :: 'LegendEntry' b v n -> 'State' ('Plot' ('ScatterPlot' v n) b) ()
-- 'addLegendEntry' :: 'LegendEntry' b v n -> 'State' ('DynamicPlot' b v n) ()
-- @
--
--  If you only care about the name of the legend, use 'key'.
addLegendEntry
  :: (HasPlotOptions Identity a b, MonadState a m)
  => LegendEntry b (V a) (N a)
  -> m ()
addLegendEntry l = legendEntries <>= [l]

-- zeroInt :: Additive v => v Int
-- zeroInt = zero

------------------------------------------------------------------------
-- AxisSpec
------------------------------------------------------------------------

-- | Information from the 'Plots.Axis.Axis' necessary to render a 'Plotable'.
data AxisSpec v n = AxisSpec
  { _specBounds    :: v (n, n)
  , _specTrans     :: Transformation v n
  , _specScale     :: v LogScale
  , _specColourMap :: ColourMap
  }

makeLenses ''AxisSpec

type instance V (AxisSpec v n) = v
type instance N (AxisSpec v n) = n

-- | Scale a number by log10-ing it and linearly scaling it so it's
--   within the same range.
scaleNum :: Floating n => (n, n) -> LogScale -> n -> n
scaleNum (a,b) s x = case s of
  LinearAxis -> x
  LogAxis    -> subtract a $ (b / logBase 10 d) * (logBase 10 x)
    where d = b - a

-- | Apply log scaling and the transform to a point.
specPoint :: (Applicative v, Additive v, Floating n) => AxisSpec v n -> Point v n -> Point v n
specPoint (AxisSpec bs tr ss _) p =
  papply tr $ over _Point (scaleNum <$> bs <*> ss <*>) p

------------------------------------------------------------------------
-- Plotable class
------------------------------------------------------------------------

-- | Class defining how plots should be rendered.
class (Typeable p, Enveloped p) => Plotable p b where
  -- | Render a plot according to the 'AxisSpec', using the 'PlotStyle'.
  renderPlotable
    :: InSpace v n p
    => AxisSpec v n
    -> PlotStyle b v n
    -> p
    -> QDiagram b v n Any

  -- | The default legend picture when the 'LegendPic' is
  --   'DefaultLegendPic'.
  defLegendPic
    :: InSpace v n p
    => PlotStyle b v n
    -> p
    -> QDiagram b v n Any
  defLegendPic = mempty

instance (Typeable b, Typeable v, Metric v, Typeable n, OrderedField n)
  => Plotable (QDiagram b v n Any) b where
  renderPlotable s _ dia = dia # transform (s^.specTrans)

instance (TypeableFloat n, Renderable (Path V2 n) b) => Plotable (Path V2 n) b where
  renderPlotable s sty path
    = stroke path
        # transform (s^.specTrans)
        # applyLineStyle sty

  defLegendPic sty _
    = (p2 (-10,0) ~~ p2 (10,0))
        # applyLineStyle sty

------------------------------------------------------------------------
-- Visibility
------------------------------------------------------------------------

-- | Class of objects that can be hidden.
class HasVisibility a where
  -- | Lens onto whether an object should be visible when rendered.
  visible :: Lens' a Bool

  -- | The opposite of 'visible'.
  hidden :: Lens' a Bool
  hidden = visible . involuted not
  {-# INLINE hidden #-}

instance HasVisibility (PlotOptions b v n) where
  visible = plotVisible

instance HasVisibility (PlotMods b v n) where
  visible = plotVisible

instance HasVisibility (Plot p b) where
  visible = plotVisible

instance HasVisibility (DynamicPlot b v n) where
  visible = plotVisible

instance HasVisibility (StyledPlot b v n) where
  visible = plotVisible

-- | Set 'visible' to 'False' for the given setter.
--
-- @
-- 'hide' 'minorTicks'          :: 'State' ('Axis' b v n) ()
-- 'hide' ('xAxis' . 'gridLines') :: 'State' ('Axis' b v n) ()
-- @
hide :: (MonadState s m, HasVisibility a) => ASetter' s a -> m ()
hide l = l . visible .= False

-- | Set 'visible' to 'True' for the given setter.
--
-- @
-- 'display' 'minorGridLines' :: 'State' ('Axis' b v n) ()
-- 'display' 'colourBar'      :: 'State' ('Axis' b v n) ()
-- @
display :: (MonadState s m, HasVisibility a) => ASetter' s a -> m ()
display l = l . visible .= True

------------------------------------------------------------------------
-- Plot modification
------------------------------------------------------------------------

-- | A 'PlotOptions' with modifications to a 'PlotStyle'.
data PlotMods b v n
  = PlotMods (PlotOptions b v n) (PlotStyle b v n -> PlotStyle b v n)

type instance V (PlotMods b v n) = v
type instance N (PlotMods b v n) = n

instance Functor f => HasPlotOptions f (PlotMods b v n) b where
  plotOptions f (PlotMods opts sty) = f opts <&> \opts' -> PlotMods opts' sty

instance Settable f => HasPlotStyle f (PlotMods b v n) b where
  plotStyle = sty . mapped where
    sty f (PlotMods opts s) = f s <&> \s' -> PlotMods opts s'

instance (Additive v, Num n) => Default (PlotMods b v n) where
  def = PlotMods def id

------------------------------------------------------------------------
-- Plot type
------------------------------------------------------------------------

-- | A parameterised plot, together with a 'PlotMods'. This type has an
--   instance of many classes for modifying specific plots.
data Plot p b =
  Plot p
       (PlotOptions b (V p) (N p))
       (PlotStyle b (V p) (N p) -> PlotStyle b (V p) (N p))
  deriving Typeable

type instance V (Plot p b) = V p
type instance N (Plot p b) = N p

instance Functor f => HasPlotOptions f (Plot p b) b where
  plotOptions f (Plot p opts sty) = f opts <&> \opts' -> Plot p opts' sty

instance Settable f => HasPlotStyle f (Plot p b) b where
  plotStyle = sty . mapped where
    sty f (Plot p opts s) = f s <&> \s' -> Plot p opts s'

instance HasOrientation p => HasOrientation (Plot p b) where
  orientation = rawPlot . orientation

-- | Make a 'Plot' with 'Default' 'PlotOptions'.
mkPlot :: (Additive (V p), Num (N p)) => p -> Plot p b
mkPlot p = Plot p def id

-- | Lens onto the raw 'Plotable' inside a 'Plot'.
rawPlot :: SameSpace p p' => Lens (Plot p b) (Plot p' b) p p'
rawPlot f (Plot p opts ps) = f p <&> \p' -> Plot p' opts ps

-- | The modifications to the 'PlotOptions' and 'PlotStyle' in a 'Plot'.
plotMods :: Lens' (Plot p b) (PlotMods b (V p) (N p))
plotMods f (Plot p opts ps) =
  f (PlotMods opts ps) <&> \(PlotMods opts' ps') -> Plot p opts' ps'

------------------------------------------------------------------------
-- DynamicPlot
------------------------------------------------------------------------

-- | A wrapped up 'Plot', used to store plots in an 'Axis'.
data DynamicPlot b v n where
  DynamicPlot :: (InSpace v n p, Plotable p b) => Plot p b -> DynamicPlot b v n
  deriving Typeable

type instance V (DynamicPlot b v n) = v
type instance N (DynamicPlot b v n) = n

-- | Prism for a 'DynamicPlot'.
_DynamicPlot :: (Plotable p b, Typeable b) => Prism' (DynamicPlot b (V p) (N p)) (Plot p b)
_DynamicPlot = prism' DynamicPlot (\(DynamicPlot p) -> cast p)

-- | Traversal over the dynamic plot without the 'Plotable' constraint
--   '_DynamicPlot' has.
dynamicPlot :: forall p b. (Typeable p, Typeable b)
            => Traversal' (DynamicPlot b (V p) (N p)) (Plot p b)
dynamicPlot f d@(DynamicPlot p) =
  case eq p of
    Just Refl -> f p <&> \p' -> DynamicPlot p'
    Nothing   -> pure d
  where eq :: Typeable a => a -> Maybe (a :~: Plot p b)
        eq _ = eqT
instance Functor f => HasPlotOptions f (DynamicPlot b v n) b where
  plotOptions f (DynamicPlot (Plot p opts sty)) =
    f opts <&> \opts' -> DynamicPlot (Plot p opts' sty)

instance Settable f => HasPlotStyle f (DynamicPlot b v n) b where
  plotStyle = sty . mapped where
    sty :: Setter' (DynamicPlot b v n) (PlotStyle b v n -> PlotStyle b v n)
    sty f (DynamicPlot (Plot p opts s)) = f s <&> \s' -> DynamicPlot (Plot p opts s')

-- | The modifications to the 'PlotOptions' and 'PlotStyle' in a 'DynamicPlot'.
dynamicPlotMods :: Lens' (DynamicPlot b v n) (PlotMods b v n)
dynamicPlotMods f (DynamicPlot (Plot p opts ps)) =
  f (PlotMods opts ps) <&> \(PlotMods opts' ps') -> DynamicPlot (Plot p opts' ps')

------------------------------------------------------------------------
-- StyledPlot
------------------------------------------------------------------------

-- | A 'DynamicPlot' with a concrete style. This is suitable for being
--   rendered with 'renderStyledPlot' and get extract the legend entries
--   with 'styledPlotLegend'.
--
--   You can make a 'StyledPlot' with 'styleDynamic'
data StyledPlot b v n where
  StyledPlot
    :: Plotable p b
    => p
    -> PlotOptions b (V p) (N p)
    -> PlotStyle b (V p) (N p)
    -> StyledPlot b (V p) (N p)

type instance V (StyledPlot b v n) = v
type instance N (StyledPlot b v n) = n

instance Functor f => HasPlotOptions f (StyledPlot b v n) b where
  plotOptions f (StyledPlot p opts sty) =
    f opts <&> \opts' -> StyledPlot p opts' sty

instance (Metric v, OrderedField n) => Enveloped (StyledPlot b v n) where
  getEnvelope (StyledPlot p opts _) =
    getEnvelope p & transform (poTransform opts)

instance Functor f => HasPlotStyle f (StyledPlot b v n) b where
  plotStyle f (StyledPlot p opts sty) =
    f sty <&> StyledPlot p opts

-- | Traversal over a raw plot of a styled plot. The type of the plot
--   must match for the traversal to be successful.
styledPlot :: forall p b. Typeable p => Traversal' (StyledPlot b (V p) (N p)) p
styledPlot f s@(StyledPlot p opts sty) =
  case eq p of
    Just Refl -> f p <&> \p' -> StyledPlot p' opts sty
    Nothing   -> pure s
  where eq :: Typeable a => a -> Maybe (a :~: p)
        eq _ = eqT

-- | Give a 'DynamicPlot' a concrete 'PlotStyle'.
styleDynamic :: PlotStyle b v n -> DynamicPlot b v n -> StyledPlot b v n
styleDynamic sty (DynamicPlot (Plot p opts styF)) = StyledPlot p opts (styF sty)

-- | Render a 'StyledPlot' given an and 'AxisSpec'.
renderStyledPlot
  :: TypeableFloat n
  => AxisSpec V2 n
  -> StyledPlot b V2 n
  -> QDiagram b V2 n Any
renderStyledPlot aSpec (StyledPlot p opts sty)
  = renderPlotable aSpec sty p
      & whenever (opts^.hidden) phantom
      & whenever (opts^.clipPlot) (clipTo $ specRect aSpec)

specRect :: TypeableFloat n => AxisSpec V2 n -> Path V2 n
specRect aSpec =
  rect (xU - xL) (yU - yL)
    # moveTo (mkP2 ((xU+xL)/2) ((yU+yL)/2))
    # transform t
  where
  V2 (xL,xU) (yL,yU) = _specBounds aSpec
  t                   = _specTrans aSpec

-- | Get the legend rendered entries from a single styled plot. The
--   resulting entries are in no particular order. See also
--   'styledPlotLegends'.
singleStyledPlotLegend
  :: StyledPlot b v n
  -> [(n, QDiagram b v n Any, String)] -- ^ @(z-order, legend pic, legend text)@
singleStyledPlotLegend (StyledPlot p opts sty) =
  map mk (opts ^. legendEntries)
  where
    mk entry = (entry ^. legendPrecedence, pic, entry ^. legendText)
      where
        pic = case lPic entry of
                DefaultLegendPic  -> defLegendPic sty p
                CustomLegendPic f -> f sty

-- | Render a list of legend entries, in order.
styledPlotLegends
  :: Ord n
  => [StyledPlot b v n]
  -> [(QDiagram b v n Any, String)] -- ^ @[(legend pic, legend text)]@
styledPlotLegends
  = map (\(_,p,t) -> (p,t))
  . sortOn (view _1)
  . concatMap singleStyledPlotLegend

-- XXX taken from "Data.List", defined here because it was only added in
-- base-4.8 (ghc-7.10)
sortOn :: Ord b => (a -> b) -> [a] -> [a]
sortOn f =
  map snd . sortBy (comparing fst) . map (\x -> let y = f x in y `seq` (y, x))