```module QuantLib.Math.Copulas
( Copula,
Copulas (..)
) where

{-| Copula type class.
-| Normally instance should implement only copulaFunc.
-| Method copula provides a precheck for [0..1] range for x and y but real implementation is in copulaFunc
-}
class Copula a where
copula :: a -> Double -> Double -> Maybe Double
copula t = precheckRange (copulaFunc t)
copulaFunc :: a -> Double -> Double -> Maybe Double

-- Copula must be in [0,1] range
precheckRange :: (Double->Double->Maybe Double) -> Double -> Double -> Maybe Double
precheckRange f x y
| x >= 0.0 || x <= 1.0
|| y>= 0.0 || y <= 1.0
= f x y
| otherwise
= Nothing

{-| Copula data types with parameters required by the concrete copula definition
-}
data Copulas = ClaytonCopula Double -- ^ Clayton copula
| MinCopula -- ^ Min copula
| MaxCopula -- ^ Max copula
| AliMikhailHaqCopula Double -- ^ Ali-Mikhail-Haq copula
| FarlieGumbelMorgensternCopula Double -- ^ Farlie-Gumbel-Morgenstern copula
| FrankCopula Double -- ^ Frank copula
| GalambosCopula Double -- ^ Galambos copula
| GaussianCopula Double -- ^ Gaussian copula /Not implemented yet!/
| GumbelCopula Double -- ^ Gumbel copula
| HuslerReissCopula Double -- ^ Husler-Reiss copula /Not implemented yet!/
| IndependentCopula -- ^ Independent copula
| MarshallOlkinCopula Double Double -- ^ Marshall-Olkin copula
| PlackettCopula Double -- ^ Plackett copula

instance Copula Copulas where
copulaFunc (ClaytonCopula theta)                    = claytonCopula theta
copulaFunc MinCopula                                = minCopula
copulaFunc MaxCopula                                = maxCopula
copulaFunc (AliMikhailHaqCopula theta)              = aliMikhailHaqCopula theta
copulaFunc (FarlieGumbelMorgensternCopula theta)    = farlieGumbelMorgenstern theta
copulaFunc (FrankCopula theta)                      = frankCopula theta
copulaFunc (GalambosCopula theta)                   = galambosCopula theta
copulaFunc (GaussianCopula rho)                     = gaussianCopula rho
copulaFunc (GumbelCopula theta)                     = gumbelCopula theta
copulaFunc (HuslerReissCopula theta)                = huslerReissCopula theta
copulaFunc IndependentCopula                        = independentCopula
copulaFunc (MarshallOlkinCopula a b)                = marshallOlkinCopula a b
copulaFunc (PlackettCopula theta)                   = plackettCopula theta

{- Private implementations   -}

aliMikhailHaqCopula :: (Fractional a, Ord a) => a -> a -> a -> Maybe a
aliMikhailHaqCopula theta x y
| theta >= -1.0 && theta <= 1.0
= Just ((x*y)/(1.0 - theta * (1.0-x)*(1.0-y)))
| otherwise
= Nothing

farlieGumbelMorgenstern :: (Fractional a, Ord a) => a -> a -> a -> Maybe a
farlieGumbelMorgenstern theta x y
| theta >= -1.0 && theta <= 1.0
= Just (x*y + theta*x*y*(1.0-x)*(1.0-y))
| otherwise
= Nothing

{-|  Original code and algorithm from the Quantlib project
implemented in Haskell by Nicholas Pezolano
npezolano "at" gmail.com
-}
claytonCopula :: Double -> Double -> Double -> Maybe Double
claytonCopula theta x y
|  theta ==0
|| theta < -1.0
= Nothing

| otherwise
= Just \$ max( (x ** (-theta) ) +   (y ** (-theta)-1.0)  **   (-1.0/theta)) 0

minCopula ::  Ord a => a -> a -> Maybe a
minCopula x y = Just (min x y)

maxCopula ::  (Fractional a, Ord a) => a -> a -> Maybe a
maxCopula x y = Just (max 0.0 (x+y-1.0))

frankCopula ::  (Eq a, Floating a) => a -> a -> a -> Maybe a
frankCopula theta x y
| theta     == 0.0 = Nothing
| otherwise = Just (-1.0/theta * log (1 + (exp (-theta*x) - 1.0) * (exp (-theta*y) -1.0) / (exp (-theta) - 1.0)   ))

galambosCopula ::  (Floating a, Ord a) => a -> a -> a -> Maybe a
galambosCopula theta x y
| theta <= 0.0  = Nothing
| otherwise     = Just (x * y * exp ( ( (-log x) ** (-theta) + (-log y) ** (-theta) ) ** (-1.0/theta)  ))

gaussianCopula ::  (Fractional a, Ord a) => a -> t -> t1 -> Maybe a1
gaussianCopula rho _ _
| rho >= -1.0 && rho <= 1.0 = undefined
| otherwise                 = Nothing

gumbelCopula ::  (Floating a, Ord a) => a -> a -> a -> Maybe a
gumbelCopula theta x y
| theta >= 1.0  = Just (exp ( - ( (-log x) ** theta + (-log y) ** theta) ** (1.0/theta)))
| otherwise     = Nothing

huslerReissCopula :: (Fractional a, Ord a) => a -> a -> a -> Maybe a
huslerReissCopula theta _ _
| theta > 0.0   = undefined
| otherwise     = Nothing

independentCopula ::  Num a => a -> a -> Maybe a
independentCopula x y = Just (x*y)

marshallOlkinCopula :: (Floating a, Ord a) => a -> a -> a -> a -> Maybe a
marshallOlkinCopula a b x y
| a >= 0.0 && b >= 0.0  = Just (min (y * (x ** (1-a))) (x * (y ** (1-b))))
| otherwise             = Nothing

plackettCopula ::  (Floating a, Ord a) => a -> a -> a -> Maybe a
plackettCopula theta x y
| theta >= 0.0 && theta /= 1.0  = Just \$ (sumXyTheta1 - sqrt (sumXyTheta1 * sumXyTheta1 - 4.0 * x * y * theta * theta1))/(2*theta1)
| otherwise                     = Nothing
where   sumXy           = x + y
theta1          = theta - 1.0
sumXyTheta1     = 1.0 + theta1 * sumXy
```