{-# LANGUAGE DataKinds, TypeFamilies, FlexibleContexts, FlexibleInstances, PolyKinds #-}
{-# LANGUAGE UndecidableInstances, MultiParamTypeClasses #-}


--ConstrainedClassMethods, 
module Polynomial.Prelude (
    -- * Types
    Polynomial(..),

    -- * Classes
    IsPolynomial(..),
    IsOrderedPolynomial(..),

    -- * Functions
    polytope2,
    (!*)
) where

import Control.Lens
import Prelude as P
import qualified Data.Map.Strict as MS
import Numeric.Algebra as NA
import qualified Numeric.Additive.Class as AD
import Debug.Trace
import Data.List
import qualified Data.Sized as DS
import GHC.TypeLits
import Data.Type.Ordinal.Builtin
import Data.Singletons.Prelude


import Polynomial.Monomial
import Arithmetic.Numbers
import Geometry.ConvexHull2





-- infix 7 !*!


class    (DecidableZero r, Rig r, Commutative r, Eq r) => CoeffRig r

-- | Synonym for instances.
instance (DecidableZero r, Rig r, Commutative r, Eq r) => CoeffRig r


-- | Polynomial requires just the type of the coefficient and the monomial ordering. 
-- | Arity is given when defining variables with 'variable' function
newtype Polynomial k ord n = Polynomial { getTerms :: MS.Map (Monomial ord n) k} deriving(Eq)

instance (Unital k, Show k, Eq k) => Show (Polynomial k Lex n) where
    show = dropPlusSign .  showTerms . reverse . MS.toList . getTerms

instance (KnownNat n, Unital k, Show k, Eq k) => Show (Polynomial k Revlex n) where
    show = dropPlusSign .  showTerms . map reverseMon . reverse . MS.toList . getTerms

reverseMon :: (KnownNat n, IsMonomialOrder ord) => (Monomial ord n, a) -> (Monomial ord n, a)
reverseMon (Monomial mon, a) = ((toMonomial . reverse . DS.toList) mon, a)


dropPlusSign :: String -> String
dropPlusSign [] = error "String too short in dropPlusSign function"
dropPlusSign [_] = error "String too short in dropPlusSign function"
dropPlusSign [_,_] = error "String too short in dropPlusSign function"
dropPlusSign s@(x:y:z:a)
    | (x:y:[z]) == " + " = a
    | otherwise = s

showTerms :: (Unital k, Eq k, Show k) =>  [(Monomial ord n, k)] -> String
showTerms [] = ""
showTerms (t:ts)
    | coeff == one = " + " ++ show mon ++ showTerms ts
    | otherwise = " + " ++ show coeff ++ show mon ++ showTerms ts
    where
        coeff = snd t
        mon = fst t


-- | Every polynomial must implement this class
class (CoeffRig (Coeff poly), KnownNat (Arity poly)) => IsPolynomial poly where
    type Coeff poly :: *
    type Arity poly :: Nat

    arity :: poly -> SNat (Arity poly)

    toPolynomial :: (Mon (Arity poly), Coeff poly) -> poly

    fromMonomial :: Mon (Arity poly) -> poly
    fromMonomial mon = toPolynomial (mon, one)

    variable :: Ordinal (Arity poly) -> poly
    variable idx = fromMonomial $ DS.replicate sing 0 & ix idx .~ 1

    -- (!*!) :: Coeff poly -> poly -> poly
    -- (!*!) = (!*)


class (IsMonomialOrder (MonOrder poly), IsPolynomial poly) => IsOrderedPolynomial poly where
    type MonOrder poly :: *

    terms :: poly -> MS.Map (Monomial (MonOrder poly) (Arity poly)) (Coeff poly)

    leadingTerm :: poly -> (Coeff poly, Monomial (MonOrder poly) (Arity poly))
    leadingTerm = (,) <$> leadingCoeff <*> leadingMonomial
    leadingMonomial :: poly -> Monomial (MonOrder poly) (Arity poly)
    leadingMonomial = snd . leadingTerm

    leadingCoeff :: poly -> Coeff poly
    leadingCoeff = fst . leadingTerm

instance (KnownNat n, IsMonomialOrder ord, CoeffRig k) => IsPolynomial (Polynomial k ord n) where
    type Coeff (Polynomial k ord n) = k
    type Arity (Polynomial k ord n) = n

    arity = DS.sLength . getMonomial . leadingMonomial
    toPolynomial (mon, coeff) = Polynomial $ MS.singleton (Monomial mon) coeff


instance (KnownNat n, CoeffRig k, IsMonomialOrder ord) => IsOrderedPolynomial (Polynomial k ord n) where
   type MonOrder (Polynomial k ord n) = ord
   terms = getTerms
   leadingTerm (Polynomial d) = case MS.lookupMax d of
                                   Just (mon, coeff) -> (coeff, mon)
                                   Nothing -> (one, one)

instance (IsMonomialOrder ord, KnownNat n) => Num (Polynomial (Tropical Integer) ord n) where
    (+) (Polynomial terms1) (Polynomial terms2) = Polynomial $ MS.unionWith     (P.+) terms1 terms2
    (*) (Polynomial terms1) (Polynomial terms2) = Polynomial $ MS.fromListWith (P.+) [ prodTerm t1 t2 | t1 <- MS.toList terms1, t2 <- MS.toList terms2]
    fromInteger x = Polynomial $ MS.singleton one (P.fromInteger x)
    negate poly =  Polynomial $ MS.map P.negate $ terms poly

instance (IsMonomialOrder ord, KnownNat n) => Fractional (Polynomial (Tropical Integer) ord n) where
    recip (Polynomial terms1) = Polynomial $ (MS.fromList . map negateTerm . MS.toList) terms1  -- Pseudo recip, only work with monic polynomials
        where
            negateTerm (mon, coef) = let
                                        listExps = DS.toList $ getMonomial mon
                                        negated = map P.negate listExps
                                    in (toMonomial negated, coef)



instance (Num k, IsMonomialOrder ord) => AD.Additive (Polynomial k ord n) where
    (+) (Polynomial terms1) (Polynomial terms2) = Polynomial $ MS.unionWith (P.+) terms1 terms2


instance (AD.Additive (Polynomial k ord n), Semiring k, Num k) => LeftModule k (Polynomial k ord n) where
    num .* poly = num !* poly

instance (AD.Additive (Polynomial k ord n), Semiring k, Num k) => RightModule k (Polynomial k ord n) where
    poly *. num = num !* poly


-- | Aux functions

prodTerm :: (KnownNat n, Num k, IsMonomialOrder ord) => (Monomial ord n, k) -> (Monomial ord n, k) -> (Monomial ord n, k)
prodTerm (mon1, coeff1) (mon2, coeff2) = (mon1 NA.* mon2, coeff1 P.* coeff2)

(!*) :: (Num k) => k -> Polynomial k ord n -> Polynomial k ord n
num !* poly = Polynomial $ MS.map (P.* num) (getTerms poly)


polytope2 :: IsOrderedPolynomial poly => poly -> [Point2D]
polytope2 = convexHull2 . map ( (\[a,b] -> (a,b)) . DS.toList . getMonomial . fst) . MS.toList . terms