-- mpoly.hs

module Math.MPoly (MPoly (..), TermOrder (..), VarSet(..),
              x, y, z, s, t, u, v, w, a, b, c, d, constMP,
              x0, x1, x2, x3, x_, monomial,
			  paramsMP, termsMP, termOrderMP, varsMP,
              toMonicMP, degMP,
              changeTermOrder,
			  quotRemMP, lt, lc, lp, ltMP,
              dividesTm, properlyDividesTm, divTm, lcmTm, gcdTm, degTm, (*/),
			  (/%), (%%), isReducibleMP,
			  toHomogeneous, fromHomogeneous,
			  evalMP, substMP) where

import List(intersperse)
import Math.QQ
import Math.FF
import Math.MergeSort
import Math.MathsPrimitives ( ($+), ($-), ($.) )


infix 5 /%, %%

-- MULTIVARIATE POLYNOMIALS

data TermOrder = Lex | Glex | Grevlex | Elim Int | Elimv [Int] deriving (Eq, Show)

data VarSet = Xyz | Xi deriving (Eq, Show)

data MPoly a = MP (TermOrder, VarSet) [Term a]


-- ACCESSOR FUNCTIONS

paramsMP (MP params _) = params

termsMP (MP _ terms) = terms

termOrderMP (MP (termOrder, _) _) = termOrder

varsMP (MP (_, vars) _) = vars


-- PRE-DEFINED VARIABLES

x = MP (Grevlex, Xyz) [(Q 1 1, [1])]
y = MP (Grevlex, Xyz) [(Q 1 1, [0,1])]
z = MP (Grevlex, Xyz) [(Q 1 1, [0,0,1])]
s = MP (Grevlex, Xyz) [(Q 1 1, [0,0,0,1])]
t = MP (Grevlex, Xyz) [(Q 1 1, [0,0,0,0,1])]
u = MP (Grevlex, Xyz) [(Q 1 1, [0,0,0,0,0,1])]
v = MP (Grevlex, Xyz) [(Q 1 1, [0,0,0,0,0,0,1])]
w = MP (Grevlex, Xyz) [(Q 1 1, [0,0,0,0,0,0,0,1])]
a = MP (Grevlex, Xyz) [(Q 1 1, [0,0,0,0,0,0,0,0,1])]
b = MP (Grevlex, Xyz) [(Q 1 1, [0,0,0,0,0,0,0,0,0,1])]
c = MP (Grevlex, Xyz) [(Q 1 1, [0,0,0,0,0,0,0,0,0,0,1])]
d = MP (Grevlex, Xyz) [(Q 1 1, [0,0,0,0,0,0,0,0,0,0,0,1])]

constMP c = MP (Grevlex, Xyz) [(c,[])]

-- Normally use x1.., and reserve x0 for dealing with homogeneous case
-- xvar i = MP Grevlex Xi [(Q 1 1, replicate i 0 ++ [1])]
x_ i = MP (Grevlex, Xi) [(Q 1 1, replicate i 0 ++ [1])]
x0 = MP (Grevlex, Xi) [(Q 1 1, [1])]
x1 = MP (Grevlex, Xi) [(Q 1 1, [0,1])]
x2 = MP (Grevlex, Xi) [(Q 1 1, [0,0,1])]
x3 = MP (Grevlex, Xi) [(Q 1 1, [0,0,0,1])]

monomial as = MP (Grevlex, Xi) [(Q 1 1, 0:as)]


-- POWER PRODUCTS
-- for example x^3 y^2 is represented as [3,2]

type PowerProduct = [Int]

-- we allow PP lists of unequal lengths, so need our own version of (==)
eqPP as bs = all (==0) (as $- bs)

multPP as bs = as $+ bs

dividesPP as bs = all (>=0) (bs $- as)

divPP as bs = as $- bs

gcdPP as [] = []
gcdPP [] bs = []
gcdPP (a:as) (b:bs) = min a b : gcdPP as bs

lcmPP as [] = as
lcmPP [] bs = bs
lcmPP (a:as) (b:bs) = max a b : lcmPP as bs


-- TERMS
-- for example, 3xy is represented as (3,[1,1])

-- !! It might be better to go directly to power products, and reduce our use of terms

type Term a = (a, PowerProduct)

eqTm (c,as) (d,bs) = c == d && as `eqPP` bs

multTm (c,as) (d,bs) = (c*d, multPP as bs)

dividesTm (_,as) (_,bs) = dividesPP as bs

-- properlyDividesTm t@(_,as) u@(_,bs) = dividesTm t u && sum as < sum bs
properlyDividesTm (_,as) (_,bs) = dividesPP as bs && not (eqPP as bs)

divTm (c,as) (d,bs) = (c/d, divPP as bs)

lcmTm (c,as) (d,bs) = (c*d, lcmPP as bs) -- !! the c*d here is arbitrary

gcdTm (c,as) (_,bs) = (c/c, gcdPP as bs)

degTm (_,as) = sum as


-- TERM ORDERINGS

isGraded Grevlex = True
isGraded Glex = True
isGraded _ = False



type TermOrdering a = Term a -> Term a -> Bool

-- in practice, term orderings always ignore the coefficients and only use the PowerProduct

lexgt (_,as) (_,bs) =
	let ds = dropWhile (==0) (as $- bs)
	in if null ds then False else head ds > 0
-- ie first non-zero difference is positive

revlexgt (_,as) (_,bs) =
	let ds = dropWhile (==0) (reverse (as $- bs))
	in if null ds then False else head ds < 0
-- ie last non-zero difference is negative

-- convert an ungraded term ordering into a graded term ordering
gradedgt :: TermOrdering a -> TermOrdering a
gradedgt termordgt t u =
	degt > degu ||
	(degt == degu && termordgt t u)
	where
		degt = degTm t
		degu = degTm u

glexgt :: TermOrdering a
glexgt = gradedgt lexgt

grevlexgt :: TermOrdering a
grevlexgt = gradedgt revlexgt

-- Elimination order - Cox et al, Ideals, Varieties and Algorithms
elim l t@(_,as) u@(_,bs) = sumlas > sumlbs || (sumlas == sumlbs && grevlexgt t u)
	where
		sumlas = sum (take l as)
		sumlbs = sum (take l bs)

elimv vs t@(_,as) u@(_,bs) = sumvas > sumvbs || (sumvas == sumvbs && grevlexgt t u)
	where
		sumvas = vs $. as
		sumvbs = vs $. bs

-- collectTerms :: TermOrdering -> [Term] -> [Term]
collectTerms order f = doCollectTerms (mergeSort' order f)
	where
		doCollectTerms [] = []
		doCollectTerms (t@(c,as):[])
			| c == 0    = []
			| otherwise = t:[]
		doCollectTerms (t1@(c,as):t2@(d,bs):ts)
			| c == 0        = doCollectTerms (t2:ts)
			| as `eqPP` bs  = doCollectTerms ((c+d,as):ts)
			| otherwise     = t1 : doCollectTerms (t2:ts)



termOrderToFn to = case to of
	Lex     -> lexgt
	Glex    -> glexgt
	Grevlex -> grevlexgt
	Elim l  -> elim l
	Elimv vs -> elimv vs

termOrderFn f = termOrderToFn (termOrderMP f)


-- EQ INSTANCE

instance Eq a => Eq (MPoly a) where
    MP _ [] == MP _ [] = True
    MP _ [(c,[])] == MP _ [(d,[])] = c == d
    MP params ts == MP params' ts' = params == params' && length ts == length ts' && and (zipWith eqTm ts ts')

-- ORD INSTANCE

instance (Eq a, Ord a) => Ord (MPoly a) where
	compare f g
		| paramsMP f /= paramsMP g = error "MPoly.compare: parameter mismatch"
		| otherwise = doCompare (termOrderFn f) (termsMP f) (termsMP g)

doCompare _ [] [] = EQ
doCompare _ _ [] = GT
doCompare _ [] _ = LT
doCompare termord (t@(c,as):ts) (u@(d,bs):us) =
	if eqPP as bs
	then case compare c d of
		LT -> LT
		GT -> GT
		EQ -> doCompare termord ts us
	else if termord t u then GT else LT


-- SHOW INSTANCE

instance (Show a, Num a) => Show (MPoly a) where
	show f =
		let vars = case varsMP f of
			Xyz -> ["x", "y", "z", "s", "t", "u", "v", "w", "a", "b", "c", "d"] ++ map (:[]) ['e'..'r']
			Xi  -> ["x" ++ show i | i <- [0..]]
		in showTerms vars (termsMP f)

-- showTerms vars ts = concat (intersperse "+" (map (showTerm vars) ts))
showTerms _ [] = "0"
showTerms vars ts = foldl1 linkTerms (map (showTerm vars) ts)
	where linkTerms t u = if head u == '-' then t ++ u else t ++ "+" ++ u

showTerm vars (c,as) = showCoeff c as ++ showPP vars as

showCoeff c as =
	let c' = show c in case c' of
	"1"       -> if any (/=0) as then "" else "1"
	"-1"      -> if any (/=0) as then "-" else "-1"
	otherwise -> c'

showPP vars indices = concat (map showPower (zip vars indices))
	where
		showPower (var,index)
			| index == 0  = ""
			| index == 1  = var
			| otherwise   = var ++ '^': show index


changeTermOrder to (MP (_, vs) ts) = MP (to, vs) ts'
	where ts' = collectTerms (termOrderToFn to) ts


-- RING OPERATIONS FOR MULTIVARIATE POLYNOMIALS

instance Num a => Num (MPoly a) where
    MP params@(to,_) f + MP params' g | params == params' = MP params (addMP tofn f g)
        where tofn = termOrderToFn to
    f@(MP params _) + MP _ g@[(_,[])] = f + MP params g -- if one of the summands is a constant, ignore parameter mismatch 
    MP _ f@[(_,[])] + g@(MP params _) = MP params f + g
    f + MP _ [] = f -- if one of the summands is zero, ignore parameter mismatch 
    MP _ [] + g = g

    negate (MP params f) = MP params (negateMP f)

    MP params@(to,_) f * MP params' g | params == params' = MP params (multMP tofn f g)
        where tofn = termOrderToFn to
    f@(MP params _) * MP _ g@[(_,[])] = f * MP params g -- if one of the summands is a constant, ignore parameter mismatch 
    MP _ f@[(_,[])] * g@(MP params _) = MP params f * g
    _ * (MP _ []) = 0
    (MP _ []) * _ = 0

    fromInteger 0 = MP (Grevlex, Xyz) []
    fromInteger n = MP (Grevlex, Xyz) [(fromInteger n, [])]
    -- NOTE: fromInteger gives you Grevlex over Q.
    -- If you want something else, use the conversion functions, or construct by hand

-- convert an mpoly from rationals to a finite field
(/%) :: MPoly QQ -> Integer -> MPoly FF
MP params ts /% p = MP params ts''
	where
		ts' = map (\(Q n 1, as) -> (toFp p n, as)) ts
		ts'' = filter (\(F _ x, _) -> x /= 0) ts'

addMP _ f [] = f
addMP _ [] f = f
addMP termord (t@(c,as):ts) (u@(d,bs):us) =
	if as `eqPP` bs
	then
		if c+d == 0
		then addMP termord ts us
		else (c+d,as) : (addMP termord ts us)
	else
		if termord t u
		then t : (addMP termord ts (u:us))
		else u : (addMP termord (t:ts) us)

negateMP f = map (\(c,as) -> (-c,as)) f

subMP termord f g = addMP termord f (negateMP g)

multMP _ _ [] = []
multMP _ [] _ = []
multMP termord (t:ts) (u:us) =
	let
		hh = [multTm t u]
		ht = map (multTm t) us
		th = map (multTm u) ts
		tt = multMP termord ts us
	in addMP termord (addMP termord hh tt) (addMP termord ht th)
-- !! experiment with addition orders. hh will be largest term, so will add in one step


instance Fractional a => Fractional (MPoly a) where
    recip (MP params [(c,[])]) = MP params [(recip c,[])]
    recip _ = error "recip not defined for non-constant MPoly"


toMonicMP f@(MP _ []) = f
toMonicMP (MP params ts@((k,_):_)) = MP params (map (\(c,as)->(c/k,as)) ts)


-- DIVISION ALGORITHM


-- initial term
ltMP (MP params (t:_)) = MP params [t]

lt f
	| null ts = error "lt 0"
	| otherwise = head ts
	where ts = termsMP f

ltSplit (MP params (t:ts)) = (MP params [t], MP params ts)

lc f = let (c,_) = lt f in c

lp f = let (_,as) = lt f in as

divideslt f g = dividesTm (lt f) (lt g)

divlt (MP params (t:_)) (MP _ (u:_)) = MP params [divTm t u]

degMP (MP (to, _) ts)
	| null ts     = -1
	| isGraded to = degTm (head ts)
	| otherwise   = foldl max 0 (map degTm ts)


t */ MP params ts = MP params (map (multTm t) ts)


quotRemMP f@(MP (termord, vs) _) gs = doQuotRemMP f (replicate (length gs) zero, zero)
	 where
		zero = MP (termord, vs) [] -- slight kludge
		doQuotRemMP h (us,r)
			| null (termsMP h)  = (us,r)
			| otherwise       = doDivisionStep h (gs,[],us,r)
		doDivisionStep h ([],us',[],r) =
			let (lth,h') = ltSplit h
			in doQuotRemMP h' (reverse us', r + lth)
			-- let lth = ltMP h
			-- in doQuotRemMP (h - lth) (reverse us', r + lth)
		doDivisionStep h (g:gs,us',u:us,r) =
			if divideslt g h
			then
				let
					newTerm = divlt h g
					u' = u + newTerm
					h' = h - (newTerm * g)
				in doQuotRemMP h' (reverse us' ++ (u' : us), r)
			else doDivisionStep h (gs,u:us',us,r)


reduceMP f gs = r where (_,r) = quotRemMP f gs


(%%) :: (Num a, Fractional a) => MPoly a -> [MPoly a] -> MPoly a
(%%) = reduceMP

isReducibleMP f gs = or [lt g `dividesTm` lt f | g <- gs]


-- evaluate the mpoly at the point passed in - eg evalMP (x^2+y) [1,2]
evalMP f vs = sum (map (\t -> evalTerm t vs) (termsMP f))
	where evalTerm (c,as) vs = foldl (*) c (zipWith (^) vs as)

-- substitute expressions for the vars - eg substMP (x1x2) [1,x+y,x^2+y^2] - (note we need to pass an expression for x0 too)
substMP f vs = sum (map (\t -> evalTerm t vs) (termsMP f))
	where evalTerm (c,as) vs = foldl (*) (constMP c) (zipWith (^) vs as)
-- (Could perhaps be called composeMP instead)

-- NOTE: The following two functions don't individually preserve variable names (though fromHomogeneous . toHomogeneous does)
toHomogeneous f@(MP params ts) =
	let
		d = degMP f
		ts' = collectTerms (termOrderFn f) (map (\(c,as) -> (c, d - sum as : as)) ts)
	in MP params ts'

fromHomogeneous f@(MP params ts) =
	let ts' = collectTerms (termOrderFn f) (map (\(c,as) -> if null as then (c,[]) else (c, tail as)) ts)
	in MP params ts'


-- Will also provide two functions toHomogeneousUsing, fromHomogeneousUsing, where you can specify the variable to be used

{-
isHomogeneousMP f = case termsMP f of
	[] -> True
	(t:ts) -> all (== degTm t) (map degTm ts)

-}
