{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE TypeFamilies  #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Diagrams.TwoD.Segment.Bernstein
-- Copyright   :  (c) 2014-2015 diagrams-lib team (see LICENSE)
-- License     :  BSD-style (see LICENSE)
-- Maintainer  :  diagrams-discuss@googlegroups.com
--
-- Bernstein polynomials, used internally by code to find
-- intersections of paths.  This module is probably not of any
-- relevance to most users of diagrams.
-----------------------------------------------------------------------------
module Diagrams.TwoD.Segment.Bernstein
  ( BernsteinPoly (..)
  , listToBernstein
  , evaluateBernstein

  , degreeElevate
  , bernsteinDeriv
  , evaluateBernsteinDerivs
  ) where

import           Data.List           (tails)
import           Diagrams.Core.V
import           Diagrams.Parametric
import           Linear.V1

-- | Compute the binomial coefficients of degree n.
binomials :: Num n => Int -> [n]
binomials n = map fromIntegral $ scanl (\x m -> x * (n - m+1) `quot` m) 1 [1..n]

data BernsteinPoly n = BernsteinPoly
  { bernsteinDegree :: Int
  , bernsteinCoeffs :: [n]
  } deriving (Show, Functor)

type instance V        (BernsteinPoly n) = V1
type instance N        (BernsteinPoly n) = n
type instance Codomain (BernsteinPoly n) = V1

-- | Create a bernstein polynomial from a list of coƫfficients.
listToBernstein :: Fractional n => [n] -> BernsteinPoly n
listToBernstein [] = 0
listToBernstein l  = BernsteinPoly (length l - 1) l

-- | Degree elevate a bernstein polynomial a number of times.
degreeElevate :: Fractional n => BernsteinPoly n -> Int -> BernsteinPoly n
degreeElevate b                    0     = b
degreeElevate (BernsteinPoly lp p) times =
  degreeElevate (BernsteinPoly (lp+1) (head p:inner p 1)) (times-1)
  where
    n = fromIntegral lp

    inner []         _ = [0]
    inner [a]        _ = [a]
    inner (a:b:rest) i = (i*a/(n+1) + b*(1 - i/(n+1))) : inner (b:rest) (i+1)

-- | Evaluate the bernstein polynomial.
evaluateBernstein :: Fractional n => BernsteinPoly n -> n -> n
evaluateBernstein (BernsteinPoly _ [])       _ = 0
evaluateBernstein (BernsteinPoly _ [b])      _ = b
evaluateBernstein (BernsteinPoly lp (b':bs)) t = go t n (b'*u) 2 bs
  where
    u = 1-t
    n = fromIntegral lp

    go tn bc tmp _ [b]      = tmp + tn*bc*b
    go tn bc tmp i (b:rest) =
      go (tn*t)              -- tn
         (bc*(n - i+1)/i)    -- bc
         ((tmp + tn*bc*b)*u) -- tmp
         (i+1)               -- i
         rest
    go _ _ _ _ []           = error "evaluateBernstein: impossible"

-- | Evaluate the bernstein polynomial and its derivatives.
evaluateBernsteinDerivs :: Fractional n => BernsteinPoly n -> n -> [n]
evaluateBernsteinDerivs b t
  | bernsteinDegree b == 0 = [evaluateBernstein b t]
  | otherwise              = evaluateBernstein b t : evaluateBernsteinDerivs (bernsteinDeriv b) t

-- | Find the derivative of a bernstein polynomial.
bernsteinDeriv :: Fractional n => BernsteinPoly n -> BernsteinPoly n
bernsteinDeriv (BernsteinPoly 0 _)  = 0
bernsteinDeriv (BernsteinPoly lp p) =
  -- BernsteinPoly (lp-1) $ map (* fromIntegral lp) $ zipWith (-) (tail p) p
  BernsteinPoly (lp-1) $ zipWith (\a b -> (a - b) * fromIntegral lp) (tail p) p

instance Fractional n => Parametric (BernsteinPoly n) where
  atParam b = V1 . evaluateBernstein b
instance Num n        => DomainBounds (BernsteinPoly n)
instance Fractional n => EndValues    (BernsteinPoly n)
instance Fractional n => Sectionable  (BernsteinPoly n) where
  splitAtParam  = bernsteinSplit
  reverseDomain (BernsteinPoly i xs) = BernsteinPoly i (reverse xs)

-- | Split a bernstein polynomial.
bernsteinSplit :: Num n => BernsteinPoly n -> n -> (BernsteinPoly n, BernsteinPoly n)
bernsteinSplit (BernsteinPoly lp p) t =
  (BernsteinPoly lp $ map head controls,
   BernsteinPoly lp $ reverse $ map last controls)
  where
    interp a b = (1-t)*a + t*b

    terp [_] = []
    terp l   = let ctrs = zipWith interp l (tail l)
               in  ctrs : terp ctrs
    controls = p : terp p

instance Fractional n => Num (BernsteinPoly n) where
  ba@(BernsteinPoly la a) + bb@(BernsteinPoly lb b)
    | la < lb   = BernsteinPoly lb $ zipWith (+) (bernsteinCoeffs $ degreeElevate ba $ lb - la) b
    | la > lb   = BernsteinPoly la $ zipWith (+) a (bernsteinCoeffs $ degreeElevate bb $ la - lb)
    | otherwise = BernsteinPoly la $ zipWith (+) a b

  ba@(BernsteinPoly la a) - bb@(BernsteinPoly lb b)
    | la < lb   = BernsteinPoly lb $ zipWith (-) (bernsteinCoeffs $ degreeElevate ba (lb - la)) b
    | la > lb   = BernsteinPoly la $ zipWith (-) a (bernsteinCoeffs $ degreeElevate bb (la - lb))
    | otherwise = BernsteinPoly la $ zipWith (-) a b

  (BernsteinPoly la a) * (BernsteinPoly lb b) =
    BernsteinPoly (la+lb) $
    zipWith (flip (/)) (binomials (la + lb)) $
                   init $ map sum $
                   map (zipWith (*) a') (down b') ++
                   map (zipWith (*) (reverse b')) (tail $ tails a')
                   -- zipWith (zipWith (*)) (tail $ tails a') (repeat $ reverse b')
    where down l = tail $ scanl (flip (:)) [] l -- [[1], [2, 1], [3, 2, 1], ...
          a' = zipWith (*) a (binomials la)
          b' = zipWith (*) b (binomials lb)

  fromInteger a = BernsteinPoly 0 [fromInteger a]

  signum (BernsteinPoly _ [])    = 0
  signum (BernsteinPoly _ (a:_)) = BernsteinPoly 0 [signum a]

  abs = fmap abs