-- |
-- Module      :  Cryptol.TypeCheck.Solve
-- Copyright   :  (c) 2013-2016 Galois, Inc.
-- License     :  BSD3
-- Maintainer  :  cryptol@galois.com
-- Stability   :  provisional
-- Portability :  portable

{-# LANGUAGE PatternGuards, BangPatterns, RecordWildCards #-}
module Cryptol.TypeCheck.Solve
  ( simplifyAllConstraints
  , proveImplication
  , tryProveImplication
  , proveModuleTopLevel
  , defaultAndSimplify
  , defaultReplExpr
  ) where

import           Cryptol.Parser.Position(thing,emptyRange)
import           Cryptol.TypeCheck.PP -- (pp)
import           Cryptol.TypeCheck.AST
import           Cryptol.TypeCheck.Monad
import           Cryptol.TypeCheck.Default
import           Cryptol.TypeCheck.SimpType(tWidth)
import           Cryptol.TypeCheck.Error(Error(..),Warning(..))
import           Cryptol.TypeCheck.Subst
                    (apSubst, isEmptySubst, substToList,
                          emptySubst,Subst,(@@), Subst, listParamSubst)
import qualified Cryptol.TypeCheck.SimpleSolver as Simplify
import           Cryptol.TypeCheck.Solver.Types
import           Cryptol.TypeCheck.Solver.Selector(tryHasGoal)

import           Cryptol.TypeCheck.Solver.SMT(Solver,proveImp,isNumeric)
import           Cryptol.TypeCheck.Solver.Improve(improveProp,improveProps)
import           Cryptol.TypeCheck.Solver.Numeric.Interval
import           Cryptol.Utils.Patterns(matchMaybe)

import           Control.Applicative ((<|>))
import           Control.Monad(mzero)
import           Data.Containers.ListUtils (nubOrd)
import           Data.Map (Map)
import qualified Data.Map as Map
import           Data.Set ( Set )
import qualified Data.Set as Set
import           Data.List(partition)
import           Data.Maybe(listToMaybe,fromMaybe)





quickSolverIO :: Ctxt -> [Goal] -> IO (Either Error (Subst,[Goal]))
quickSolverIO _ [] = return (Right (emptySubst, []))
quickSolverIO ctxt gs =
  case quickSolver ctxt gs of
    Left err -> return (Left err)
    Right (su,gs') ->
      do msg (vcat (map (pp . goal) gs' ++ [pp su]))
         return (Right (su,gs'))
  where
  msg _ = return ()
{-
  shAsmps = case [ pp x <+> text "in" <+> ppInterval i |
               (x,i) <- Map.toList ctxt ] of
              [] -> text ""
              xs -> text "ASMPS:" $$ nest 2 (vcat xs $$ text "===")
  msg d = putStrLn $ show (
             text "quickSolver:" $$ nest 2 (vcat
                [ shAsmps
                , vcat (map (pp.goal) gs)
                , text "==>"
                , d
                ])) -- -}

quickSolver :: Ctxt   -- ^ Facts we can know
            -> [Goal] -- ^ Need to solve these
            -> Either Error (Subst,[Goal])
            -- ^ Left: contradicting goals,
            --   Right: inferred types, unsolved goals.
quickSolver ctxt gs0 = go emptySubst [] gs0
  where
  go su [] [] = Right (su,[])

  go su unsolved [] =
    case matchMaybe (findImprovement noIncompatible unsolved) of
      Nothing -> Right (su,unsolved)
      Just imp ->
        case imp of
          Right (newSu, subs) ->
            go (newSu @@ su) [] (subs ++ apSubst newSu unsolved)
          Left err -> Left err

  go su unsolved (g : gs)
    | Set.member (goal g) (saturatedAsmps ctxt) = go su unsolved gs

  go su unsolved (g : gs) =
    case Simplify.simplifyStep ctxt (goal g) of
      Unsolvable          -> Left (UnsolvableGoals [g])
      Unsolved            -> go su (g : unsolved) gs
      SolvedIf subs       ->
        let cvt x = g { goal = x }
        in go su unsolved (map cvt subs ++ gs)

  -- Probably better to find more than one.
  findImprovement inc [] =
    do let bad = Map.intersectionWith (,) (integralTVars inc) (fracTVars inc)
       case Map.minView bad of
         Just ((g1,g2),_) -> pure $ Left $ UnsolvableGoals [g1,g2]
         Nothing -> mzero

  findImprovement inc (g : gs) =
    do (su,ps) <- improveProp False ctxt (goal g)
       return (Right (su, [ g { goal = p } | p <- ps ]))
    <|>
    findImprovement (addIncompatible g inc) gs


--------------------------------------------------------------------------------
-- Look for type variable with incompatible constraints

data Incompatible = Incompatible
  { integralTVars :: Map TVar Goal    -- ^ Integral a
  , fracTVars     :: Map TVar Goal    -- ^ Field a or FLiteral 
  }

noIncompatible :: Incompatible
noIncompatible = Incompatible
  { integralTVars = Map.empty
  , fracTVars     = Map.empty
  }

addIncompatible :: Goal -> Incompatible -> Incompatible
addIncompatible g i =
  fromMaybe i $
  do tv <- tIsVar =<< pIsIntegral (goal g)
     pure i { integralTVars = Map.insert tv g (integralTVars i) }
  <|>
  do tv <- tIsVar =<< pIsField (goal g)
     pure i { fracTVars = Map.insert tv g (fracTVars i) }
  <|>
  do (_,_,_,t) <- pIsFLiteral (goal g)
     tv        <- tIsVar t
     pure i { fracTVars = Map.insert tv g (fracTVars i) }





--------------------------------------------------------------------------------


defaultReplExpr :: Solver -> Expr -> Schema ->
                    IO (Maybe ([(TParam,Type)], Expr))

defaultReplExpr sol expr sch =
  do mb <- defaultReplExpr' sol numVs numPs
     case mb of
       Nothing -> return Nothing
       Just numBinds -> return $
         do let optss = map tryDefVar otherVs
            su    <- listToMaybe
                       [ binds | nonSu <- sequence optss
                               , let binds = nonSu ++ numBinds
                               , validate binds ]
            tys <- sequence [ lookup v su | v <- sVars sch ]
            return (su, appExpr tys)

  where
  validate binds =
    let su = listParamSubst binds
    in null (concatMap pSplitAnd (apSubst su (sProps sch)))

  (numVs,otherVs) = partition (kindIs KNum) (sVars sch)
  (numPs,otherPs) = partition isNumeric (sProps sch)


  kindIs k x = kindOf x == k

  gSet  = goalsFromList
           [ Goal { goal = p
                  , goalRange = emptyRange
                  , goalSource = CtDefaulting } | p <- otherPs ]

  fLitGoals = flitDefaultCandidates gSet

  tryDefVar :: TParam -> [(TParam, Type)]
  tryDefVar a
    -- REPL defaulting for floating-point literals
    | Just m <- Map.lookup (TVBound a) fLitGoals
    = case m of
        Just ((_,t),_) -> [(a,t)]
        Nothing        -> []

    -- REPL defaulting for integer literals
    | Just gt <- Map.lookup (TVBound a) (literalGoals gSet)
    = let ok p = not (Set.member (TVBound a) (fvs p)) in
      [ (a,t) | t <- [ tInteger, tWord (tWidth (goal gt)) ]
              , ok t ]

    -- REPL defaulting for variables unconstrained by a literal constraint
    | otherwise = [ (a,t) | t <- [tInteger, tRational, tBit] ]

  appExpr tys = foldl (\e1 _ -> EProofApp e1)
                      (foldl ETApp expr tys)
                      (sProps sch)


defaultAndSimplify :: [TVar] -> [Goal] -> ([TVar],[Goal],Subst,[Warning],[Error])
defaultAndSimplify as gs =
  let (as1, gs1, su1, ws) = defLit
      (as2, gs2, su2, errs) = improveByDefaultingWithPure as1 gs1
  in (as2,gs2,su2 @@ su1, ws, errs)

  where
  defLit
    | isEmptySubst su = nope
    | otherwise       = case quickSolver mempty (apSubst su gs) of
                          Left _ -> nope -- hm?
                          Right (su1,gs1) -> (as1,gs1,su1@@su,ws)
    where (as1,su,ws) = defaultLiterals as gs
          nope        = (as,gs,emptySubst,[])



simplifyAllConstraints :: InferM ()
simplifyAllConstraints =
  do simpHasGoals
     gs <- getGoals
     case gs of
       [] -> return ()
       _ ->
        case quickSolver mempty gs of
          Left err -> recordError err
          Right (su,gs1) ->
            do extendSubst su
               addGoals gs1

-- | Simplify @Has@ constraints as much as possible.
simpHasGoals :: InferM ()
simpHasGoals = go False [] =<< getHasGoals
  where
  go _     []       []  = return ()
  go True  unsolved []  = go False [] unsolved
  go False unsolved []  = mapM_ addHasGoal unsolved

  go changes unsolved (g : todo) =
    do (ch,solved) <- tryHasGoal g
       let changes'  = ch || changes
           unsolved' = if solved then unsolved else g : unsolved
       changes' `seq` unsolved `seq` go changes' unsolved' todo


-- | Try to clean-up any left-over constraints after we've checked everything
-- in a module.  Typically these are either trivial things, or constraints
-- on the module's type parameters.
proveModuleTopLevel :: InferM ()
proveModuleTopLevel =
  do simplifyAllConstraints
     gs <- getGoals
     let vs = Set.toList (Set.filter isFreeTV (fvs gs))
         (_,gs1,su1,ws,errs) = defaultAndSimplify vs gs
     extendSubst su1
     mapM_ recordWarning ws
     mapM_ recordError errs

     cs <- getParamConstraints
     case cs of
       [] -> addGoals gs1
       _  -> do su2 <- proveImplication False Nothing [] [] gs1
                extendSubst su2

-- | Prove an implication, and return any improvements that we computed.
-- Records errors, if any of the goals couldn't be solved.
proveImplication :: Bool -> Maybe Name ->
  [TParam] -> [Prop] -> [Goal] -> InferM Subst
proveImplication dedupErrs lnam as ps gs =
  do evars <- varsWithAsmps
     solver <- getSolver

     extraAs <- (map mtpParam . Map.elems) <$> getParamTypes
     extra   <- map thing <$> getParamConstraints

     (mbErr,su) <- io (proveImplicationIO solver dedupErrs lnam evars
                            (extraAs ++ as) (extra ++ ps) gs)
     case mbErr of
       Right ws  -> mapM_ recordWarning ws
       Left errs -> mapM_ recordError errs
     return su

-- | Tries to prove an implication. If proved, then returns `Right (m_su ::
-- InferM Subst)` where `m_su` is an `InferM` computation that results in the
-- solution substitution, and records any warning invoked during proving. If not
-- proved, then returns `Left (m_err :: InferM ())`, which records all errors
-- invoked during proving.
tryProveImplication :: 
  Maybe Name -> [TParam] -> [Prop] -> [Goal] -> InferM Bool
tryProveImplication lnam as ps gs =
  do evars <- varsWithAsmps
     solver <- getSolver

     extraAs <- (map mtpParam . Map.elems) <$> getParamTypes
     extra   <- map thing <$> getParamConstraints

     (mbErr,_su) <- io (proveImplicationIO solver False lnam evars
                            (extraAs ++ as) (extra ++ ps) gs)
     case mbErr of
       Left {}  -> pure False
       Right {} -> pure True

proveImplicationIO :: Solver
                   -> Bool     -- ^ Whether to remove duplicate goals in errors
                   -> Maybe Name     -- ^ Checking this function
                   -> Set TVar -- ^ These appear in the env., and we should
                               -- not try to default them
                   -> [TParam] -- ^ Type parameters
                   -> [Prop]   -- ^ Assumed constraint
                   -> [Goal]   -- ^ Collected constraints
                   -> IO (Either [Error] [Warning], Subst)
proveImplicationIO _ _         _ _         _  []     []  = return (Right [], emptySubst)
proveImplicationIO s dedupErrs f varsInEnv ps asmps0 gs0 =
  do let ctxt = buildSolverCtxt asmps
     res <- quickSolverIO ctxt gs
     case res of
       Left erro -> return (Left [erro], emptySubst)
       Right (su,[]) -> return (Right [], su)
       Right (su,gs1) ->
         do gs2 <- proveImp s asmps gs1
            case gs2 of
              [] -> return (Right [], su)
              gs3 ->
                do let free = filter isFreeTV
                            $ Set.toList
                            $ Set.difference (fvs (map goal gs3)) varsInEnv
                   case defaultAndSimplify free gs3 of
                     (_,_,newSu,_,errs)
                        | isEmptySubst newSu ->
                                 return (Left (err gs3:errs), su) -- XXX: Old?
                     (_,newGs,newSu,ws,errs) ->
                       do let su1 = newSu @@ su
                          (res1,su2) <- proveImplicationIO s dedupErrs f varsInEnv ps
                                                 (apSubst su1 asmps0) newGs
                          let su3 = su2 @@ su1
                          case res1 of
                            Left bad -> return (Left (bad ++ errs), su3)
                            Right ws1
                              | null errs -> return (Right (ws++ws1),su3)
                              | otherwise -> return (Left errs, su3)
  where
  err us = cleanupError
           $ UnsolvedDelayedCt
           $ DelayedCt { dctSource = f
                       , dctForall = ps
                       , dctAsmps  = asmps0
                       , dctGoals  = if dedupErrs then nubOrd us else us
                       }


  asmps1 = concatMap pSplitAnd asmps0
  (asmps,gs) =
     let gs1 = [ g { goal = p } | g <- gs0, p <- pSplitAnd (goal g)
                                , notElem p asmps1 ]
     in case matchMaybe (improveProps True mempty asmps1) of
          Nothing -> (asmps1,gs1)
          Just (newSu,newAsmps) ->
             ( [ TVar x =#= t | (x,t) <- substToList newSu ]
               ++ newAsmps
             , [ g { goal = apSubst newSu (goal g) } | g <- gs1 ]
             )




cleanupError :: Error -> Error
cleanupError err =
  case err of
    UnsolvedDelayedCt d ->
      let noInferVars = Set.null . Set.filter isFreeTV . fvs . goal
          without = filter noInferVars (dctGoals d)
      in UnsolvedDelayedCt $
            if not (null without) then d { dctGoals = without } else d

    _ -> err



buildSolverCtxt :: [Prop] -> Ctxt
buildSolverCtxt ps0 =
  let ps = saturateProps mempty ps0
      ivals = assumptionIntervals mempty (Set.toList ps)
   in SolverCtxt
      { intervals = ivals
      , saturatedAsmps = ps
      }

 where
 saturateProps gs [] = gs
 saturateProps gs (p:ps)
   | Set.member p gs = saturateProps gs ps
   | Just (n,_) <- pIsLiteral p =
       let gs' = Set.fromList [p, pFin n] <> gs
        in saturateProps gs' ps
   | otherwise =
        let gs' = Set.singleton p <> superclassSet p <> gs
         in saturateProps gs' ps

 assumptionIntervals as ps =
   case computePropIntervals as ps of
     NoChange -> as
     InvalidInterval {} -> as -- XXX: say something
     NewIntervals bs -> Map.union bs as
