{-# LANGUAGE DeriveDataTypeable    #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes            #-}
{-# LANGUAGE TemplateHaskell       #-}
{-# LANGUAGE TypeFamilies          #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Plots.Axis.Ticks
-- Copyright   :  (C) 2015 Christopher Chalmers
-- License     :  BSD-style (see the file LICENSE)
-- Maintainer  :  Christopher Chalmers
-- Stability   :  experimental
-- Portability :  non-portable
--
-- Ticks for being placed on an axis or a 'ColourBar'.
--
----------------------------------------------------------------------------
module Plots.Axis.Ticks
  ( -- * Major ticks
    MajorTicks
  , HasMajorTicks (..)
  , majorTicksHelper
  , logMajorTicks

    -- * Minor ticks
  , MinorTicks
  , HasMinorTicks (..)
  , minorTicksHelper

    -- * Both major and minor ticks
  , Ticks
  , HasTicks (..)
  , ticksAlign
  , ticksStyle
  , ticksVisible

    -- * Tick alignment
  , TicksAlignment (..)
  , autoTicks
  , centreTicks
  , centerTicks
  , insideTicks
  , outsideTicks

    -- * Helper functions
  , hideTicks
  , majorTickPositions
  , minorTickPositions
  , linearMajorTicks
  ) where

import           Control.Lens     hiding (transform, ( # ))
import           Data.Data
import           Data.Default
import           Data.Foldable    as F
import           Data.Ord
import           Plots.Types
import           Plots.Util

import           Diagrams.Prelude

------------------------------------------------------------------------
-- Types
------------------------------------------------------------------------

-- Tick alignment ------------------------------------------------------

-- | Set the portion of the tick above and below the axis.
data TicksAlignment
  = TickSpec !Rational !Rational
  | AutoTick -- center tick for middle axis, outside tick otherwise
  deriving (Show, Eq)

-- | Set the tick type depending on the axis line position. 'centreTick'
--   for 'middleAxis', 'insideTick' for everything else.
autoTicks :: TicksAlignment
autoTicks = AutoTick

-- | Set the tick to be in the centre of the axis with total length of
--   the corresponding tick length.
centreTicks :: TicksAlignment
centreTicks  = TickSpec 0.5 0.5

-- | Synonym for 'centreTicks'.
centerTicks :: TicksAlignment
centerTicks  = centreTicks

-- | Align the ticks to be inside a box axis.
insideTicks :: TicksAlignment
insideTicks  = TickSpec 0 1

-- | Align the ticks to be outside a box axis.
outsideTicks :: TicksAlignment
outsideTicks = TickSpec 1 0

-- -- | Do not show any ticks.
-- noTicks :: TicksAlignment
-- noTicks = NoTick

------------------------------------------------------------------------
-- Minor ticks
------------------------------------------------------------------------

-- | The big ticks on the axis line.
data MajorTicks v n = MajorTicks
  { matFunction :: (n,n) -> [n]
  , matAlign    :: TicksAlignment
  , matLength   :: n
  , matStyle    :: Style v n
  , matVisible  :: Bool
  }

instance TypeableFloat n => Default (MajorTicks v n) where
  def = MajorTicks
    { matFunction = linearMajorTicks 5
    , matAlign    = autoTicks
    , matLength   = 5
    , matStyle    = mempty # lwO 0.4
    , matVisible  = True
    }

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

-- | Class of things that have a 'MajorTicks'.
class HasMajorTicks f a where
  -- | Lens onto the 'MajorTicks' of something.
  majorTicks :: LensLike' f a (MajorTicks (V a) (N a))

  -- | The function used to place ticks for this axis, given the bounds
  --   of the axis. The result of these major ticks are also used as
  --   guides for 'MinorTicks', 'MajorGridLines' and 'MinorGridLines'.
  --
  --   Default is @'linearMinorTicks' 5@.
  majorTicksFunction :: Functor f => LensLike' f a ((N a, N a) -> [N a])
  majorTicksFunction = majorTicks . lens matFunction (\mat a -> mat {matFunction = a})

  -- | Alignment of the major ticks. Choose between 'autoTicks'
  --   (default), 'centreTicks', 'insideTicks' or 'outsideTicks'.
  majorTicksAlignment :: Functor f => LensLike' f a TicksAlignment
  majorTicksAlignment = majorTicks . lens matAlign (\mat a -> mat {matAlign = a})

  -- | The total length the major ticks.
  --
  --   Default is @7@.
  majorTicksLength :: Functor f => LensLike' f a (N a)
  majorTicksLength = majorTicks . lens matLength (\mat a -> mat {matLength = a})

  -- | The style used to render the major ticks.
  --
  --   Default is @'lwO' 0.6 'mempty'@ (subject to change).
  majorTicksStyle :: Functor f => LensLike' f a (Style (V a) (N a))
  majorTicksStyle = majorTicks . lens matStyle (\mat sty -> mat {matStyle = sty})

instance HasMajorTicks f (MajorTicks v n) where
  majorTicks = id

instance HasVisibility (MajorTicks v n) where
  visible = lens matVisible (\mat b -> mat {matVisible = b})

------------------------------------------------------------------------
-- Minor ticks
------------------------------------------------------------------------

-- | The small ticks on the axis line.
data MinorTicks v n = MinorTicks
  { mitFunction :: [n] -> (n,n) -> [n]
  , mitAlign    :: TicksAlignment
  , mitLength   :: n
  , mitStyle    :: Style v n
  , mitVisible  :: Bool
  }

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

instance TypeableFloat n => Default (MinorTicks v n) where
  def = MinorTicks
    { mitFunction = minorTicksHelper 4
    , mitAlign    = autoTicks
    , mitLength   = 3
    , mitStyle    = mempty # lwO 0.4
    , mitVisible  = True
    }

-- | Class of things that have a single 'MinorTicks'.
class HasMinorTicks f a where
  -- | Lens onto the 'MinorTicks' of something.
  minorTicks :: LensLike' f a (MinorTicks (V a) (N a))

  -- | The function used to place ticks for this axis, given the result
  --   of 'majorTicksFunction' and the bounds of the axis.
  --
  --   Default is @'linearMinorTicks' 3@.
  minorTicksFunction :: Functor f => LensLike' f a ([N a] -> (N a, N a) -> [N a])
  minorTicksFunction = minorTicks . lens mitFunction (\mit a -> mit {mitFunction = a})

  -- | Alignment of the minor ticks. Choose between 'autoTicks'
  --   (default), 'centreTicks', 'insideTicks' or 'outsideTicks'.
  minorTicksAlignment :: Functor f => LensLike' f a TicksAlignment
  minorTicksAlignment = minorTicks . lens mitAlign (\mit a -> mit {mitAlign = a})

  -- | The total length the minor ticks.
  --
  --   Default is @3@.
  minorTicksLength :: Functor f => LensLike' f a (N a)
  minorTicksLength = minorTicks . lens mitLength (\mit a -> mit {mitLength = a})

  -- | The style used to render the minor ticks.
  --
  --   Default is @'lwO' 0.4 'mempty'@ (subject to change).
  minorTicksStyle :: Functor f => LensLike' f a (Style (V a) (N a))
  minorTicksStyle = minorTicks . lens mitStyle (\mit sty -> mit {mitStyle = sty})

instance HasMinorTicks f (MinorTicks v n) where
  minorTicks = id

instance HasVisibility (MinorTicks v n) where
  visible = lens mitVisible (\mit sty -> mit {mitVisible = sty})

------------------------------------------------------------------------
-- Both ticks
------------------------------------------------------------------------

-- | Both 'MajorTicks' and 'MinorTicks' together.
data Ticks v n = Ticks (MajorTicks v n) (MinorTicks v n)
-- Ticks are originally split up into major and minor so we can reuse
-- major for the colour bar. I'm still undecided whether it's worth all
-- the extra boilerplate here.

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

-- | Class of things with both 'MajorTicks' and 'MinorTicks'.
class (HasMinorTicks f a, HasMajorTicks f a) => HasTicks f a where
  bothTicks :: LensLike' f a (Ticks (V a) (N a))

instance Functor f => HasTicks f (Ticks v n) where
  bothTicks = id

instance Functor f => HasMajorTicks f (Ticks v n) where
  majorTicks f (Ticks ma mi) = f ma <&> \ma' -> Ticks ma' mi

instance Functor f => HasMinorTicks f (Ticks v n) where
  minorTicks f (Ticks ma mi) = f mi <&> \mi' -> Ticks ma mi'

instance TypeableFloat n => Default (Ticks v n) where
  def = Ticks def def

instance Typeable n => HasStyle (Ticks v n) where
  applyStyle s = over ticksStyle (applyStyle s)

-- | Traversal over both major and minor tick alignment.
ticksAlign :: (HasTicks f a, Applicative f) => LensLike' f a TicksAlignment
ticksAlign = bothTicks . aligns
  where
    aligns f a = (\m mn -> a & majorTicksAlignment .~ m & minorTicksAlignment .~ mn)
                  <$> f (a ^. majorTicksAlignment) <*> f (a ^. minorTicksAlignment)

-- | Traversal over both major and minor tick styles.
ticksStyle :: (HasTicks f a, Applicative f) => LensLike' f a (Style (V a) (N a))
ticksStyle = bothTicks . styles
  where
    styles f a = (\m mn -> a & majorTicksStyle .~ m & minorTicksStyle .~ mn)
              <$> f (a ^. majorTicksStyle) <*> f (a ^. minorTicksStyle)

-- | Traversal over the visibility of both major and minor ticks.
ticksVisible :: (HasTicks f a, Applicative f) => LensLike' f a Bool
ticksVisible = bothTicks . visibles
  where
    visibles f a = (\m mn -> a & majorTicks . visible .~ m & minorTicks. visible .~ mn)
              <$> f (a ^. majorTicks . visible) <*> f (a ^. minorTicks . visible)

-- | Hides the 'Minor' ticks when trying to render something. This can
--   be used on multiple types:
--
-- @
-- 'hideTicks' :: 'Axis' b c n       -> 'Axis' b c n
-- 'hideTicks' :: 'SingleAxis' b v n -> 'SingleAxis' b v n
-- 'hideTicks' :: 'Ticks' v n        -> 'Ticks' v n
-- 'hideTicks' :: 'MinorTicks' v n   -> 'MinorTicks' v n
-- @
hideTicks :: HasTicks Identity a => a -> a
hideTicks = ticksVisible .~ False

-- | Setter over the final positions the major ticks. This is not as
--   general as 'majorTicksFunction' because you don't have access to
--   the bounds but it can be useful when you know exactly what ticks
--   you want to add or modify existing tick positions.
majorTickPositions
  :: (HasMajorTicks f a, Settable f)
  => LensLike' f a [N a]
majorTickPositions = majorTicksFunction . mapped

-- | Setter over the final positions the major ticks. This is not as
--   general as 'minorTicksFunction' because you don't have access to
--   the bounds but it can be useful when you know exactly what ticks
--   you want to add or modify existing tick positions.
minorTickPositions
  :: (HasMinorTicks f a, Settable f)
  => LensLike' f a [N a]
minorTickPositions = minorTicksFunction . mapped . mapped

------------------------------------------------------------------------
-- Calculating ticks
------------------------------------------------------------------------

-- Linear ticks --------------------------------------------------------

-- | Ticks whose value ends in 1, 0.5, 0.25, 0.2 (*10^n).
linearMajorTicks :: (RealFrac n, Floating n) => n -> (n, n) -> [n]
linearMajorTicks = majorTicksHelper [1, 0.5, 0.25, 0.2, 0.3]

-- Logarithmic ticks ---------------------------------------------------

-- | Place n ticks at powers of 10 on the axis.
logMajorTicks :: (RealFrac n, Floating n) => n -> (n, n) -> [n]
logMajorTicks n (a,b) =
  -- Logarithmic ticks are just like linear ticks but in a different domain.
  map (10**) $ majorTicksHelper ts n (log10 (max 2 a), log10 b)
    where ts = [1,2,3,4,5,6,7,8,9]

-- Ticks helpers -------------------------------------------------------

-- | Place n linear spaced ticks between each major tick.
minorTicksHelper
  :: Fractional n
  => Int    -- ^ Number of minor ticks between each major tick
  -> [n]    -- ^ Positions of major ticks
  -> (n, n) -- ^ Bounds
  -> [n]    -- ^ Minor tick positions
minorTicksHelper n ts _ = F.concat $ go ts where
  -- we won't want x1 and x2 to be minor ticks too we init/tail them.
  go (x1:x2:xs) = (init . tail) (enumFromToN x1 x2 (n+2)) : go (x2:xs)
  go _          = []

-- | Choose ticks whose step size is a multiple of 10 of the allowed
--   numbers and tries to match the number of desired ticks.
--
--   Note that the resulting tick positions may go out of the range of
--   the bounds. This is so the minor ticks can be chosen correctly if a
--   tick doesn't end exactly on a bound. When we render, we ignore all
--   ticks outside the bounds.
majorTicksHelper
  :: (RealFrac n, Floating n)
  => [n]    -- ^ Allowed numbers (up to powers of 10)
  -> n      -- ^ desired number of ticks
  -> (n, n) -- ^ bounds
  -> [n]    -- ^ tick positions
majorTicksHelper ts0 n (a,b) = iterateN n' (+h) a'
  where
  i  = fromIntegral (floor ( a / h ) :: Int)

  a' = i*h
  n' = ceiling ((b - a')/h) + 1

  -- Find the a value from our potential ticks that's closest to our
  -- ideal height.
  h  = minimumBy (comparing $ abs . (h' -)) ts'

  -- Ideal height for the desired number of ticks.
  h' = d / n

  -- Potential step heights that look nice and are in a suitable range
  -- for the axis bounds.
  ts' = map (* 10 ^^ (floor $ log10 d :: Int)) (ts0 ++ map (*10) ts0)
  d   = abs $ b - a

-- logged :: Floating a => Iso' a a
-- logged = iso log10 (10**)

log10 :: Floating a => a -> a
log10 = logBase 10