{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, FlexibleContexts, UndecidableInstances,
             RankNTypes #-}
module Graphics.Curves.Image
  ( module Graphics.Curves.Image
  , (<>) )
  where

import Control.Applicative
import Data.Monoid
import Data.List
import Data.Maybe

import Graphics.Curves.Math
import Graphics.Curves.Colour
import Graphics.Curves.Curve
import Graphics.Curves.BoundingBox
import Graphics.Curves.Attribute

type Op a = a -> a -> a

-- TODO: - explicit transformation matrices (for efficiency?)
-- | The image type.
data Image = ICurve Curves
           | Combine (Op (Maybe Colour)) Image Image
           | IEmpty

-- Image operations -------------------------------------------------------

-- | A blend function is used to compute the resulting colour when 'combine'ing
--   two images.
type BlendFunc = Maybe Colour -> Maybe Colour -> Maybe Colour

-- | Alpha 'blend' the first colour on top of the second colour.
unionBlend :: BlendFunc
unionBlend c1 c2 = case (c1, c2) of
  (Nothing, c)       -> c
  (c, Nothing)       -> c
  (Just c1, Just c2) -> Just (blend c1 c2)

-- | The alpha value of the result is the product of the alpha values of the
--   two inputs.
intersectBlend :: BlendFunc
intersectBlend c1 c2 = case (c1, c2) of
  (_, Nothing)       -> Nothing
  (Nothing, _)       -> Nothing
  (Just c1, Just c2) -> Just $ setAlpha (getAlpha c2 * getAlpha c1) (blend c1 c2)

-- | Multiplies the alpha value of the first colour by 1 - the alpha value of
--   the second colour.
diffBlend :: BlendFunc
diffBlend c c' = case c' of
  Nothing -> c
  Just c' -> opacity (1 - getAlpha c') <$> c

-- | 'mappend' = 'combine' 'unionBlend'
instance Monoid Image where
  mempty      = IEmpty
  mappend a b = Combine unionBlend a b

-- | Combine two images using the specified blend function.
combine :: BlendFunc -> Image -> Image -> Image
combine f a b = Combine f a b

-- | Map a function over the colours of an image.
mapColour :: (Colour -> Colour) -> Image -> Image
mapColour f = combine (const $ fmap f) IEmpty

infixr 7 ><
infixl 8 <->

-- | The intersection of two images.
--
-- > (><) = combine intersectBlend
(><) :: Image -> Image -> Image
a >< b = combine intersectBlend a b

-- | Subtract the second image from the first.
--
-- > (<->) = combine diffBlend
(<->) :: Image -> Image -> Image
a <-> b = combine diffBlend a b

-- | A simple curve whose points are given by the function argument. The first
--   two arguments specify the range of the function. The function must be
--   continuous on this interval.
--
--   For example, a straight line between points @p@ and @q@ can be implemented as
--
--   @curve 0 1 ('interpolate' p q)@
curve :: Scalar -> Scalar -> (Scalar -> Point) -> Image
curve t0 t1 f = curve' t0 t1 f (const id)

-- | @curve_ = 'curve' 0 1@
curve_ :: (Scalar -> Point) -> Image
curve_ = curve 0 1

-- | The most general form of curve. The curve function is split in two, one
--   function from the parameter to an arbitrary 'Transformable' object, and a
--   second function from this object (and the parameter value) to a point on
--   the curve. The power of this combinator comes from the fact that
--   transformations (e.g. 'translate', 'scale', 'rotate') of the curve apply
--   only to the (result of the) first function. This means that the points
--   computed by the second function are measured in pixels of the final image.
--
--   For an example, see the 'Graphics.Curves.Geometry.arrow' combinator,
--   which uses a line 'Segment' as the intermediate type and computes the
--   arrow head in the second function, to ensure that the arrow head has the
--   same dimensions regardless of how the arrow is scaled.
curve' :: Transformable a => Scalar -> Scalar -> (Scalar -> a) -> (Scalar -> a -> Point) -> Image
curve' t0 t1 f g = ICurve $ Curves [Curve (f . tr) (g . tr) (\_ _ _ -> defaultCurveLineStyle) 1] defaultCurveFillStyle
  where
    tr t = t0 + t * (t1 - t0)

-- | Compute the length of the curves of an image by approximating it by a
--   series of straight-line segments, each no longer than specified by the
--   first argument.
curveLength :: Scalar -> Image -> Scalar
curveLength _ IEmpty                 = 0
curveLength r (ICurve (Curves cs _)) = sum $ map (curveLength' r) cs
curveLength r (Combine _ i j)        = curveLength r i + curveLength r j

mapCurves :: (Curves -> Curves) -> Image -> Image
mapCurves f IEmpty          = IEmpty
mapCurves f (ICurve c)      = ICurve (f c)
mapCurves f (Combine b i j) = Combine b (mapCurves f i) (mapCurves f j)

mapCurve :: (Curve -> Curve) -> Image -> Image
mapCurve f = mapCurves (\(Curves cs s) -> Curves (map f cs) s)

-- | Reverse the direction of all curves in an image. Useful in conjunction
--   with '+++'.
reverseImage :: Image -> Image
reverseImage = mapCurve reverseCurve

-- | Freeze the size of an image around the given point. Scaling the image will
--   only affect the position of the image, not the size. Translation and
--   rotation affect the image normally.
--
--   @'scaleFrom' p ('diag' k) (freezeImageSize p i) == freezeImageSize p i@
--
--   Scaling with non-uniform scale factors will still distort the image,
--   however.
freezeImageSize :: Point -> Image -> Image
freezeImageSize p = mapCurve (freezeCurve fr p)
  where
    fr = Freeze{ freezeSize = True, freezeOrientation = False }

-- | Freeze image orientation. Rotations of the image will only affect the
--   position of the image, not its orientation. Translation and scaling
--   affect the image normally.
--
--   @'rotateAround' p a (freezeImageOrientation p i) == freezeImageOrientation p i@
freezeImageOrientation :: Point -> Image -> Image
freezeImageOrientation p = mapCurve (freezeCurve fr p)
  where
    fr = Freeze{ freezeSize = False, freezeOrientation = True }

-- | Freeze both the size and the orientation of an image.
--
-- @freezeImage p i == 'freezeImageSize' p i ('freezeImageOrientation' p i)@
freezeImage :: Point -> Image -> Image
freezeImage p = mapCurve (freezeCurve fr p)
  where
    fr = Freeze{ freezeSize = True, freezeOrientation = True }

-- | Unfreeze an image. After unfreezing any frozen features will be affected
--   by transformations again.
unfreezeImage :: Image -> Image
unfreezeImage = mapCurve unfreeze
  where
    unfreeze (Curve f g st n) = Curve (\t -> g t (f t)) (const id) st n

instance HasAttribute CurveAttribute Image where
  modifyAttribute attr f = mapCurves (modifyAttribute attr f)

instance Transformable Image where
  transform f = mapCurves (transform f)

infixl 9 ++>
infixr 8 +++, <++

-- | Join the right-most curve of the first image to the left-most curve of the
--   second image. The 'Graphics.Curves.Style.Style' is inherited from the
--   curve of the first image. If the end point of the first curve does not
--   coincide with the starting point of the second curve a straight line is
--   added to connect the two. This combinator is useful when using
--   parameterized line styles (such as 'Graphics.Curves.Style.dashed').
(+++) :: Image -> Image -> Image
ICurve c1     +++ ICurve c2     = ICurve $ joinCurve c1 c2
i             +++ IEmpty        = i
IEmpty        +++ i             = i
Combine f i j +++ c             = Combine f i (j +++ c)
c             +++ Combine f i j = Combine f (c +++ i) j

-- | Prepend a point to the left-most curve of an image. @p <++ i@ is equivalent
--   to  @'line' p q '+++' i@ if @q@ is the starting point of the left-most
--   curve of @i@.
(<++) :: Point -> Image -> Image
p <++ ICurve c      = ICurve $ prependPoint p c
p <++ Combine b i j = Combine b (p <++ i) j
p <++ IEmpty        = point p

-- | Append a point to the right-most curve of an image. @i ++> p@ is
--   equivalent to @i '+++' 'line' q p@ if @q@ is the end point of the right-most
--   curve of @i@.
(++>) :: Image -> Point -> Image
ICurve cs     ++> p = ICurve $ appendPoint cs p
IEmpty        ++> p = point p
Combine b i j ++> p = Combine b i (j ++> p)

-- | Like '+++' but doesn't join the end points of the curves.
(+.+) :: Image -> Image -> Image
ICurve (Curves cs1 s) +.+ ICurve (Curves cs2 _) = ICurve $ Curves (cs1 ++ cs2) s
i                     +.+ IEmpty                = i
IEmpty                +.+ i                     = i
Combine f i j         +.+ c                     = Combine f i (j +.+ c)
c                     +.+ Combine f i j         = Combine f (c +.+ i) j

-- | A straight line between two points.
line :: Point -> Point -> Image
line p q = curve_ (interpolate p q)

-- | A single point.
point :: Point -> Image
point p = curve_ (const p)

-- | A circle given by its center and radius.
circle :: Point -> Scalar -> Image
circle p r = circleSegment p r 0 (2 * pi)

-- | A circle segment. The third and fourth arguments are the start and end
--   angle of the segment. If the start angle is bigger than the end angle it's
--   the clockwise segment, otherwise the counterclockwise segment. For instance,
--   @circleSegment 0 1 0 pi@ is the top half circle starting in 'unitX' and
--   ending in @-'unitX'@, whereas @circleSegment 0 1 0 (-pi)@ is the bottow
--   half circle with the same start and end points.
circleSegment :: Point -> Scalar -> Scalar -> Scalar -> Image
circleSegment c r a b | b < a = reverseImage $ circleSegment c r b a
circleSegment (Vec x y) r a b =
  curve a b (\α -> Vec (x + r * cos α) (y + r * sin α))

-- | A connected sequence of straight lines. The list must have at least two
--   elements.
lineStrip :: [Point] -> Image
lineStrip [] = error "lineStrip: []"
lineStrip [p]       = point p
lineStrip [p, q]    = line p q
lineStrip [p, q, r] = p <++ line q r
lineStrip ps        = lineStrip qs +++ lineStrip rs
  where
    (qs, rs) = splitAt (div (length ps) 2) ps

-- | A polygon.
--
-- > poly ps = lineStrip (ps ++ [head ps])
poly :: [Point] -> Image
poly (p:ps) = lineStrip ([p] ++ ps ++ [p])
poly [] = error "poly: []"

-- | Differentiating the curves of an image
differentiate :: Image -> Image
differentiate = mapCurve differentiateCurve

-- | Apply a function to all points of an image. The function also gets the
--   curve parameter (between 0 and 1) of the given point. This applies after
--   all transformations so the points are measured in pixels, unless the image
--   is later unfrozen with 'unfreezeImage'.
mapImage :: (Scalar -> Point -> Point) -> Image -> Image
mapImage h = mapCurve pp
  where
    pp (Curve f g st n) = Curve f (\t -> h t . g t) st n

-- | Apply a transformation to an image. Unlike 'mapImage' the transformation
--   is applied immediately.
transformImage :: (forall a. Transformable a => Scalar -> a -> a) -> Image -> Image
transformImage h = mapCurve pp
  where
    pp (Curve f g st n) = Curve (\t -> h t (f t)) g st n

-- | Zipping two images. Both images must have the same number of curves
--   'combine'd in the same order. As with 'mapImage' the zipping takes place
--   after all transformations.
zipImage :: (Scalar -> Point -> Point -> Point) -> Image -> Image -> Image
zipImage f (ICurve c) (ICurve c') = ICurve (zipCurves f c c')
zipImage f IEmpty IEmpty = IEmpty
zipImage f (Combine g a b) (Combine _ c d) =
  Combine g (zipImage f a c) (zipImage f b d)

-- B-Splines --------------------------------------------------------------

-- | A <http://en.wikipedia.org/wiki/B-spline#Uniform_cubic_B-splines uniform cubic B-spline>
--   with the given control points.
bSpline :: [Point] -> Image
bSpline ps = foldl1 (+++) $ map seg (takeWhile ((>=4).length) $ map (take 4) (tails ps))
  where
    m = map (map (/ 6)) [[-1, 3, -3, 1], [3, -6, 0, 4], [-3, 3, 3, 1], [1, 0, 0, 0]]
    coefs t = map diag $ mmul [t^3, t^2, t, 1] m
    mmul v m = map (vmul v) m
    vmul u v = sum $ zipWith (*) u v

    seg ps = curve_ f
      where
        f t = vmul (coefs t) ps

-- | A closed B-spline.
--
-- > closedBSpline ps = bSpline (ps ++ take 3 ps)
closedBSpline :: [Point] -> Image
closedBSpline ps = bSpline $ ps ++ take 3 ps

-- | A B-spline which starts in the first control point and ends in the last
-- control point. This is achieved by adding two extra copies of the first and
-- last points.
bSpline' (p:ps) = bSpline $ p:p:p:ps ++ replicate 2 (last (p:ps))
bSpline' [] = error "bSpline': empty list"

-- Bézier curves ----------------------------------------------------------

-- | A Bézier curve of degree n with the given control points @[p0 .. pn]@.
bezierSegment :: [Point] -> Image
bezierSegment []  = error "bezierSegment: empty list"
bezierSegment [p] = point p
bezierSegment ps  = zipImage (\t p q -> interpolate p q t) (bezierSegment (init ps)) (bezierSegment (tail ps))

-- | A strip of cubic Bézier curves.
bezier :: [Point] -> Image
bezier ps | n < 4 || mod n 3 /= 1 = error "bezier: needs 3k + 1 points (k > 0)"
  where n = length ps
bezier ps = foldr1 (+++) (map bezierSegment $ quads ps)
  where
    quads [p] = []
    quads (p0:p1:p2:p3:ps) = [p0, p1, p2, p3] : quads (p3:ps)