{-# LANGUAGE DeriveDataTypeable #-}
module Scyther.Theory (

  -- * Theorems
    Theorem
  , thmProto
  , thmName
  , thmSequent
  , thmProof
  , isAxiom

  -- * Theory items
  , ThyItem(..)

  -- * Theory Datatypes
  , Theory(..)

  -- ** Construction
  , adaptTheoryName
  , insertItem
  , shrinkTheory
  , proveSequents
  , mapTheorySequents
  , addMissingTypingInvariants
  , ensureUniqueRoleNames
  , composeParallel

  -- ** Queries
  , lookupProtocol
  , unsoundTheorems
  , theoryProofSize
  , theoryOverview
  , classifyProperties
  -- , wfTheory

) where

import Safe
import Data.Either (partitionEithers)
import Data.Monoid
import Data.List (isPrefixOf)
import qualified Data.Set as S
import qualified Data.Map as M
import Data.DAG.Simple
import Data.Data
import Data.Generics.Uniplate.Data

import Control.Monad
import Control.Monad.State

import System.FilePath

import Extension.Prelude

import Scyther.Facts
import Scyther.Sequent
import Scyther.Proof

------------------------------------------------------------------------------
-- Theorems
------------------------------------------------------------------------------

-- | A theorem is just a named proof.
type Theorem = Named Proof

-- | The name of a theorem.
thmName :: Theorem -> String
thmName = fst

-- | The proof of a theorem.
thmProof :: Theorem -> Proof
thmProof = snd

-- | The sequent of a theorem.
thmSequent :: Theorem -> Sequent
thmSequent = prfSequent . thmProof

-- | The protocol of a theorem.
thmProto :: Theorem -> Protocol
thmProto = prfProto . thmProof

-- | Check if a theorem is an axiom.
isAxiom :: Theorem -> Bool
isAxiom = isAxiomProof . snd


------------------------------------------------------------------------------
-- Theory Items
------------------------------------------------------------------------------


-- | A theory item is a (meta-)logical fact representable by our security
-- protocol theory.
data ThyItem =
    ThyProtocol Protocol
  | ThySequent (Named Sequent)
  | ThyTheorem (Named Proof)
  | ThyText String
  deriving( Eq, Show, Data, Typeable {-! NFData !-} )


------------------------------------------------------------------------------
-- Theories
------------------------------------------------------------------------------

data Theory = Theory {
    thyName  :: String
  , thyItems :: [ThyItem]
  }
  deriving( Eq, Show, Data, Typeable {-! NFData !-} )


-- Construction
---------------

-- | Map the items of a theory.
mapTheoryItems :: ([ThyItem] -> [ThyItem]) -> Theory -> Theory
mapTheoryItems f (Theory name items) = Theory name (f items)

-- | Map the sequents of a theory.
mapTheorySequents :: (Sequent -> Sequent) -> Theory -> Theory
mapTheorySequents f = mapTheoryItems (map mapThyItemSequents)
  where
  mapThyItemSequents item = case item of
    ThySequent (name, se)  -> ThySequent (name, f se)
    ThyTheorem (name, prf) -> ThyTheorem (name, mapProofSequents f prf)
    ThyProtocol _          -> item
    ThyText _              -> item

-- | Insert a theory item into a theory.
insertItem :: ThyItem -> Theory -> Theory
insertItem item thy = thy { thyItems = thyItems thy ++ [item] }

-- | Most proofs require a typing invariant to be present for the protocol
-- under investigation. We try to derive one from the protcol specification
-- for every protocol that has none.
addMissingTypingInvariants :: Theory -> Theory
addMissingTypingInvariants thy =
    Theory (thyName thy) (concatMap add $ thyItems thy)
  where
    mkTypingSequent p = case mscTyping p of
        Just typ -> return $ ThySequent ( protoName p ++ "_msc_typing"
                                        , Sequent (empty p) (FAtom (ATyping typ)))
        Nothing  -> mzero

    add item = case item of
        ThyProtocol p 
          | noTypingInvariant p -> [item] ++ mkTypingSequent p
        _                       -> [item]

    noTypingInvariant p = and $ do
        item <- thyItems thy
        se <- case item of
                ThySequent (_, se)  -> return se
                ThyTheorem (_, prf) -> return (prfSequent prf)
                _                   -> mzero
        guard (isTypingFormula $ seConcl se)
        return (p /= seProto se)

-- | Prove all claims with the given heuristic optionally using the given
-- bound.
proveSequents :: (Sequent -> Theorem -> Bool) 
                 -- ^ Predicate determining theorems for reuse for proving the
                 -- given sequent.
              -> ([Named Sequent] -> Sequent -> Maybe Proof) 
                 -- ^ Proof construction function.
              -> Theory -> Theory
proveSequents reuse prover thy =
  Theory (thyName thy) (flip execState [] . mapM_ prove $ thyItems thy)
  where
  prove (item@(ThySequent (name, se))) = do
    prevItems <- get
    let reusableThms = [ (thmName th, thmSequent th) 
                       | ThyTheorem th <- prevItems, reuse se th, thmProto th == seProto se]

    case prover reusableThms se of
      Just prf -> modify (++[ThyTheorem (name, prf)])
      Nothing  -> modify (++[item])

  prove item = modify (++[item])

-- | Only keep theorems for which the given predicate is true or which are
-- referenced by some kept theorem.
shrinkTheory :: (String -> Bool) -> Theory -> Theory
shrinkTheory mustKeep (Theory name items) =
  Theory name (filter keep items)
  where
  thmInfo thm = (thmName thm, thmProto thm)
  kept = [ thmInfo thm | ThyTheorem thm <- items, mustKeep $ thmName thm ]
  dependencies =
    [ (thmInfo thm, dep) 
    | ThyTheorem thm <- items, dep <- S.toList . depends $ thmProof thm ]
  toKeep = reachableSet kept dependencies
  keep :: ThyItem -> Bool
  keep (ThyTheorem thm) = (thmInfo thm `S.member` toKeep)
  keep (ThySequent se)  = mustKeep $ fst se
  keep _                = True

-- | Adapt the theory name to the base name of the given file.
adaptTheoryName :: FilePath -> Theory -> Theory
adaptTheoryName file thy = thy { thyName = takeBaseName file }

-- | Ensures that all roles are globally uniquely named by prefixing all role
-- names with their corresponding protocol name, if two roles have equal names.
ensureUniqueRoleNames :: Theory -> Theory
ensureUniqueRoleNames thy
  | unique rs = thy
  | otherwise = mapTheoryItems (map addPrefix) thy
  where
    rs = [roleName r | ThyProtocol p <- thyItems thy, r <- protoRoles p]

    prefixRoleName p r = r {roleName = protoName p ++ "_" ++ roleName r}

    getProtocol (ThyProtocol p) = return p
    getProtocol (ThySequent s)  = return (seProto $ snd s)
    getProtocol (ThyTheorem p)  = return (thmProto p)
    getProtocol (ThyText _)     = mzero

    addPrefix item = case getProtocol item of
        Just p  -> transformBi (prefixRoleName p) item
        Nothing -> item

-- | Compose all protocol in the theory in parallel. Assumes that both protocol
-- names as well as role names are /globally/ unique.
composeParallel :: Theory -> Theory
composeParallel thy 
    | length ps <= 1 = thy
    | otherwise      = thy { thyItems = [ThyProtocol pc, ThySequent sec] ++ items3 }
  where
    (ps,  items1) = partitionEithers $ do
        item <- thyItems thy
        case item of 
            ThyProtocol p -> return $ Left p
            _             -> return $ Right item

    (typs, items2) = partitionEithers $ do
        item <- items1 
        case item of
            ThySequent (_, Sequent _ (FAtom (ATyping (Typing typ))))
              -> return $ Left typ
            ThySequent x -> return $ Right $ ThySequent $ prefixProtoName (seProto $ snd x) x
            ThyTheorem x -> return $ Right $ ThyTheorem $ prefixProtoName (thmProto x) x
            _            -> return $ Right item


    prefixProtoName :: Protocol -> (String, a) -> (String, a)
    prefixProtoName p x@(n, v)
      | protoName p `isPrefixOf` n = x
      | otherwise                  = (protoName p ++ "_" ++ n, v)
     
    namec = thyName thy
    pc    = Protocol namec (concatMap protoRoles ps)
    sec   = ( namec ++ "_composed_typing"
            , Sequent (empty pc) (FAtom (ATyping (Typing (M.unions typs))))
            )

    replaceProtocol :: Protocol -> Protocol
    replaceProtocol = const pc

    items3 = transformBi replaceProtocol items2


-- Queries
----------

-- | Find a protocol in the theory according to its name.
lookupProtocol :: String -> Theory -> Maybe Protocol
lookupProtocol name thy = 
  headMay [proto | ThyProtocol proto <- thyItems thy, protoName proto == name]

-- | Find all unsound theorems of the theory.
unsoundTheorems :: Theory -> [(Protocol, String)]
unsoundTheorems (Theory _ items) = 
  [ (prfProto prf, name) 
  | ThyTheorem (name, prf) <- items, not (sound prf) 
  ] ++
  [ (seProto se, name) 
  | ThySequent (name, se) <- items
  ]

-- | Total proof size.
theoryProofSize :: Theory -> ProofSize
theoryProofSize (Theory _ items) = mconcat $
  [ proofSize (thmProof th) | ThyTheorem th <- items ] ++
  [ missingProofSize        | ThySequent _  <- items ]

-- | Count and classify the properties selected by the given predicate into
-- secrecy properties, authentication properties, and other properties.
classifyProperties :: (String -> Bool) -> Theory -> (Int, Int, Int)
classifyProperties toClassify = foldl classify (0,0,0) . thyItems
  where
  getSequent (ThyTheorem thm) = do
    guard (toClassify $ thmName thm) 
    return $ thmSequent thm
  getSequent (ThySequent se)  = do
    guard (toClassify (fst se))
    return (snd se)
  getSequent _                = mzero

  classify counts@(nSec, nAuth, nOther) item = case getSequent item of
    Just se | isContradictionProp se   -> (succ nSec,      nAuth,      nOther)
    Just se | isExistsStructureProp se -> (     nSec, succ nAuth,      nOther)
    Just _                             -> (     nSec,      nAuth, succ nOther)
    Nothing                            -> counts

  -- | True, if the sequent implies 'FFalse'.
  isContradictionProp :: Sequent -> Bool
  isContradictionProp = (FAtom AFalse ==) . seConcl

  -- | True, if the sequents conclusion contains an existentially quantified
  -- thread.
  isExistsStructureProp :: Sequent -> Bool
  isExistsStructureProp = hasQuantifiers . seConcl

-------------------------------------------------------------------------------
-- Overview generation
-------------------------------------------------------------------------------

theoryOverview :: Theory -> Theory
theoryOverview = error "theoryOverview: upgrade to new infrastructure"

{-
lookupTypeInvariants :: Protocol -> Theory -> [(ThyItem, Maybe Typing)]
lookupTypeInvariants proto thy = do
  item <- thyItems thy
  se <- case item of
    ThySequent _ se -> return se
    ThyTheorem th   -> return $ thmSequent th
    _               -> mzero
  guard (seProto se == proto)
  case seConcl se of
    FTyping typ -> return (item, typ)
    _           -> mzero

-- | The overview of a theory lists for each protocol its associated typings
-- together with the corresponding chain rule. If no typing is given, then it
-- tries to infer it from the message sequence chart.
theoryOverview :: Theory -> Theory
theoryOverview thy@(Theory name items) = 
  Theory name $ concat [ protoOverview proto | ThyProtocol proto <- items]
  where
  protoOverview proto = 
    (ThyText $ "Overview of protocol '"++protoName proto++"'") : 
    ThyProtocol proto : typInvs
    where
    typInvs = case lookupTypeInvariants proto thy of
      [] -> case mscTyping proto of
        Just typing -> addChainRule
          ( ThySequent "auto_msc_typing" $
               Sequent proto (FFacts emptyFacts) (FTyping (Just typing))
          , Just typing )
        Nothing   -> return $
          ThyText "failed to infer typing from protocol specification\n\
                  \(maybe some message patterns are not unifiable)"
      xs -> concatMap addChainRule xs

    addChainRule (item, typ) = 
      [ item
      , ThyText $ unlines
          [ "Note that the chain rule below is only an informal representation of the"
          , "actual chain rule, which you can find in the Isabelle theories. In particular,"
          , "it is missing the cases for the initial intruder knowledge and uses a dummy"
          , "conclusion 'False'." ]
      , ThyTheorem $ Theorem ("chain_rule_OF_" ++ name) (displayChainRule proto typ)
      ]
      where
      name = case item of
        ThySequent n _ -> n
        ThyTheorem th  -> thmName th
        _              -> error $ "theoryOverview: expected sequent or theorem item"


-}