{-# LANGUAGE GADTs          #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes     #-}
{-# LANGUAGE Safe           #-}
{-# LANGUAGE ViewPatterns   #-}

-- | Convert modular transition systems ('TransSys') into Kind2 file
-- specifications.
module Copilot.Theorem.Kind2.Translate
  ( toKind2
  , Style (..)
  ) where

import Copilot.Theorem.TransSys
import qualified Copilot.Theorem.Kind2.AST as K

import Control.Exception.Base (assert)

import Data.Function (on)
import Data.Maybe (fromJust)

import Data.List (sort, sortBy)
import Data.Map (Map, (!))

import qualified Data.Map as Map
import qualified Data.Bimap as Bimap

-- The following properties MUST hold for the given transition system :
-- * Nodes are sorted by topological order
-- * Nodes are `completed`, which means the dependency graph is transitive
--   and each node imports all the local variables of its dependencies
--

type DepGraph = Map NodeId [NodeId]

-- | Style of the Kind2 files produced: modular (with multiple separate nodes),
-- or all inlined (with only one node).
--
-- In the modular style, the graph is simplified to remove cycles by collapsing
-- all nodes participating in a strongly connected components.
--
-- In the inlined style, the structure of the modular transition system is
-- discarded and the graph is first turned into a /non-modular transition/
-- /system/ with only one node, which can be then converted into a Kind2 file.
data Style = Inlined | Modular

-- | Produce a Kind2 file that checks the properties specified.
toKind2 :: Style     -- ^ Style of the file (modular or inlined).
        -> [PropId]  -- ^ Assumptions
        -> [PropId]  -- ^ Properties to be checked
        -> TransSys  -- ^ Modular transition system holding the system spec
        -> K.File
toKind2 style assumptions checkedProps spec =
  addAssumptions spec assumptions
  $ trSpec (complete spec') predCallsGraph assumptions checkedProps
  where
    predCallsGraph = specDependenciesGraph spec'
    spec' = case style of
      Inlined -> inline spec
      Modular -> removeCycles spec

trSpec :: TransSys -> DepGraph -> [PropId] -> [PropId] -> K.File
trSpec spec predCallsGraph _assumptions checkedProps = K.File preds props
  where
    preds = map (trNode spec predCallsGraph) (specNodes spec)
    props = map trProp $
      filter ((`elem` checkedProps) . fst) $
      Map.toList (specProps spec)

trProp :: (PropId, ExtVar) -> K.Prop
trProp (pId, var) = K.Prop pId (trVar . extVarLocalPart $ var)

trNode :: TransSys -> DepGraph -> Node -> K.PredDef
trNode spec predCallsGraph node =
  K.PredDef { K.predId, K.predStateVars, K.predInit, K.predTrans }
  where
    predId = nodeId node
    predStateVars = gatherPredStateVars spec node
    predInit  = mkConj $ initLocals  node
                         ++ map (trExpr False) (nodeConstrs node)
                         ++ predCalls True spec predCallsGraph node
    predTrans = mkConj $ transLocals node
                         ++ map (trExpr True) (nodeConstrs node)
                         ++ predCalls False spec predCallsGraph node

addAssumptions :: TransSys -> [PropId] -> K.File -> K.File
addAssumptions spec assumptions (K.File {K.filePreds, K.fileProps}) =
  K.File (changeTail aux filePreds) fileProps
  where
    changeTail f (reverse -> l) = case l of
      []     -> error "impossible"
      x : xs -> reverse $ f x : xs

    aux pred =
      let init'  = mkConj ( K.predInit  pred : map K.StateVar vars )
          trans' = mkConj ( K.predTrans pred : map K.PrimedStateVar vars )
      in pred { K.predInit = init', K.predTrans = trans' }

    vars =
      let bindings   = nodeImportedVars (specTopNode spec)
          toExtVar a = fromJust $ Map.lookup a (specProps spec)
          toTopVar (ExtVar nId v) = assert (nId == specTopNodeId spec) v
      in map (varName . toTopVar . toExtVar) assumptions

-- The ordering really matters here because the variables
-- have to be given in this order in a pred call
-- Our convention :
-- * First the local variables, sorted by alphabetical order
-- * Then the imported variables, by alphabetical order on
--   the father node then by alphabetical order on the variable name

gatherPredStateVars :: TransSys -> Node -> [K.StateVarDef]
gatherPredStateVars spec node = locals ++ imported
  where
    nodesMap = Map.fromList [(nodeId n, n) | n <- specNodes spec]
    extVarType :: ExtVar -> K.Type
    extVarType (ExtVar n v) =
      case nodeLocalVars (nodesMap ! n) ! v of
        VarDescr Integer _ -> K.Int
        VarDescr Bool    _ -> K.Bool
        VarDescr Real    _ -> K.Real

    locals =
      map (\v -> K.StateVarDef (varName v)
              (extVarType $ ExtVar (nodeId node) v) [])
         . sort . Map.keys $ nodeLocalVars node

    imported =
      map (\(v, ev) -> K.StateVarDef (varName v) (extVarType ev) [])
      . sortBy (compare `on` snd) . Bimap.toList $ nodeImportedVars node

mkConj :: [K.Term] -> K.Term
mkConj []  = trConst Bool True
mkConj [x] = x
mkConj xs  = K.FunApp "and" xs

mkEquality :: K.Term -> K.Term -> K.Term
mkEquality t1 t2 = K.FunApp "=" [t1, t2]

trVar :: Var -> K.Term
trVar v = K.StateVar (varName v)

trPrimedVar :: Var -> K.Term
trPrimedVar v = K.PrimedStateVar (varName v)

trConst :: Type t -> t -> K.Term
trConst Integer v     = K.ValueLiteral (show v)
trConst Real    v     = K.ValueLiteral (show v)
trConst Bool    True  = K.ValueLiteral "true"
trConst Bool    False = K.ValueLiteral "false"

initLocals :: Node -> [K.Term]
initLocals node =
  concatMap f (Map.toList $ nodeLocalVars node)
  where
    f (v, VarDescr t def) =
      case def of
        Pre     c _ -> [mkEquality (trVar v) (trConst t c)]
        Expr    e   -> [mkEquality (trVar v) (trExpr False e)]
        Constrs cs  -> map (trExpr False) cs

transLocals :: Node -> [K.Term]
transLocals node =
  concatMap f (Map.toList $ nodeLocalVars node)
  where
   f (v, VarDescr _ def) =
      case def of
        Pre _ v' -> [mkEquality (trPrimedVar v) (trVar v')]
        Expr e   -> [mkEquality (trPrimedVar v) (trExpr True e)]
        Constrs cs  -> map (trExpr True) cs

predCalls :: Bool -> TransSys -> DepGraph -> Node -> [K.Term]
predCalls isInitCall spec predCallsGraph node =
  map mkCall toCall
  where
    nid = nodeId node
    toCall = predCallsGraph ! nid
    nodesMap = Map.fromList [(nodeId n, n) | n <- specNodes spec]

    nodeLocals n =
      map (ExtVar n) . sort . Map.keys
      . nodeLocalVars $ (nodesMap ! n)

    mkCall callee
      | isInitCall =
          K.PredApp callee K.Init (argsSeq trVar)
      | otherwise  =
          K.PredApp callee K.Trans (argsSeq trVar ++ argsSeq trPrimedVar)
      where

        calleeLocals = nodeLocals callee
        calleeImported =
          (concatMap nodeLocals . sort . nodeDependencies) $ nodesMap ! callee

        localAlias trVarF ev =
          case Bimap.lookupR ev $ nodeImportedVars node of
            Nothing -> error $
              "This spec is not complete : "
              ++ show ev ++ " should be imported in " ++ nid
            Just v -> trVarF v

        argsSeq trVarF =
          map (localAlias trVarF) (calleeLocals ++ calleeImported)

trExpr :: Bool -> Expr t -> K.Term
trExpr primed = tr
  where
    tr :: forall t . Expr t -> K.Term
    tr (Const t c) = trConst t c
    tr (Ite _ c e1 e2) = K.FunApp "ite" [tr c, tr e1, tr e2]
    tr (Op1 _ op e) = K.FunApp (show op) [tr e]
    tr (Op2 _ op e1 e2) = K.FunApp (show op) [tr e1, tr e2]
    tr (VarE _ v) = if primed then trPrimedVar v else trVar v
