{-# LANGUAGE FlexibleContexts     #-}
{-# LANGUAGE FlexibleInstances    #-}
{-# LANGUAGE GADTs                #-}
{-# LANGUAGE ScopedTypeVariables  #-}
{-# LANGUAGE TypeFamilies         #-}
{-# LANGUAGE UndecidableInstances #-}

module Diagrams.Deform (Deformation(..), Deformable(..), asDeformation) where

import           Control.Lens        (under, _Unwrapped)
import           Data.AffineSpace
import           Data.Basis
import           Data.MemoTrie
import           Data.Monoid         hiding ((<>))
import           Data.Semigroup
import           Data.VectorSpace

import           Diagrams.Core
import           Diagrams.Located
import           Diagrams.Parametric
import           Diagrams.Path
import           Diagrams.Segment
import           Diagrams.Trail

------------------------------------------------------------
-- Deformations

-- | @Deformations@ are a superset of the affine transformations
-- represented by the 'Transformation' type.  In general they are not
-- invertable.  @Deformation@s include projective transformations.
-- @Deformation@ can represent other functions from points to points
-- which are "well-behaved", in that they do not introduce small wiggles.
data Deformation v = Deformation (Point v -> Point v)

instance Semigroup (Deformation v) where
    (Deformation p1) <> (Deformation p2) = Deformation (p1 . p2)

instance Monoid (Deformation v) where
    mappend = (<>)
    mempty = Deformation id

class Deformable a where
    -- | @deform' epsilon d a@ transforms @a@ by the deformation @d@.
    -- If the type of @a@ is not closed under projection, approximate
    -- to accuracy @epsilon@.
    deform' :: Scalar (V a) -> Deformation (V a) -> a -> a

    -- | @deform d a@ transforms @a@ by the deformation @d@.
    -- If the type of @a@ is not closed under projection, @deform@
    -- should call @deform'@ with some reasonable default value of
    -- @epsilon@.
    deform  :: Deformation (V a) -> a -> a

-- | @asDeformation@ converts a 'Transformation' to a 'Deformation' by
-- discarding the inverse transform.  This allows reusing
-- @Transformation@s in the construction of @Deformation@s.
asDeformation
  ::  ( HasTrie (Basis v), HasBasis v) => Transformation v -> Deformation v
asDeformation t = Deformation f' where
      f' = papply t

------------------------------------------------------------
-- Instances

instance Deformable (Point v) where
    deform' = const deform
    deform (Deformation l) = l

-- | Cubic curves are not closed under perspective projections.
-- Therefore @Segment@s are not an instance of Deformable.  However,
-- the deformation of a @Segment@ can be approximated to arbitrary
-- precision by a series of @Segment@s.  @deformSegment@ does this,
-- which allows types built from lists of @Segment@s to themselves be
-- @Deformable@.
deformSegment :: (VectorSpace v, InnerSpace v, s ~ Scalar v, Ord s, Fractional s, Floating s) =>
                  s -> Deformation v -> FixedSegment v -> [FixedSegment v]
deformSegment epsilon t s
    | goodEnough epsilon t s = [approx t s]
    | otherwise  = concatMap (deformSegment epsilon t) [s1, s2]
  where
    (s1, s2) = splitAtParam s 0.5

approx :: (VectorSpace v, InnerSpace v, s ~ Scalar v, Ord s, Fractional s, Floating s) =>
          Deformation v -> FixedSegment v -> FixedSegment v
approx t (FLinear p0 p1) = FLinear (deform t p0) (deform t p1)
approx t (FCubic p0 c1 c2 p1) = FCubic (f p0) (f c1) (f c2) (f p1) where
      f = deform t

goodEnough :: (VectorSpace v, InnerSpace v, s ~ Scalar v, Ord s, Fractional s, Floating s) =>
              s -> Deformation v -> FixedSegment v -> Bool
goodEnough e t s =
    all (< e) [magnitude $ deform t (s `atParam` u) .-. approx t s `atParam` u
              | u <- [0.25, 0.5, 0.75]]

instance (VectorSpace v, InnerSpace v,
          s ~ Scalar v, Ord s, Fractional s, Floating s, Show s, Show v) =>
         Deformable (Located (Trail v)) where
             deform' eps p t
                 | isLine $ unLoc t  = line `at` p0
                 | otherwise = glueTrail line `at` p0
               where
                 segs = concatMap (deformSegment eps p) $ fixTrail t
                 p0 = case segs of
                     (FLinear start _:_) -> start
                     (FCubic start _ _ _:_) -> start
                     _      -> loc t  -- default in case of empty trail
                 line = trailFromSegments $ map (unLoc . fromFixedSeg) segs
             deform p t = deform' (0.01 * extent) p t where
                           -- estimate the "size" of the Trail' as
                           -- the maximum distance to any vertex
                extent = maximum . map dist . trailVertices $ t
                dist pt = magnitude $ pt .-. loc t

instance (VectorSpace v, InnerSpace v,
          s ~ Scalar v, Ord s, Fractional s, Floating s, Show s, Show v) =>
         Deformable (Path v) where
             deform' eps p = under _Unwrapped $ map (deform' eps p)
             deform p = under _Unwrapped $ map (deform p)