{- |
Module      :  ./OWL2/ShipSyntax.hs
Copyright   :  (c) C. Maeder DFKI GmbH
License     :  GPLv2 or higher, see LICENSE.txt

Maintainer  :  Christian.Maeder@dfki.de
Stability   :  provisional
Portability :  portable

different pretty printing for the SHIP tool
-}

module OWL2.ShipSyntax where

import OWL2.AS

import Common.Doc
import Common.DocUtils
import Common.Parsec

import Control.Monad

import Data.Char

import Text.ParserCombinators.Parsec

data Concept
  = CName String
  | NominalC String
  | NotC Concept
  | JoinedC (Maybe JunctionType) [Concept] -- Nothing denotes disjoint union
  | Quant (Either QuantifierType (CardinalityType, Int)) Role Concept
  deriving (Show, Eq, Ord)

topC :: Concept
topC = CName "T"

data NotOrInverse = NotR | InvR deriving (Show, Eq, Ord)

data Role
  = RName String
  | NominalR String String
  | UnOp NotOrInverse Role
  | JoinedR (Maybe JunctionType) [Role] -- Nothing denotes composition!
  deriving (Show, Eq, Ord)

topR :: Role
topR = RName "TxT"

botR :: Role
botR = UnOp NotR topR

ppJunction :: Bool -> Maybe JunctionType -> Doc
ppJunction c mt = text $ case mt of
  Just t -> case t of
    UnionOf -> "+ "
    IntersectionOf -> "& "
  Nothing -> if c then "<+> " else ". "

pppNegConcept :: Concept -> Doc
pppNegConcept c = case c of
    JoinedC {} -> parens
    _ -> id
  $ ppConcept c

pppConcept :: Bool -> Maybe JunctionType -> Concept -> Doc
pppConcept notLast mt c = case c of
    Quant {} | notLast -> parens
    JoinedC mti _
      | mti /= mt && maybe True (const $ maybe False (/= UnionOf) mt) mti
        -> parens
    _ -> id
  $ ppConcept c

ppConcept :: Concept -> Doc
ppConcept co = case co of
  CName s -> (if co == topC then keyword else text) s
  NominalC s -> braces $ text s
  NotC c -> text "~" <> pppNegConcept c
  JoinedC t l -> case reverse l of
    [] -> ppConcept $ if t == Just IntersectionOf then topC else NotC topC
    f : r -> fsep . prepPunctuate (ppJunction True t)
      . reverse $ pppConcept False t f : map (pppConcept True t) r
  Quant q r c -> fsep [(case q of
    Left v -> keyword (showQuant v)
    Right (n, i) -> text (showCard n) <+> text (show i)
    ) <+> ppRole r, dot <+> ppConcept c]

showQuant :: QuantifierType -> String
showQuant v = case v of
      AllValuesFrom -> "all"
      SomeValuesFrom -> "ex"

showCard :: CardinalityType -> String
showCard n = case n of
      MinCardinality -> ">="
      MaxCardinality -> "<="
      ExactCardinality -> "="

showSame :: SameOrDifferent -> String
showSame s = case s of
  Same -> "=="
  Different -> "!="

pppRole :: Maybe JunctionType -> Role -> Doc
pppRole mt r = case r of
    JoinedR (Just ti) _
      | maybe True (\ to -> ti /= to && ti == UnionOf) mt
        -> parens
    _ -> id
  $ ppRole r

ppRole :: Role -> Doc
ppRole ro = case ro of
  RName s -> (if ro == topR then keyword else text) s
  NominalR s t -> braces $ parens $ text s <> comma <+> text t
  UnOp o r -> (case o of
    NotR -> text "~"
    InvR -> keyword "inv") <> (case r of
      JoinedR {} -> parens
      _ | o == InvR -> parens
      _ -> id) (ppRole r)
  JoinedR t l -> case l of
    [] -> ppRole $ if t /= Just UnionOf then topR else botR
    _ -> fsep . prepPunctuate (ppJunction False t) $ map (pppRole t) l

skip :: CharParser st ()
skip = forget $ many $ single (satisfy isSpace) <|> nestedComment "/*" "*/"
       <|> tryString "//" <++> many (noneOf "\n")

myLetter :: CharParser st Char
myLetter = satisfy $ \ c -> isAlphaNum c || elem c "_"

nominal :: CharParser st String
nominal = reserved
  ["_", "all", "ex", "inv", "not", "if", "effect", "causes", "cond", "exec"]
  (many1 myLetter) << skip

key :: String -> CharParser st ()
key s = forget $ try $ string s >> notFollowedBy myLetter

skipKey :: String -> CharParser st ()
skipKey s = key s << skip

quant :: CharParser st QuantifierType
quant = choice $ map (\ a -> key (showQuant a) >> return a)
        [AllValuesFrom, SomeValuesFrom]

card :: CharParser st CardinalityType
card = choice $ map (\ a -> tryString (showCard a) >> return a)
       [MinCardinality, MaxCardinality, ExactCardinality]

quantOrCard :: CharParser st (Either QuantifierType (CardinalityType, Int))
quantOrCard = fmap Left quant
  <|> do
  c <- card << skip
  i <- many1 digit
  return $ Right (c, read i)

skipChar :: Char -> CharParser st ()
skipChar c = char c >> skip

commaP, colonP, equalP, bulletP :: CharParser st ()
commaP = skipChar ','
colonP = skipChar ':'
equalP = skipChar '='
bulletP = skipChar '.'

primConcept :: CharParser st Concept
primConcept = do
    q <- quantOrCard << skip
    r <- primRole
    bulletP
    fmap (Quant q r) concept
  <|> ((key "not" <|> skipChar '~') >> skip >> fmap NotC primConcept)
  <|> braced (fmap NominalC nominal) -- allow more nominals
  <|> parent concept
  <|> fmap CName nominal

braced :: CharParser st a -> CharParser st a
braced p = skipChar '{' >> p << skipChar '}'

parent :: CharParser st a -> CharParser st a
parent p = skipChar '(' >> p << skipChar ')'

binPP :: ([a] -> a) -> CharParser st sep -> CharParser st a -> CharParser st a
binPP f sp p = do
  a <- p
  l <- many $ sp >> p
  return $ if null l then a else f $ a : l

binP :: ([a] -> a) -> String -> CharParser st a -> CharParser st a
binP f s = binPP f $ tryString s >> skip

binC :: ([a] -> a) -> Char -> CharParser st a -> CharParser st a
binC f c = binPP f $ skipChar c

andConcept :: CharParser st Concept
andConcept = binC (JoinedC $ Just IntersectionOf) '&' primConcept

plus :: CharParser st ()
plus = try $ char '+' >> notFollowedBy (char '>') >> skip

orConcept :: CharParser st Concept
orConcept = binPP (JoinedC $ Just UnionOf) plus andConcept

concept :: CharParser st Concept
concept = binP (JoinedC Nothing) "<+>" orConcept

notOrInv :: CharParser st NotOrInverse
notOrInv = (char '~' >> return NotR)
  <|> (key "inv" >> return InvR)

nomPair :: (String -> String -> a) -> CharParser st a
nomPair f = parent $ liftM2 f nominal $ commaP >> nominal

primRole :: CharParser st Role
primRole = do
    o <- notOrInv << skip
    fmap (UnOp o) primRole
  <|> braced (nomPair NominalR)
  <|> parent role
  <|> fmap RName nominal

compRole :: CharParser st Role
compRole = binC (JoinedR Nothing) '.' primRole

andRole :: CharParser st Role
andRole = binC (JoinedR $ Just IntersectionOf) '&' compRole

role :: CharParser st Role
role = binPP (JoinedR $ Just UnionOf) plus andRole

data EqOrLess = Eq | Less deriving (Show, Eq, Ord)

data RoleType = RoleType Concept Concept deriving (Show, Eq, Ord)

-- a missing rhs may be modelled as "< T" or by no constructors
data ConceptRhs
  = ADTCons [TBoxCons]
  | ConceptRel EqOrLess Concept
  deriving (Show, Eq, Ord)

data TBoxCons = TBoxCons Concept [(Role, Concept)]
  deriving (Show, Eq, Ord)

data TBox
  = ConceptDecl Concept ConceptRhs
  | DisjointCs [Concept]
  deriving (Show, Eq, Ord)

data RBox
  = RoleDecl Role RoleType
  | RoleRel Role EqOrLess Role
  | RoleProp Character Role
  deriving (Show, Eq, Ord)

-- | assertions
data ABox
  = AConcept String Concept
  | ARole String String Role
  | AIndividual String SameOrDifferent String
  deriving (Show, Eq, Ord)

data Box = Box [TBox] [RBox] [ABox]
  deriving (Show, Eq, Ord)

ppEqOrLess :: EqOrLess -> Doc
ppEqOrLess s = case s of
  Eq -> equals
  Less -> less

ppRoleType :: RoleType -> Doc
ppRoleType (RoleType s t) =
  fsep [colon <+> ppConcept s, cross <+> ppConcept t]

ppConceptRhs :: ConceptRhs -> Doc
ppConceptRhs rhs = case rhs of
  ADTCons cs -> if null cs then empty else
     text "::=" <+> vcat (prepPunctuate (text "| ") $ map ppTBoxCons cs)
  ConceptRel o c -> ppEqOrLess o <+> ppConcept c

ppTBoxCons :: TBoxCons -> Doc
ppTBoxCons (TBoxCons c sels) =
  ppConcept c <> if null sels then empty else
    parens . sepByCommas $ map
      (\ (r, a) -> ppRole r <+> colon <+> ppConcept a) sels

ppTBox :: TBox -> Doc
ppTBox b = case b of
  ConceptDecl d m -> ppConcept d <+> ppConceptRhs m
  DisjointCs cs -> keyword "Disjoint" <> parens (sepByCommas $ map ppConcept cs)

ppRBox :: RBox -> Doc
ppRBox b = case b of
  RoleDecl r t -> fsep [ppRole r, ppRoleType t]
  RoleRel r o t -> fsep [ppRole r, ppEqOrLess o <+> ppRole t]
  RoleProp c s -> keyword (showCharacter c) <> parens (ppRole s)

ppABox :: ABox -> Doc
ppABox b = case b of
  AConcept n c -> text n <> colon <> ppConcept c
  ARole s t r -> parens (text s <> comma <+> text t)
    <> colon <> ppRole r
  AIndividual s c t -> text s <> text (showSame c) <> text t

ppBox :: Box -> Doc
ppBox (Box ts rs as) = let ppM c = keyword $ '%' : c : "BOX" in
  vcat $ ppM 'T' : map ppTBox ts ++ ppM 'R' : map ppRBox rs
           ++ ppM 'A' : map ppABox as

showCharacter :: Character -> String
showCharacter c = case c of
    Functional -> "Func"
    InverseFunctional -> "FuncInv"
    Reflexive -> "Ref"
    Irreflexive -> "Irref"
    Symmetric -> "Sym"
    Asymmetric -> "Asym"
    Antisymmetric -> "Dis"
    Transitive -> "Trans"

character :: CharParser st Character
character = choice $ map (\ a -> key (showCharacter a) >> return a)
  [minBound .. maxBound]

eqOrLess :: CharParser st EqOrLess
eqOrLess = (equalP >> return Eq) <|> (skipChar '<' >> return Less)

tbox :: CharParser st TBox
tbox = (key "Disjoint" >> fmap DisjointCs
        (parent $ concept <:> many (commaP >> concept)))
  <|> try (liftM2 ConceptDecl concept
    (liftM2 ConceptRel eqOrLess concept
     <|> fmap ADTCons
     ((tryString "::=" >> skip >> sepBy tboxCons (skipChar '|'))
      <|> (char ':' >> pzero)
      <|> (string "==" >> pzero)
      <|> (string "!=" >> pzero)
      <|> return [])))

tboxCons :: CharParser st TBoxCons
tboxCons = liftM2 TBoxCons concept
  . optionL . parent . flip sepBy commaP
  . pair role $ colonP >> concept

rbox :: CharParser st RBox
rbox = liftM2 RoleProp character (parent role)
  <|> try (do
    r <- role
    liftM2 (RoleRel r) eqOrLess role
      <|> fmap (RoleDecl r) (liftM2 RoleType
        (colonP >> concept)
        $ skipChar '*' >> concept)) -- added try to recognize abox

abox :: CharParser st ABox
abox = liftM2 ($) (nomPair ARole) (colonP >> role)
  <|> do
    n <- nominal
    fmap (AConcept n) (colonP >> concept)
      <|> liftM2 (AIndividual n) (pSame << skip) nominal

pSame :: CharParser st SameOrDifferent
pSame = choice $ map (\ a -> tryString (showSame a) >> return a)
        [Same, Different]

box :: CharParser st Box
box = do
  forget $ many imprts
  optional $ skipKey "%TBOX"
  ts <- many tbox
  optional $ skipKey "%RBOX"
  rs <- many rbox
  optional $ skipKey "%ABOX"
  as <- many abox
  return $ Box ts rs as

imprts :: CharParser st ()
imprts = skipKey "import" << many1 (myLetter <|> char '.') << skip
  << optional rename

rename :: CharParser st ()
rename = do
  skipKey "with"
  optional $ skipKey "concepts" << nmap
  optional $ skipKey "roles" << nmap
  optional $ skipKey "individuals" << nmap
  return ()

nmap :: GenParser Char st [()]
nmap = sepBy1 (nominal >> skipKey "as" << nominal) commaP

ppp :: (a -> Doc) -> CharParser () a -> String -> String
ppp pr pa s = case parse (skip >> pa << eof) "" s of
  Right a -> show $ pr a
  Left e -> show e
