{-# LANGUAGE DeriveFunctor, TypeSynonymInstances, FlexibleInstances #-}
{-| Curves is an easy to use library for creating images. The basic
    primitive is a curve, which, in the simplest case, is a continuous function
    from a 'Scalar' parameter to a 2-dimensional 'Point' on the 'curve'. Images
    are rendered ('renderImage') as PNG images.
-}
module Graphics.Curves
  (
    module Graphics.Curves.Math
  , module Graphics.Curves.Colour
    -- * Image
  , Image
    -- ** Curves
  , point, line, lineStrip, poly, circle, circleSegment
    -- ** Advanced curves
  , curve, curve_, curve'
  , bSpline, bSpline', closedBSpline
  , bezier, bezierSegment
    -- ** Operating on curves
  , reverseImage
  , (+++), (+.+), (<++), (++>)
  , differentiate, mapImage, zipImage, transformImage
  , curveLength
    -- ** Advanced image manipulation
  , freezeImageSize, freezeImageOrientation, freezeImage, freezeImageStyle
  , unfreezeImage
    -- ** Combining images
  , BlendFunc
  , combine, mapColour
  , unionBlend, intersectBlend, diffBlend
  , (<>)
  , (><), (<->)
    -- ** Query functions
  , imageBounds, sampleImage
    -- * Image attributes
    -- | Image attributes control things like the colour and width of curves.
  , module Graphics.Curves.Attribute
  , module Graphics.Curves.Style
    -- * Rendering
  , autoFit, autoStretch
  , renderImage
    -- * Other
  , version
  )
  where

import Graphics.Curves.Math
import Graphics.Curves.BoundingBox
import Graphics.Curves.Curve
import Graphics.Curves.Image
import Graphics.Curves.Colour
import Graphics.Curves.Render hiding (sampleImage)
import Graphics.Curves.Compile
import Graphics.Curves.Attribute
import Graphics.Curves.Style

import Data.Version (showVersion)
import qualified Paths_curves as Paths

-- | Scale the an image to fit inside the the box given by the two points
--   (bottom-left and top-right corners).
autoFit :: Point -> Point -> Image -> Image
autoFit p q = loop 0
  where
    -- Repeat autoFit until reasonably stable. This makes it work for features
    -- that are scaling insensitive (line widths and frozen images).
    loop oldk i
      | abs (k - 1) < 0.01    = i'
      | abs (oldk - k) < 0.01 = i'  -- not making progress
      | otherwise             = loop k i'
      where
        (k, i') = autoFit' p q i

autoFit' :: Point -> Point -> Image -> (Scalar, Image)
autoFit' p0 p1 i =
  (getX k, translate (p0 - q0 + offs) $ scaleFrom q0 k i)
  where
    Seg q0 q1 = bboxToSegment $ bounds $ compileImage i
    screen = p1 - p0
    world  = q1 - q0
    k      = diag $ vuncurry min (screen / world)
    world' = k * world
    offs   = 0.5 * (screen - world')

-- | Scale the an image to fit inside the the box given by the two points
--   (bottom-left and top-right corners). Does not preserve aspect ratio.
autoStretch :: Point -> Point -> Image -> Image
autoStretch p q = loop 0
  where
    -- Repeat autoStretch until reasonably stable. This makes it work for features
    -- that are scaling insensitive (line widths and frozen images).
    loop oldk i
      | abs (getX k - 1) < 0.01 &&
        abs (getY k - 1) < 0.01      = i'
      | getX (abs $ oldk - k) < 0.01 = i'  -- not making progress
      | otherwise                    = loop k i'
      where
        (k, i') = autoStretch' p q i

autoStretch' :: Point -> Point -> Image -> (Vec, Image)
autoStretch' p0 p1 i =
  (k, translate (p0 - q0 + offs) $ scaleFrom q0 k i)
  where
    Seg q0 q1 = bboxToSegment $ bounds $ compileImage i
    screen = p1 - p0
    world  = q1 - q0
    k      = screen / world
    world' = k * world
    offs   = 0.5 * (screen - world')

-- | Compute the bounds of an image, returning a line segment from the bottom
--   left corner to the top right corner of the bounding box. This function
--   ignores line widths. Note that using pixel based features (for instance,
--   produced by 'freezeImageSize') means that the bounds may become invalid if
--   the image is scaled.
imageBounds :: Image -> Segment
imageBounds i0
  | d < 50    = getBounds (50 / d) i
  | otherwise = s
  where
    i = i0 `with` [LineWidth := 0, LineBlur := 0, FillBlur := 0]
    s@(Seg p q) = getBounds 1 i
    d = vuncurry max (q - p)
    getBounds k' i = scale (1/k) $ bboxToSegment $ bounds $ compileImage $ scale k i
      where k = diag k'

sampleImage :: Image -> Scalar -> [[Point]]
sampleImage IEmpty t                 = []
sampleImage (Combine _ i j) t        = sampleImage i t ++ sampleImage j t
sampleImage (ICurve (Curves cs _)) t = [map (sampleCurve t) cs]
  where
    sampleCurve t (Curve f g _ _) = g t (f t)

-- | Freeze the line style of an image. This means that the pixel parameters
--   (distance along the curve and pixel position) are given as they are at
--   this moment, and won't be affected by later transformations.
freezeImageStyle :: Image -> Image
freezeImageStyle i = mapCurve (freezeLineStyle res) i
  where res = vuncurry min (p1 - p0) / 100
        Seg p0 p1 = imageBounds i

-- ImageElement -----------------------------------------------------------

class Transformable a => ImageElement a where
  toImage :: a -> Image

instance ImageElement Image where
  toImage = id

instance ImageElement Segment where
  toImage (Seg p q) = line p q

instance ImageElement Vec where
  toImage = point

version :: String
version = showVersion Paths.version