-- |
-- Module      :  $Header$
-- Copyright   :  (c) 2014-2016 Galois, Inc.
-- License     :  BSD3
-- Maintainer  :  cryptol@galois.com
-- Stability   :  provisional
-- Portability :  portable
--
-- Separate Non-Linear Constraints
-- When we spot a non-linear expression, we name it and add it to a map.
--
-- If we see the same expression multiple times, then we give it the same name.
--
-- The body of the non-linear expression is not processed further,
-- so the resulting map should not contain any of the newly minted names.

{-# LANGUAGE Safe, RecordWildCards #-}

module Cryptol.TypeCheck.Solver.Numeric.NonLin
  ( nonLinProp
  , NonLinS, nonLinSubst
  , initialNonLinS
  , apSubstNL
  , lookupNL
  ) where

import Cryptol.TypeCheck.Solver.Numeric.AST
import Cryptol.TypeCheck.Solver.Numeric.Simplify
import Cryptol.TypeCheck.Solver.Numeric.SimplifyExpr
import Cryptol.Utils.Panic(panic)

-- import           Data.GenericTrie (Trie)
-- import qualified Data.GenericTrie as Trie
import           Data.Maybe ( fromMaybe )
import           MonadLib
import           Data.Map (Map)
import qualified Data.Map as Map

type Trie   = Map

trie_empty :: Map k a
trie_empty  = Map.empty

trie_insert :: Expr -> a -> Map Expr a -> Map Expr a
trie_insert = Map.insert

trie_delete :: Expr -> Map Expr a -> Map Expr a
trie_delete = Map.delete

trie_lookup :: Expr -> Map Expr a -> Maybe a
trie_lookup = Map.lookup


-- | Factor-out non-linear terms, by naming them.
nonLinProp :: NonLinS -> Prop -> ([Prop], NonLinS)
nonLinProp s0 prop = (p : ps, s)
  where ((p,ps),s) = runNL s0 (nonLinPropM prop)

{- | Apply a substituin to the non-linear expression database.
Returns `Nothing` if nothing was affected.
Otherwise returns `Just`, and a substitution for non-linear expressions
that became linear.

The definitions of NL terms do not contain other named NL terms,
so it does not matter if the substitution contains bindings like @_a = e@.

There should be no bindings that mention NL term names in the definitions
of the substition (i.e, things like @x = _a@ are NOT ok).
-}
apSubstNL :: Subst -> NonLinS -> Maybe (Subst, [Prop], NonLinS)
apSubstNL su s0 = case runNL s0 (mApSubstNL su) of
                    ((Nothing,_),_) -> Nothing
                    ((Just su1,ps),r) -> Just (su1,ps,r)

lookupNL :: Name -> NonLinS -> Maybe Expr
lookupNL x NonLinS { .. } = Map.lookup x nonLinExprs


runNL :: NonLinS -> NonLinM a -> ((a, [Prop]), NonLinS)
runNL s m = runId
          $ runStateT s
          $ do a  <- m
               ps <- finishTodos
               return (a,ps)

-- | Get the known non-linear terms.
nonLinSubst :: NonLinS -> Subst
nonLinSubst = nonLinExprs

-- | The initial state for the linearization process.
initialNonLinS :: NonLinS
initialNonLinS = NonLinS
  { nextName = 0
  , nonLinExprs = Map.empty
  , nlKnown = trie_empty
  , nlTodo = []
  }


data SubstOneRes = NoChange
                   -- ^ Substitution does not affect the expression.

                 | Updated (Maybe (Name,Expr))
                   -- ^ The expression was updated and, maybe, it became linear.


{- | Apply the substituint to all non-linear bindings.
Returns `Nothing` if nothing was affected.
Otherwise returns `Just`, and a substituion mapping names that used
to be non-linear but became linear.

Note that we may return `Just empty`, indicating that some non-linear
expressions were updated, but they remained non-linear. -}
mApSubstNL :: Subst -> NonLinM (Maybe Subst)
mApSubstNL su =
  do s <- get
     answers <- mapM (mApSubstOneNL su) (Map.toList (nonLinExprs s))
     return (foldr upd Nothing answers)
  where
  upd NoChange ch     = ch
  upd (Updated mb) ch = let lsu = fromMaybe Map.empty ch
                        in Just (case mb of
                                   Nothing    -> lsu
                                   Just (x,e) -> Map.insert x e lsu)


mApSubstOneNL :: Subst -> (Name,Expr) -> NonLinM SubstOneRes
mApSubstOneNL su (x,e) =
  case apSubst su e of
    Nothing -> return NoChange
    Just e1 ->
      case crySimpExprMaybe e1 of

        Nothing ->
          sets $ \NonLinS { .. } ->
            ( Updated Nothing
            , NonLinS { nonLinExprs = Map.insert x e1 nonLinExprs
                      , nlKnown = trie_insert e1 x (trie_delete e nlKnown)
                      , .. }
            )

        Just e2
          | isNonLinOp e2 ->
          sets $ \NonLinS { .. } ->
            (Updated Nothing
            , NonLinS { nonLinExprs = Map.insert x e2 nonLinExprs
                      , nlKnown = trie_insert e2 x (trie_delete e nlKnown)
                      , .. }
            )

          | otherwise ->
            do sets_ $ \NonLinS { .. } ->
                 NonLinS { nonLinExprs = Map.delete x nonLinExprs
                         , nlKnown = trie_delete e nlKnown
                         , ..
                         }
               es <- mapM nonLinExprM (cryExprExprs e2)
               let e3 = cryRebuildExpr e2 es
               return (Updated (Just (x,e3)))



-- | Is the top-level operator a non-linear one.
isNonLinOp :: Expr -> Bool
isNonLinOp expr =
  case expr of
    K _   -> False
    Var _ -> False

    _ :+ _ -> False

    _ :- _ -> False

    x :* y ->
      case (x,y) of
        (K _, _)  -> False
        (_, K _)  -> False
        _         -> True

    Div _ y ->
      case y of
        K (Nat n) -> n == 0
        _         -> True

    Mod _ y ->
      case y of
        K (Nat n) -> n == 0
        _         -> True

    _ :^^ _       -> True

    Min _ _       -> False

    Max _ _       -> False

    Width _       -> True

    LenFromThen _ _ _ -> True -- See also comment on `LenFromThenTo`

    LenFromThenTo x y _ ->
      case (x,y) of
        (K _, K _) -> False
        _          -> True    -- Actually, as long as the difference bettwen
                              -- `x` and `y` is constant we'd be OK, but not
                              -- sure how to do that...

nlImplied :: Expr -> Name -> [Prop]
nlImplied expr x =
  map crySimplify $
  case expr of
    K (Nat n) :^^ e | n >= 2 -> [ Var x :>= one, Var x :>= e :+ one ]
    Mod _ e                  -> [ e     :>= Var x :+ one ]

    _                        -> []




nonLinPropM :: Prop -> NonLinM Prop
nonLinPropM prop =
  case prop of
    PFalse      -> return PFalse
    PTrue       -> return PTrue
    Not p       -> Not   `fmap` nonLinPropM p
    p :&& q     -> (:&&) `fmap` nonLinPropM p `ap` nonLinPropM q
    p :|| q     -> (:||) `fmap` nonLinPropM p `ap` nonLinPropM q
    Fin (Var _) -> return prop
    Fin _       -> unexpected
    x :==: y    -> (:==:) `fmap` nonLinExprM x `ap` nonLinExprM y
    x :>: y     -> (:>:)  `fmap` nonLinExprM x `ap` nonLinExprM y

    _ :== _     -> unexpected
    _ :>= _     -> unexpected
    _ :> _      -> unexpected

  where
  unexpected = panic "nonLinPropM" [ show (ppProp prop) ]



nonLinExprM :: Expr -> NonLinM Expr
nonLinExprM expr
  | isNonLinOp expr = nameExpr expr
  | otherwise = cryRebuildExpr expr `fmap` mapM nonLinExprM (cryExprExprs expr)



type NonLinM = StateT NonLinS Id

data NonLinS = NonLinS
  { nextName    :: !Int
  , nonLinExprs :: Subst
  , nlKnown     :: Trie Expr Name
  , nlTodo      :: [Prop]
  } deriving Show

nameExpr :: Expr -> NonLinM Expr
nameExpr e = sets $ \s ->
  case trie_lookup e (nlKnown s) of
    Just x -> (Var x, s)
    Nothing ->
      let x  = nextName s
          n  = SysName x
          s1 = NonLinS { nextName = 1 + x
                       , nonLinExprs = Map.insert n e (nonLinExprs s)
                       , nlKnown = trie_insert e n (nlKnown s)
                       , nlTodo = nlImplied e n ++ nlTodo s
                       }
      in (Var n, s1)


finishTodos :: NonLinM [Prop]
finishTodos =
  do s <- get
     case nlTodo s of
       [] -> return []
       p : ps ->
        do set s { nlTodo = ps }
           p'  <- nonLinPropM p
           ps' <- finishTodos
           return (p' : ps')
