module RSAGL.Color.ColorSpace
    (ColorSpace(..),
     ImportColorCoordinates(..),
     ExportColorCoordinates(..),
     AffineColorSpace,
     ColorWheel,
     ColorChannel,
     ChannelIndex,
     LinearMetric(..),
     newChannel,
     newAngularChannel,
     newRadialChannel,
     newMaximalChannel,
     viewChannel,
     channel_u,
     channel_v,
     channel_w,
     newColorSpace,
     newColorWheel,
     color_space_rgb,
     color_wheel_rgbl,
     transformColorFromTo,
     transformColor)
    where

import RSAGL.Math.Types
import RSAGL.Math.Matrix
import RSAGL.Math.Vector
import RSAGL.Math.AbstractVector
import RSAGL.Math.Affine
import Data.Vec (nearZero)
import RSAGL.Math.Angle
import Control.Arrow (first,second)

-- | An affine transformation from the default RGB color space
-- to the specified color space.
newtype AffineColorSpace = AffineColorSpace Matrix
    deriving (Show)

-- | A rotatable color space.
newtype ColorWheel = ColorWheel Matrix
    deriving (Show)

-- | A specific component of a 3-channel color space.
data ColorChannel =
    -- A color space in which the 'u_channel' is the channel in question.
    LinearChannel Matrix
    -- | A channel that depends on the input color.
  | Preconfigure (Color -> ColorChannel)

-- | 'channel_u', 'channel_v', 'channel_w', of a 3-channel color space.
data ChannelIndex = ChannelIndex { channel_index :: Matrix }

-- | The first channel of a color space represented by the ordered tripple,
-- @(u,v,w)@.
channel_u :: ChannelIndex
channel_u = ChannelIndex identity_matrix

-- | The second channel of a color space represented by the ordered tripple,
-- @(u,v,w)@.
channel_v :: ChannelIndex
channel_v = ChannelIndex $ matrix
    [[0,1,0,0],
     [1,0,0,0],
     [0,0,1,0],
     [0,0,0,1]]

-- | The third channel of a color space represented by the ordered tripple,
-- @(u,v,w)@.
channel_w :: ChannelIndex
channel_w = ChannelIndex $ matrix
   [[0,0,1,0],
    [0,1,0,0],
    [1,0,0,0],
    [0,0,0,1]]

-- | Pick a channel from a color space.
newChannel :: (ColorSpace cs) => ChannelIndex -> cs -> ColorChannel
newChannel (ChannelIndex ch_ix) cs = LinearChannel $ ch_ix `matrixMultiply` m
    where (AffineColorSpace m) = affineColorSpaceOf cs

-- | Construct an isotropic 'ColorChannel' that runs along a
-- hue angle.  The meaning of the hue angle depends on the
-- primary colors used in the construction of the color wheel.
newAngularChannel :: ColorWheel -> Angle -> ColorChannel
newAngularChannel (ColorWheel m) a = LinearChannel $
        rotationMatrix (Vector3D 0 0 1) (scalarMultiply (-1) a)
    `matrixMultiply`
        m

-- | Construct an isotropic 'ColorChannel' along the radii
-- of a color wheel.  This is exactly like calling
-- 'newAngularChannel' knowing in advance the specific
-- hue of the color in question.
newRadialChannel :: ColorWheel -> ColorChannel
newRadialChannel (ColorWheel m) = Preconfigure $ \c ->
    let Point3D u v w = exportColorCoordinates c $ AffineColorSpace m
        (a,_) = cartesianToPolar (u,v)
        in newAngularChannel (ColorWheel m) a

-- | Construct a 'ColorChannel' representing the maximum
-- of the three color components.  For example, the
-- maximum of @RGB 0.25 0.5 0.4@ is 0.5.
newMaximalChannel :: AffineColorSpace -> ColorChannel
newMaximalChannel cs@(AffineColorSpace m) = Preconfigure $ \c ->
    let Point3D u v w = exportColorCoordinates c cs
        maxi = maximum [abs u,abs v, abs w]
        magn = magnitude [u,v,w]
        in LinearChannel $
               scale' (maxi / if nearZero magn then 1.0 else magn) $
                   rotateToFrom (Vector3D 1 0 0) (Vector3D u v w) $
                       transform m identity_matrix

-- | A view of a specific color channel, such as red, or luminance.
data LinearMetric = LinearMetric {
    -- | The range of a color channel that is within gamut.
    -- This range depends on the channel and the particular
    -- color being observed, and may not exist if the
    -- color itself is out of gamut.
    linear_gamut_bounds :: Maybe (RSdouble,RSdouble),
    -- | A function to modify a color channel independantly
    -- from the other color channels in the same color space.
    linear_color_function :: RSdouble -> Color,
    -- | The value of the particular color channel for the
    -- particular color.
    linear_value :: RSdouble,
    -- | The original color.
    linear_original :: Color }

instance Show LinearMetric where
    show x = show (linear_gamut_bounds x,
                   linear_value x,
                   exportColorCoordinates (linear_original x) color_space_rgb)

-- | Read a specific channel of a color.
viewChannel :: (ExportColorCoordinates c) =>
                  ColorChannel -> c -> LinearMetric
viewChannel (LinearChannel m) c = LinearMetric {
              linear_gamut_bounds = case intersections of
                  [] -> Nothing
                  is -> Just (minimum is,maximum is),
              linear_color_function = \x -> importColorCoordinates $
                  transformColorFromTo (AffineColorSpace m) (Point3D x v w),
              linear_value = u,
              linear_original = transformColor c }
    where Point3D u v w = exportColorCoordinates c $ AffineColorSpace m
          rgb@(Point3D r g b) = exportColorCoordinates c color_space_rgb
          -- The vector describinb an adjustment of the specified
          -- color channel.
          rgb'@(Vector3D r' g' b') = colorVectorOf (AffineColorSpace m)
                                                   (Vector3D 1 0 0)
          -- Find the intersections between the vector describing an
          -- adjustment of the specified channel and the boundaries
          -- of the RGB gamut's color cube.
          intersections = map (+u) $ filter gamutValid $
               map (uncurry (/)) $ filter (not . nearZero . snd)
                   [(1-r,r'),(1-g,g'),(1-b,b'),
                    (-r,r'),(-g,g'),(-b,b')]
          -- Construct the color with a particular linear adjustment of the
          -- specified color channel
          colorFunction x = rgb `add` scalarMultiply x rgb'
          -- Test that a color is within the RGB device gamut.
          gamutValid x = let Point3D r'' g'' b'' = colorFunction x
              in (>=3) $ length $ filter (>= (-0.001)) $ filter (<= 1.001) $ [r'',g'',b'']
viewChannel (Preconfigure f) c =
    viewChannel (f $ transformColor c) c

-- | A color space specification or color type that has an associated
-- color space.
--
-- If a type implements both 'ImportColorCoordinates' and
-- 'ColorSpace', then it must ensure that:
--
-- @importColorCoordinates f =
--     (let c = importColorCoordinates (const $ f $ affineColorSpaceOf c) in c)@
--
-- This is not hard -- all that is required is that
-- @affineColorSpaceOf undefined@ is defined.
--
class ColorSpace cs where
    affineColorSpaceOf :: cs -> AffineColorSpace

-- | A color type that can export its color coordinates.
-- An easy implementation is
--
-- @transformColorFromTo your_color_space your_color_coordinates@
--
-- If a type implements both 'ExportColorCoordinates' and
-- 'ImportColorCoordinates', then it must ensure that
-- @importColorCoordinates . exportColorCoordinates = id@.
--
class ExportColorCoordinates c where
    exportColorCoordinates ::
        c -> AffineColorSpace -> Point3D

-- | A color type that can import its color coordinates.
class ImportColorCoordinates c where
    importColorCoordinates ::
        (AffineColorSpace -> Point3D) -> c

instance ColorSpace AffineColorSpace where
    affineColorSpaceOf = id

instance ColorSpace ColorWheel where
    affineColorSpaceOf (ColorWheel m) = AffineColorSpace m

-- | A generic representation of Color.
newtype Color = Color { fromColor ::
    AffineColorSpace -> Point3D }

instance ExportColorCoordinates Color where
    exportColorCoordinates = fromColor

instance ImportColorCoordinates Color where
    importColorCoordinates = Color

-- | Construct a new color space.  This requires a minimal point
-- (the black point in an additive color space, or the white point
-- in a subtractive color space), and three primary colors.
-- The three primarys color correspond to the 'channel_u',
-- 'channel_v', and 'channel_w' respectively.
newColorSpace :: (ExportColorCoordinates c) =>
    c -> c -> c -> c -> AffineColorSpace
newColorSpace k u v w = AffineColorSpace $ matrixInverse $
            translationMatrix (vectorToFrom k' zero)
        `matrixMultiply`
            xyzMatrix (u' `sub` k')
                      (v' `sub` k')
                      (w' `sub` k')
    where k' = exportColorCoordinates k color_space_rgb
          u' = exportColorCoordinates u color_space_rgb
          v' = exportColorCoordinates v color_space_rgb
          w' = exportColorCoordinates w color_space_rgb

-- | Construct a new color wheel.  This requires a minimal point,
-- (the black point in an additive color space, or the white point
-- in a subtractive color space), and three primary colors with
-- assigned hue angles and value parameters.
-- The hue angle maps onto 'channel_u' and 'channel_v', while
-- the value parameter maps directly and additively onto
-- 'channel_w'.
newColorWheel :: (ExportColorCoordinates c) =>
    c -> (c,Angle,RSdouble) ->
         (c,Angle,RSdouble) ->
         (c,Angle,RSdouble) ->
         ColorWheel
newColorWheel k (u,theta_u,u') (v,theta_v,v') (w,theta_w,w') =
        ColorWheel $ uvw_to_wheel `matrixMultiply` matrixInverse uvw_to_rgb
    where uvw_to_wheel = matrix $
              [ [cosine theta_u, cosine theta_v, cosine theta_w, 0 ],
                [  sine theta_u,   sine theta_v,   sine theta_w, 0 ],
                [            u',             v',             w', 0 ],
                [             0,              0,              0, 1 ] ]
          AffineColorSpace uvw_to_rgb = newColorSpace k u v w

-- | A color wheel constructed with red, green and blue device primaries
-- and a luminance component.  This is the basis of the HCL color system.
{-# NOINLINE color_wheel_rgbl #-}
color_wheel_rgbl :: ColorWheel
color_wheel_rgbl = ColorWheel $ matrix $
  [ [1.0    , -0.5       , -0.5                  , 0.0],
    [0.0    , (sqrt 3/2) , (negate $ sqrt 3 / 2) , 0.0],
    [0.2126 , 0.7152     , 0.0722                , 0.0],
    [0.0    , 0.0        , 0.0                   , 1.0] ]

-- | The red-green-blue device color space.
{-# NOINLINE color_space_rgb #-}
color_space_rgb :: AffineColorSpace
color_space_rgb = AffineColorSpace $ identity_matrix

-- | Transform ordered triples between color spaces.
transformColorFromTo :: AffineColorSpace ->
                        Point3D ->
                        AffineColorSpace ->
                        Point3D
transformColorFromTo (AffineColorSpace source)
                     uvw
                     (AffineColorSpace destination) =
    transform (destination `matrixMultiply` matrixInverse source)
              uvw

-- | Transform a color vector into RGB space.
colorVectorOf :: AffineColorSpace ->
                 Vector3D ->
                 Vector3D
colorVectorOf (AffineColorSpace m) uvw =
    inverseTransform m uvw

{-# RULES
"transformColor::a->a"    transformColor = id
  #-}
-- | Transform colors between color spaces.
transformColor :: (ExportColorCoordinates source,
                   ImportColorCoordinates dest) =>
                  source -> dest
transformColor = importColorCoordinates . exportColorCoordinates