{-# LANGUAGE DataKinds, EmptyDataDecls, FlexibleContexts, FlexibleInstances #-}
{-# LANGUAGE GADTs, MultiParamTypeClasses, NoMonomorphismRestriction        #-}
{-# LANGUAGE ParallelListComp, PolyKinds, QuasiQuotes, RankNTypes           #-}
{-# LANGUAGE ScopedTypeVariables, StandaloneDeriving, TypeFamilies          #-}
{-# LANGUAGE TypeOperators, UndecidableInstances                            #-}
module Algebra.Field.Galois (GF'(), IsGF', modPoly, modVec,
                             withIrreducible, linearRepGF, linearRepGF',
                             reifyGF', generateIrreducible,
                             withGF', GF, ConwayPolynomial(..),
                             Conway, primitive, conway,
                             conwayFile, addConwayPolynomials)  where
import Algebra.Field.Finite
import Algebra.Field.Galois.Conway
import Algebra.Internal
import Algebra.Prelude.Core               hiding (varX)
import Algebra.Ring.Polynomial.Univariate

import           Control.Lens                 (imap)
import           Control.Monad                (replicateM)
import           Control.Monad.Loops          (iterateUntil)
import           Control.Monad.Random         (MonadRandom)
import           Control.Monad.Random         (uniform)
import qualified Data.Foldable                as F
import qualified Data.Ratio                   as Rat
import           Data.Reflection              (Reifies (..), reify)
import           Data.Singletons.Prelude.Enum (SEnum (..))
import           Data.Singletons.TypeLits     (withKnownNat)
import qualified Data.Sized.Builtin           as SV
import qualified Data.Traversable             as T
import qualified Data.Vector                  as V
import qualified GHC.TypeLits                 as TL
import qualified Numeric.Algebra              as NA
import           Numeric.Domain.Euclidean     (Euclidean)
import           Numeric.Domain.GCD           (GCDDomain, gcd)
import           Numeric.Semiring.ZeroProduct (ZeroProductSemiring)
import qualified Prelude                      as P

-- | Galois field of order @p^n@.
--   @f@ stands for the irreducible polynomial over @F_p@ of degree @n@.
data GF' p (n :: TL.Nat) (f :: *) = GF' { runGF' :: Sized n (F p) }
deriving instance Reifies p Integer => Eq (GF' p n f)

-- | Galois Field of order @p^n@. This uses conway polynomials
--   as canonical minimal polynomial and it should be known at
--   compile-time (i.e. @Reifies (Conway p n) (Unipol (F n))@
--   instances should be defined to use field operations).
type GF (p :: TL.Nat) n = GF' p n (Conway p n)

modPoly :: forall p n f. (KnownNat n, Reifies p Integer) => Unipol (F p) -> GF' p n f
modPoly = GF' . polyToVec

modVec :: Sized n (F p) -> GF' p n f
modVec = GF'

instance (Reifies p Integer, Show (F p)) => Show (GF' p n f)  where
  showsPrec d (GF' (v :< vs)) =
    if F.all isZero vs
    then showsPrec d v
    else showChar '<' . showString (showPolynomialWith (singleton "ΞΎ") 0 $ vecToPoly $ v :< vs) . showChar '>'
  showsPrec _ _ = showString "0"

instance (Reifies p Integer, Show (F p)) => PrettyCoeff (GF' p n f)

varX :: CoeffRing r => Unipol r
varX = var [od|0|]

vecToPoly :: (CoeffRing r)
          => Sized n r -> Unipol r
vecToPoly v = sum $ imap (\i c -> injectCoeff c * varX^fromIntegral i) $ F.toList v

polyToVec :: forall n r. (CoeffRing r, KnownNat n) => Unipol r -> Sized n r
polyToVec f = unsafeFromList' [ coeff (leadingMonomial $ (varX ^ i) `asTypeOf` f) f
                              | i <- [0..fromIntegral (fromSing (sing :: SNat n))]]

instance Reifies p Integer => Additive (GF' p n f)  where
  GF' v + GF' u = GF' $ SV.zipWithSame (+) v u

instance (Reifies p Integer, KnownNat n) => Monoidal (GF' p n f) where
  zero = GF' $ SV.replicate' zero

instance Reifies p Integer => LeftModule Natural (GF' p n f) where
  n .* GF' v = GF' $ SV.map (n .*) v

instance Reifies p Integer => RightModule Natural (GF' p n f) where
  GF' v *. n = GF' $ SV.map (*. n) v

instance Reifies p Integer => LeftModule Integer (GF' p n f) where
  n .* GF' v = GF' $ SV.map (n .*) v

instance Reifies p Integer => RightModule Integer (GF' p n f) where
  GF' v *. n = GF' $ SV.map (*. n) v

instance (KnownNat n, Reifies p Integer) => Group (GF' p n f) where
  negate (GF' v) = GF' $ SV.map negate v
  GF' u - GF' v  = GF' $ SV.zipWithSame (-) u v

instance (Reifies p Integer) => Abelian (GF' p n f)

instance (KnownNat n, Reifies f (Unipol (F p)), Reifies p Integer)
      => Multiplicative (GF' p n f) where
  GF' u * GF' v =
    let t = (vecToPoly u * vecToPoly v) `rem` reflect (Proxy :: Proxy f)
    in GF' $ polyToVec t

instance (KnownNat n, Reifies f (Unipol (F p)), Reifies p Integer) => Unital (GF' p n f) where
  one =
    case zeroOrSucc (sing :: SNat n) of
      IsZero   -> GF' NilL
      IsSucc k -> withKnownNat k $ GF' $ one :< SV.replicate' zero

instance (KnownNat n, Reifies f (Unipol (F p)), Reifies p Integer) => Semiring (GF' p n f)

instance (KnownNat n, Reifies f (Unipol (F p)), Reifies p Integer) => Rig (GF' p n f) where
  fromNatural n =
    case zeroOrSucc (sing :: SNat n) of
      IsZero -> GF' SV.empty
      IsSucc k -> withKnownNat k $ GF' $ fromNatural n :< SV.replicate' zero

instance (KnownNat n, Reifies f (Unipol (F p)), Reifies p Integer) => Commutative (GF' p n f)

instance (KnownNat n, Reifies f (Unipol (F p)), Reifies p Integer) => Ring (GF' p n f) where
  fromInteger n =
    case zeroOrSucc (sing :: SNat n) of
      IsZero   -> GF' NilL
      IsSucc k -> withKnownNat k $ GF' $ fromInteger n :< SV.replicate' zero

instance (KnownNat n, Reifies p Integer) => DecidableZero (GF' p n f) where
  isZero (GF' sv) = F.all isZero sv

instance (KnownNat n, Reifies p Integer, Reifies f (Unipol (F p))) => DecidableUnits (GF' p n f) where
  isUnit (GF' sv) = not $ F.all isZero sv
  recipUnit a | isZero a = Nothing
              | otherwise = Just $ recip a

instance (Reifies p Integer, Reifies f (Unipol (F p)), KnownNat n)
      => Characteristic (GF' p n f) where
  char _ = char (Proxy :: Proxy (F p))

instance (Reifies p Integer, Reifies f (Unipol (F p)), KnownNat n)
      => Division (GF' p n f) where
  recip f =
    let p = reflect (Proxy :: Proxy f)
        (_,_,r) = P.head $ euclid p $ vecToPoly $ runGF' f
    in GF' $ polyToVec $ r `rem` p

instance (KnownNat n, Reifies p Integer, Reifies f (Unipol (F p)))
      => DecidableAssociates (GF' p n f) where
  isAssociate p n =
    (isZero p && isZero n) || (not (isZero p) && not (isZero n))
instance (KnownNat n, Reifies p Integer, Reifies f (Unipol (F p)))
      => ZeroProductSemiring (GF' p n f)
instance (KnownNat n, Reifies p Integer, Reifies f (Unipol (F p)))
      => UnitNormalForm (GF' p n f)
instance (KnownNat n, Reifies p Integer, Reifies f (Unipol (F p)))
      => IntegralDomain (GF' p n f)
instance (KnownNat n, Reifies p Integer, Reifies f (Unipol (F p)))
      => GCDDomain (GF' p n f)
instance (KnownNat n, Reifies p Integer, Reifies f (Unipol (F p)))
      => UFD (GF' p n f)
instance (KnownNat n, Reifies p Integer, Reifies f (Unipol (F p)))
      => PID (GF' p n f)
instance (KnownNat n, Reifies p Integer, Reifies f (Unipol (F p)))
      => Euclidean (GF' p n f)

instance (Reifies p Integer, Reifies f (Unipol (F p)), KnownNat n) => P.Num (GF' p n f) where
  (+) = (NA.+)
  (-) = (NA.-)
  negate = NA.negate
  (*) = (NA.*)
  fromInteger = NA.fromInteger
  abs = error "not defined"
  signum = error "not defined"

instance (Reifies p Integer, Reifies f (Unipol (F p)), KnownNat n) => P.Fractional (GF' p n f) where
  fromRational u = fromInteger (Rat.numerator u) / fromInteger (Rat.denominator u)
  (/) = (/)
  recip = recip

-- | @generateIrreducible p n@ generates irreducible polynomial over F_@p@ of degree @n@.
generateIrreducible :: (MonadRandom m, FiniteField k, Eq k)
                    => proxy k -> Natural -> m (Unipol k)
generateIrreducible p n =
  iterateUntil (\f -> all (\i -> one == gcd (varX^(order p^i) - varX) f ) [1.. n `div` 2]) $ do
    cs <- replicateM (fromIntegral n) $ uniform (elements p)
    let f = varX^n + sum [ injectCoeff c * (varX^i) | c <- cs | i <- [0..n P.- 1]]
    return f

withIrreducible :: forall p a. KnownNat p
                => Unipol (F p)
                -> (forall f (n :: Nat). (Reifies f (Unipol (F p))) => Proxy (GF' p n f) -> a)
                -> a
withIrreducible r f =
  case toSing (fromIntegral $ totalDegree' r) of
    SomeSing sn ->
      withKnownNat sn $
      reify r (f. proxyGF' (Proxy :: Proxy (F n)) sn)

reifyGF' :: MonadRandom m => Natural -> Natural
         -> (forall (p :: TL.Nat) (f :: *) (n :: TL.Nat) . (Reifies p Integer, Reifies f (Unipol (F p)))
                       => Proxy (GF' p n f) -> a)
         -> m a
reifyGF' p n f = reifyPrimeField (P.toInteger p) $ \pxy -> do
  mpol <- generateIrreducible pxy n
  case toSing (fromIntegral p) of
    SomeSing sp -> return $ withKnownNat sp $ withIrreducible mpol f

linearRepGF :: GF' p n f -> V.Vector (F p)
linearRepGF = SV.unsized . runGF'

linearRepGF' :: GF' p n f -> V.Vector Integer
linearRepGF' = V.map naturalRepr . linearRepGF

withGF' :: MonadRandom m
        => Natural -> Natural
        -> (forall (p :: TL.Nat) f (n :: TL.Nat) . (Reifies p Integer, Reifies f (Unipol (F p)))
            => GF' p n f)
        -> m (V.Vector Integer)
withGF' p n f = reifyGF' p n $ V.map naturalRepr . linearRepGF . asProxyTypeOf f

proxyGF' :: Proxy (F p) -> SNat n -> Proxy f -> Proxy (GF' p n f)
proxyGF' _ _ Proxy = Proxy

-- | Type-constraint synonym to work with Galois field.
class (KnownNat n, KnownNat p, Reifies f (Unipol (F p))) => IsGF' p n f
instance (KnownNat n, KnownNat p, Reifies f (Unipol (F p))) => IsGF' p n f


instance (KnownNat n, IsGF' p n f) => ZeroProductSemiring (GF' p n f)

instance (KnownNat n, IsGF' p n f) => FiniteField (GF' p n f) where
  power _ = fromIntegral $ fromSing (sing :: SNat n)
  elements _ =
    let sn = sing :: SNat n
    in P.map GF' $ T.sequence $
       SV.replicate sn $ elements Proxy

primitive :: forall p n f. (IsGF' p n f) => GF' p (n + 1) f
primitive = withKnownNat (sSucc (sing :: SNat n)) $ GF' $ polyToVec $ var [od|0|]

-- | Conway polynomial (if definition is known).
conway :: forall p n. ConwayPolynomial p n
       => SNat p -> SNat n -> Unipol (F p)
conway = conwayPolynomial