-- | Basic stuff for propositional logic: datatype, parsing and
-- printing.  'IsPropositional' is a subclass of 'IsLiteral' of
-- formula types that support binary combinations.

{-# OPTIONS_GHC -Wall #-}

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

module Data.Logic.ATP.Prop
    (
    -- * Propositional formulas
      IsPropositional((.|.), (.&.), (.<=>.), (.=>.), foldPropositional', foldCombination)
    , BinOp(..), binop
    , (), (==>), (), ()
    , (), (<=>), (), (<==>)
    , (), (·)
    , ()
    , foldPropositional
    , zipPropositional
    , convertPropositional
    , convertToPropositional
    , precedencePropositional
    , associativityPropositional
    , prettyPropositional
    , showPropositional
    , onatomsPropositional
    , overatomsPropositional
    -- * Restricted propositional formula class
    , JustPropositional
    -- * Interpretation of formulas.
    , eval
    , atoms
    -- * Truth Tables
    , TruthTable(TruthTable)
    , onallvaluations
    , truthTable
    -- * Tautologies and related concepts
    , tautology
    , unsatisfiable
    , satisfiable
    -- * Substitution
    , psubst
    -- * Dualization
    , dual
    -- * Simplification
    , psimplify
    , psimplify1
    -- * Normal forms
    , nnf
    , nenf
    , list_conj
    , list_disj
    , mk_lits
    , allsatvaluations
    , dnfSet
    , purednf
    , simpdnf
    , rawdnf
    , dnf
    , purecnf
    , simpcnf
    , cnf'
    , cnf_
    , trivial
    -- * Instance
    , Prop(P, pname)
    , PFormula(F, T, Atom, Not, And, Or, Imp, Iff)
    -- * Tests
    , testProp
    ) where

import Data.Foldable as Foldable (null)
import Data.Function (on)
import Data.Data (Data)
import Data.List as List (groupBy, intercalate, map, sortBy)
import Data.Logic.ATP.Formulas (atom_union, fromBool, IsAtom,
                                IsFormula(AtomOf, asBool, true, false, atomic, overatoms, onatoms), prettyBool)
import Data.Logic.ATP.Lib ((|=>), distrib, fpf, setAny)
import Data.Logic.ATP.Lit ((.~.), (¬), convertLiteral, convertToLiteral, IsLiteral(foldLiteral', naiveNegate, foldNegation),
                           JustLiteral, LFormula, negate, positive, )
import Data.Logic.ATP.Pretty (Associativity(InfixN, InfixR, InfixA), Doc, HasFixity(precedence, associativity),
                              Precedence, Pretty(pPrint, pPrintPrec), prettyShow, Side(Top, LHS, RHS, Unary), testParen, text,
                              notPrec, andPrec, orPrec, impPrec, iffPrec, leafPrec, boolPrec)
import Data.Map.Strict as Map (Map)
import Data.Maybe (fromMaybe)
import Data.Monoid ((<>))
import Data.Set as Set (empty, filter, fromList, intersection, isProperSubsetOf, map,
                        minView, partition, Set, singleton, toAscList, union)
import Data.String (IsString(fromString))
import Data.Typeable (Typeable)
import Prelude hiding (negate, null)
import Text.PrettyPrint.HughesPJClass (maybeParens, PrettyLevel, vcat)
import Test.HUnit

-- |A type class for propositional logic.  If the type we are writing
-- an instance for is a zero-order (aka propositional) logic type
-- there will generally by a type or a type parameter corresponding to
-- atom.  For first order or predicate logic types, it is generally
-- easiest to just use the formula type itself as the atom type, and
-- raise errors in the implementation if a non-atomic formula somehow
-- appears where an atomic formula is expected (i.e. as an argument to
-- atomic or to the third argument of foldPropositional.)
class IsLiteral formula => IsPropositional formula where
    -- | Disjunction/OR
    (.|.) :: formula -> formula -> formula
    -- | Conjunction/AND.  @x .&. y = (.~.) ((.~.) x .|. (.~.) y)@
    (.&.) :: formula -> formula -> formula
    -- | Equivalence.  @x .<=>. y = (x .=>. y) .&. (y .=>. x)@
    (.<=>.) :: formula -> formula -> formula
    -- | Implication.  @x .=>. y = ((.~.) x .|. y)@
    (.=>.) :: formula -> formula -> formula

    -- | A fold function that distributes different sorts of formula
    -- to its parameter functions, one to handle binary operators, one
    -- for negations, and one for atomic formulas.  See examples of its
    -- use to implement the polymorphic functions below.
    foldPropositional' :: (formula -> r)                     -- ^ fold on some higher order formula
                       -> (formula -> BinOp -> formula -> r) -- ^ fold on a binary operation formula.  Functions
                                                             -- of this type can be constructed using 'binop'.
                       -> (formula -> r)                     -- ^ fold on a negated formula
                       -> (Bool -> r)                        -- ^ fold on a boolean formula
                       -> (AtomOf formula -> r)              -- ^ fold on an atomic formula
                       -> formula -> r
    -- | An alternative fold function for binary combinations of formulas
    foldCombination :: (formula -> r) -- other
                    -> (formula -> formula -> r) -- disjunction
                    -> (formula -> formula -> r) -- conjunction
                    -> (formula -> formula -> r) -- implication
                    -> (formula -> formula -> r) -- equivalence
                    -> formula -> r

-- | Implication synonyms.  Note that if the -XUnicodeSyntax option is
-- turned on the operator ⇒ can not be declared/used as a function -
-- it becomes a reserved special character used in type signatures.
(), (), (==>), () :: IsPropositional formula => formula -> formula -> formula
() = (.=>.)
() = (.=>.)
(==>) = (.=>.)
() = (.=>.)
infixr 3 .=>., , , ==>, 

-- | If-and-only-if synonyms
(<=>), (<==>), (), () :: IsPropositional formula => formula -> formula -> formula
(<=>) = (.<=>.)
(<==>) = (.<=>.)
() = (.<=>.)
() = (.<=>.)
infixl 2 .<=>., <=>, <==>, , 

-- | And/conjunction synonyms
(), (·) :: IsPropositional formula => formula -> formula -> formula
() = (.&.)
(·) = (.&.)
infixl 5 .&., , ·

-- | Or/disjunction synonyms
() :: IsPropositional formula => formula -> formula -> formula
() = (.|.)
infixl 4 .|., 

-- | Deconstruct a 'JustPropositional' formula.
foldPropositional :: JustPropositional pf =>
                     (pf -> BinOp -> pf -> r) -- ^ fold on a binary operation formula
                  -> (pf -> r)                -- ^ fold on a negated formula
                  -> (Bool -> r)              -- ^ fold on a boolean formula
                  -> (AtomOf pf -> r)         -- ^ fold on an atomic formula
                  -> pf -> r
foldPropositional = foldPropositional' (error "JustPropositional failure")

-- | This type is used to construct the first argument of 'foldPropositional'.
data BinOp
    = (:<=>:)
    | (:=>:)
    | (:&:)
    | (:|:)
    deriving (Eq, Ord, Data, Typeable, Show, Enum, Bounded)

-- | Combine formulas with a 'BinOp', for use building the first
-- argument of 'foldPropositional'.
binop :: IsPropositional formula => formula -> BinOp -> formula -> formula
binop f1 (:<=>:) f2 = f1 .<=>. f2
binop f1 (:=>:) f2 = f1 .=>. f2
binop f1 (:&:) f2 = f1 .&. f2
binop f1 (:|:) f2 = f1 .|. f2

-- | Combine two 'JustPropositional' formulas if they are similar.
zipPropositional :: (JustPropositional pf1, JustPropositional pf2) =>
                    (pf1 -> BinOp -> pf1 -> pf2 -> BinOp -> pf2 -> Maybe r) -- ^ Combine two binary operation formulas
                 -> (pf1 -> pf2 -> Maybe r)                                 -- ^ Combine two negated formulas
                 -> (Bool -> Bool -> Maybe r)                               -- ^ Combine two boolean formulas
                 -> (AtomOf pf1 -> AtomOf pf2 -> Maybe r)                   -- ^ Combine two atomic formulas
                 -> pf1 -> pf2 -> Maybe r                                   -- ^ Result is Nothing if the formulas don't unify
zipPropositional co ne tf at fm1 fm2 =
    foldPropositional co' ne' tf' at' fm1
    where
      co' l1 op1 r1 = foldPropositional (co l1 op1 r1) (\_ -> Nothing) (\_ -> Nothing) (\_ -> Nothing) fm2
      ne' x1 = foldPropositional (\_ _ _ -> Nothing)     (ne x1)     (\_ -> Nothing) (\_ -> Nothing) fm2
      tf' x1 = foldPropositional (\_ _ _ -> Nothing) (\_ -> Nothing)     (tf x1)     (\_ -> Nothing) fm2
      at' a1 = foldPropositional (\_ _ _ -> Nothing) (\_ -> Nothing) (\_ -> Nothing)     (at a1)     fm2

-- | Convert any instance of 'JustPropositional' to any 'IsPropositional' formula.
convertPropositional :: (JustPropositional pf1, IsPropositional pf2) =>
                        (AtomOf pf1 -> AtomOf pf2) -- ^ Convert an atomic formula
                     -> pf1 -> pf2
convertPropositional ca pf =
    foldPropositional co ne tf (atomic . ca) pf
    where
      co p (:&:) q = (convertPropositional ca p) .&. (convertPropositional ca q)
      co p (:|:) q = (convertPropositional ca p) .|. (convertPropositional ca q)
      co p (:=>:) q = (convertPropositional ca p) .=>. (convertPropositional ca q)
      co p (:<=>:) q = (convertPropositional ca p) .<=>. (convertPropositional ca q)
      ne p = (.~.) (convertPropositional ca p)
      tf = fromBool

-- | Convert any instance of 'IsPropositional' to a 'JustPropositional' formula.  Typically the
-- ho (higher order) argument calls error if it encounters something it can't handle.
convertToPropositional :: (IsPropositional formula, JustPropositional pf) =>
                          (formula -> pf)               -- ^ Convert a higher order formula
                       -> (AtomOf formula -> AtomOf pf) -- ^ Convert an atomic formula
                       -> formula -> pf
convertToPropositional ho ca fm =
    foldPropositional' ho co ne tf (atomic . ca) fm
    where
      co p (:&:) q = (convertToPropositional ho ca p) .&. (convertToPropositional ho ca q)
      co p (:|:) q = (convertToPropositional ho ca p) .|. (convertToPropositional ho ca q)
      co p (:=>:) q = (convertToPropositional ho ca p) .=>. (convertToPropositional ho ca q)
      co p (:<=>:) q = (convertToPropositional ho ca p) .<=>. (convertToPropositional ho ca q)
      ne p = (.~.) (convertToPropositional ho ca p)
      tf = fromBool

-- | Implementation of 'precedence' for any 'JustPropostional' type.
precedencePropositional ::JustPropositional pf => pf -> Precedence
precedencePropositional = foldPropositional co (\_ -> notPrec) (\_ -> boolPrec) precedence
    where
      co _ (:&:) _ = andPrec
      co _ (:|:) _ = orPrec
      co _ (:=>:) _ = impPrec
      co _ (:<=>:) _ = iffPrec

associativityPropositional :: JustPropositional pf => pf -> Associativity
associativityPropositional = foldPropositional co (const InfixA) (const InfixN) associativity
    where
      co _ (:&:) _ = InfixA
      co _ (:|:) _ = InfixA
      co _ (:=>:) _ = InfixR
      co _ (:<=>:) _ = InfixA -- yes, InfixA: (a<->b)<->c == a<->(b<->c)

-- | Implementation of 'pPrint' for any 'JustPropostional' type.
prettyPropositional :: forall pf. JustPropositional pf =>
                       Side -> PrettyLevel -> Rational -> pf -> Doc
prettyPropositional side l r fm =
    maybeParens (testParen side r (precedence fm) (associativity fm)) $ foldPropositional co ne tf at fm
    where
      co :: pf -> BinOp -> pf -> Doc
      co p (:&:) q = prettyPropositional LHS l (precedence fm) p <> text "∧" <>  prettyPropositional RHS l (precedence fm) q
      co p (:|:) q = prettyPropositional LHS l (precedence fm) p <> text "∨" <> prettyPropositional RHS l (precedence fm) q
      co p (:=>:) q = prettyPropositional LHS l (precedence fm) p <> text "⇒" <> prettyPropositional RHS l (precedence fm) q
      co p (:<=>:) q = prettyPropositional LHS l (precedence fm) p <> text "⇔" <> prettyPropositional RHS l (precedence fm) q
      ne p = text "¬" <> prettyPropositional Unary l (precedence fm) p
      tf x = prettyBool x
      at x = pPrintPrec l r x

-- | Implementation of 'show' for any 'JustPropositional' type.  For clarity, show methods fully parenthesize
showPropositional :: JustPropositional pf => Side -> Int -> pf -> ShowS
showPropositional side parentPrec fm =
    showParen (testParen side parentPrec (precedence fm) (associativity fm)) $ foldPropositional co ne tf at fm
    where
      co p (:&:) q = showPropositional LHS (precedence fm) p . showString " .&. " . showPropositional RHS (precedence fm) q
      co p (:|:) q = showPropositional LHS (precedence fm) p . showString " .|. " . showPropositional RHS (precedence fm) q
      co p (:=>:) q = showPropositional LHS (precedence fm) p . showString " .=>. " . showPropositional RHS (precedence fm) q
      co p (:<=>:) q = showPropositional LHS (precedence fm) p . showString " .<=>. " . showPropositional RHS (precedence fm) q
      ne p = showString "(.~.) " . showPropositional Unary (succ (precedence fm)) p
      tf x = showsPrec (precedence fm) x
      at x = showString "atomic " . showsPrec (precedence fm) x

-- | Implementation of 'onatoms' for any 'JustPropositional' type.
onatomsPropositional :: JustPropositional pf => (AtomOf pf -> AtomOf pf) -> pf -> pf
onatomsPropositional f fm =
    foldPropositional co ne tf at fm
    where
      co p op q = binop (onatomsPropositional f p) op (onatomsPropositional f q)
      ne p = (.~.) (onatomsPropositional f p)
      tf flag = fromBool flag
      at x = atomic (f x)

-- | Implementation of 'overatoms' for any 'JustPropositional' type.
overatomsPropositional :: JustPropositional pf => (AtomOf pf -> r -> r) -> pf -> r -> r
overatomsPropositional f fof r0 =
    foldPropositional co ne (const r0) (flip f r0) fof
    where
      co p _ q = overatomsPropositional f p (overatomsPropositional f q r0)
      ne fof' = overatomsPropositional f fof' r0

-- | An instance of IsPredicate.
data Prop = P {pname :: String} deriving (Eq, Ord)

-- Because of the IsString instance, the Show instance can just be a String.
instance Show Prop where
    show (P {pname = s}) = show s

instance IsAtom Prop

-- Allows us to say "q" instead of P "q" or P {pname = "q"}
instance IsString Prop where
    fromString = P

instance Pretty Prop where
    pPrint = text . pname

instance HasFixity Prop where
    precedence (P _) = leafPrec

-- | An instance of IsPropositional.
data PFormula atom
    = F
    | T
    | Atom atom
    | Not (PFormula atom)
    | And (PFormula atom) (PFormula atom)
    | Or (PFormula atom) (PFormula atom)
    | Imp (PFormula atom) (PFormula atom)
    | Iff (PFormula atom) (PFormula atom)
    deriving (Eq, Ord, Read, Data, Typeable)

-- Build a Haskell expression for this formula
instance (IsPropositional (PFormula atom), Show atom) => Show (PFormula atom) where
    showsPrec p x = showPropositional Top p x -- . showString " :: PFormula Prop"

instance IsAtom atom => HasFixity (PFormula atom) where
    precedence = precedencePropositional
    associativity = associativityPropositional

instance IsAtom atom => IsFormula (PFormula atom) where
    type AtomOf (PFormula atom) = atom
    asBool T = Just True
    asBool F = Just False
    asBool _ = Nothing
    true = T
    false = F
    atomic = Atom
    overatoms = overatomsPropositional
    onatoms = onatomsPropositional

instance IsAtom atom => IsPropositional (PFormula atom) where
    (.|.) = Or
    (.&.) = And
    (.=>.) = Imp
    (.<=>.) = Iff
    foldPropositional' _ co ne tf at fm =
        case fm of
          Imp p q -> co p (:=>:) q
          Iff p q -> co p (:<=>:) q
          And p q -> co p (:&:) q
          Or p q -> co p (:|:) q
          _ -> foldLiteral' (error "IsPropositional PFormula") ne tf at fm
    foldCombination other dj cj imp iff fm =
        case fm of
          Or a b -> a `dj` b
          And a b -> a `cj` b
          Imp a b -> a `imp` b
          Iff a b -> a `iff` b
          _ -> other fm

instance IsAtom atom => IsLiteral (PFormula atom) where
    naiveNegate = Not
    foldNegation normal inverted (Not x) = foldNegation inverted normal x
    foldNegation normal _ x = normal x
    foldLiteral' ho ne tf at fm =
        case fm of
          T -> tf True
          F -> tf False
          Atom a -> at a
          Not l -> ne l
          _ -> ho fm

instance IsAtom atom => Pretty (PFormula atom) where
    pPrintPrec = prettyPropositional Top

-- | Class that indicates a formula type *only* supports Propositional
-- features - it has no way to represent quantifiers.
class IsPropositional formula => JustPropositional formula

instance IsAtom atom => JustPropositional (PFormula atom)

-- | Interpretation of formulas.
eval :: JustPropositional pf => pf -> (AtomOf pf -> Bool) -> Bool
eval fm v =
    foldPropositional co ne tf at fm
    where
      co p (:&:) q = (eval p v) && (eval q v)
      co p (:|:) q = (eval p v) || (eval q v)
      co p (:=>:) q = not (eval p v) || (eval q v)
      co p (:<=>:) q = (eval p v) == (eval q v)
      ne p = not (eval p v)
      tf = id
      at = v

-- | Recognizing tautologies.
tautology :: JustPropositional pf => pf -> Bool
tautology fm = onallvaluations (&&) (eval fm) (\_s -> False) (atoms fm)

-- | Related concepts.
unsatisfiable :: JustPropositional pf => pf -> Bool
unsatisfiable = tautology . (.~.)
satisfiable :: JustPropositional pf  => pf -> Bool
satisfiable = not . unsatisfiable

onallvaluations :: Ord atom => (r -> r -> r) -> ((atom -> Bool) -> r) -> (atom -> Bool) -> Set atom -> r
onallvaluations cmb subfn v ats =
    case minView ats of
      Nothing -> subfn v
      Just (p, ps) ->
          let v' t q = (if q == p then t else v q) in
          cmb (onallvaluations cmb subfn (v' False) ps) (onallvaluations cmb subfn (v' True) ps)

-- | Return the set of propositional variables in a formula.
atoms :: IsFormula formula => formula -> Set (AtomOf formula)
atoms fm = atom_union singleton fm

data TruthTable a = TruthTable [a] [TruthTableRow] deriving (Eq, Show)
type TruthTableRow = ([Bool], Bool)

-- | Code to print out truth tables.
truthTable :: JustPropositional pf => pf -> TruthTable (AtomOf pf)
truthTable fm =
    TruthTable atl (onallvaluations (<>) mkRow (const False) ats)
    where
      ats = atoms fm
      mkRow v = [(List.map v atl, eval fm v)]
      atl = Set.toAscList ats

instance Pretty atom => Pretty (TruthTable atom) where
    pPrint (TruthTable ats rows) = vcat (List.map (text . intercalate "|" . center) rows'')
        where
          center :: [String] -> [String]
          center cols = Prelude.map (uncurry center') (zip colWidths cols)
          center' :: Int -> String -> String
          center' width s = let (q, r) = divMod (width - length s) 2 in replicate q ' ' ++ s ++ replicate (q + r) ' '
          hdrs = List.map prettyShow ats ++ ["result"]
          rows'' = hdrs : List.map (uncurry replicate) (zip colWidths (repeat '-')) : rows'
          rows' :: [[String]]
          rows' = List.map (\(cols, result) -> List.map prettyShow (cols ++ [result])) rows
          cellWidths :: [[Int]]
          cellWidths = List.map (List.map length) (hdrs : rows')
          colWidths :: [Int]
          colWidths = List.map (foldl1 max) (transpose cellWidths)

transpose               :: [[a]] -> [[a]]
transpose []             = []
transpose ([]   : xss)   = transpose xss
transpose ((x:xs) : xss) = (x : [h | (h:_) <- xss]) : transpose (xs : [ t | (_:t) <- xss])

-- | Make sure the precedence and associativity implied by the Haskell
-- infix/infixl/infixr declarations matches the precedence and
-- associativity implied by the HasFixity instances.  It would be nice
-- to define one in terms of the other, but I don't know how to query
-- the precedence and associativity of an operator, and I don't know
-- how to (successfully) generate an infix/infixl/infixr declaration
-- using template haskell (the obvious thing didn't work:
--
--    $(pure [InfixD (Fixity quantPrec TH.InfixR) '(∀)])

-- | Tests precedence handling in pretty
-- printer.  1. Need to test: associativity of like operators
-- 2. precedence of every pair of adjacent operators 3. Stuff about
-- infix operators
test00 :: Test
test00 =
{-
    TestList [testPrecedence, testAssociativity]
    where
      testPrecedence :: Test
      testPrecedence =
          TestList (
-}
    TestList (List.map (\(input, expected) -> TestCase $ assertEqual "precedence" (text expected) (pPrint input))
                      [( p .&. (q .|. r)   , "p∧(q∨r)" ),
                       ( (p .&. q) .|. r   , "(p∧q)∨r" ),
                       ( p .&. q .|. r     , "(p∧q)∨r" ),
                       ( p .|. q .&. r     , "p∨(q∧r)" ),
                       ( p .&. q .&. r     , "p∧q∧r"   ),
                       ( p .|. q .|. r     , "p∨q∨r"   ),
                       ( (p .=>. q) .=>. r , "(p⇒q)⇒r" ),
                       ( p .=>. (q .=>. r) , "p⇒q⇒r"   ),
                       ( p .=>. q .=>. r   , "p⇒q⇒r"   )])
    where
      byPrec :: IsPropositional formula => [[(Rational, formula -> formula -> formula)]]
      byPrec = groupBy ((==) `on` fst) . sortBy (compare `on` fst) $ binops
      -- All the operators we will test, with the 'Precedence' value
      -- we assigned.  we need to make sure the 'Precedence' value
      -- matches the values in the infix/infixl/infixr declarations.
      binops :: IsPropositional formula => [(Rational, formula -> formula -> formula)]
      binops = ands ++ ors ++ imps ++ iffs
          where
            ands :: IsPropositional formula => [(Rational, formula -> formula -> formula)]
            ands = List.map (andPrec,) [(.&.), (), (·)]
            ors :: IsPropositional formula => [(Rational, formula -> formula -> formula)]
            ors = List.map (orPrec,) [(.|.), ()]
            imps :: IsPropositional formula => [(Rational, formula -> formula -> formula)]
            imps = List.map (impPrec,) [(.=>.), (), (==>), (), ()]
            iffs :: IsPropositional formula => [(Rational, formula -> formula -> formula)]
            iffs = List.map (iffPrec,) [(.<=>.), (<=>), (), ()]
      preops :: IsPropositional formula => [(Rational, formula -> formula)]
      preops = nots
          where
            nots :: IsPropositional formula => [(Rational, formula -> formula)]
            nots = List.map (notPrec,) [(.~.), (¬)]
      (p, q, r) = (Atom (P "p"), Atom (P "q"), Atom (P "r"))
      -- What about these?
      -- (∴)


{-
test00 = TestCase $ assertEqual "parenthesization" expected (List.map prettyShow input)
          (input, expected) = unzip [( p .&. (q .|. r)   , "p∧(q∨r)" ),
                                     ( (p .&. q) .|. r   , "(p∧q)∨r" ),
                                     ( p .&. q .|. r     , "(p∧q)∨r" ),
                                     ( p .|. q .&. r     , "p∨(q∧r)" ),
                                     ( p .&. q .&. r     , "p∧q∧r"   ),
                                     ( (p .=>. q) .=>. r , "(p⇒q)⇒r" ),
                                     ( p .=>. (q .=>. r) , "p⇒q⇒r"   ),
                                     ( p .=>. q .=>. r   , "p⇒q⇒r"   )]
-}

test01 :: Test
test01 =
    let fm = atomic "p" .=>. atomic "q" .<=>. (atomic "r" .&. atomic "s") .|. (atomic "t" .<=>. ((.~.) ((.~.) (atomic "u"))) .&. atomic "v") :: PFormula Prop
        input = (prettyShow fm, show fm)
        expected = (-- Pretty printed
                    "p⇒q⇔(r∧s)∨(t⇔u∧v)",
                    -- Haskell expression
                    "atomic \"p\" .=>. atomic \"q\" .<=>. (atomic \"r\" .&. atomic \"s\") .|. (atomic \"t\" .<=>. atomic \"u\" .&. atomic \"v\")"
                    ) in
    TestCase $ assertEqual "Build Formula 1" expected input

test02 :: Test
test02 = TestCase $ assertEqual "Build Formula 2" expected input
    where input = (Atom "fm" .&. Atom "fm") :: PFormula Prop
          expected = (And (Atom "fm") (Atom "fm"))

test03 :: Test
test03 = TestCase $ assertEqual "Build Formula 3"
                                (Atom "fm" .|. Atom "fm" .&. Atom "fm" :: PFormula Prop)
                                (Or (Atom "fm") (And (Atom "fm") (Atom "fm")))

-- Example of use.

test04 :: Test
test04 = TestCase $ assertEqual "fixity tests" expected input
    where (input, expected) = unzip (List.map (\ (fm, flag) -> (eval fm v0, flag)) pairs)
          v0 x = error $ "v0: " ++ show x
          pairs :: [(PFormula Prop, Bool)]
          pairs =
              [ ( true .&. false .=>. false .&. true,  True)
              , ( true .&. true  .=>. true  .&. false, False)
              , (   false   true   true,             True)  -- "∧ binds more tightly than ∨"
              , (  (false   true)  true,             True)
              , (   false  (true   true),            False)
              , (  (¬) true  true,                    True)  -- "¬ binds more tightly than ∨"
              , (  (¬) (true  true),                  False)
              ]

-- Example.

test06 :: Test
test06 = TestCase $ assertEqual "atoms test" (atoms $ p .&. q .|. s .=>. ((.~.) p) .|. (r .<=>. s)) (Set.fromList [P "p",P "q",P "r",P "s"])
    where (p, q, r, s) = (Atom (P "p"), Atom (P "q"), Atom (P "r"), Atom (P "s"))

-- Example.

test07 :: Test
test07 = TestCase $ assertEqual "truth table 1 (p. 36)" expected input
    where input = (truthTable $ p .&. q .=>. q .&. r)
          expected =
              (TruthTable
               [P "p", P "q", P "r"]
               [([False,False,False],True),
               ([False,False,True],True),
               ([False,True,False],True),
               ([False,True,True],True),
               ([True,False,False],True),
               ([True,False,True],True),
               ([True,True,False],False),
               ([True,True,True],True)])
          (p, q, r) = (Atom (P "p"), Atom (P "q"), Atom (P "r"))

-- Additional examples illustrating formula classes.

test08 :: Test
test08 = TestCase $
    assertEqual "truth table 2 (p. 39)"
                (truthTable $  ((p .=>. q) .=>. p) .=>. p)
                (TruthTable
                 [P "p", P "q"]
                 [([False,False],True),
                  ([False,True],True),
                  ([True,False],True),
                  ([True,True],True)])
        where (p, q) = (Atom (P "p"), Atom (P "q"))

test09 :: Test
test09 = TestCase $
    assertEqual "truth table 3 (p. 40)" expected input
        where input = (truthTable $ p .&. ((.~.) p))
              expected = (TruthTable
                          [P "p"]
                          [([False],False),
                          ([True],False)])
              p = Atom (P "p")

-- Examples.

test10 :: Test
test10 = TestCase $ assertEqual "tautology 1 (p. 41)" True (tautology $ p .|. ((.~.) p)) where p = Atom (P "p")
test11 :: Test
test11 = TestCase $ assertEqual "tautology 2 (p. 41)" False (tautology $ p .|. q .=>. p) where (p, q) = (Atom (P "p"), Atom (P "q"))
test12 :: Test
test12 = TestCase $ assertEqual "tautology 3 (p. 41)" False (tautology $ p .|. q .=>. q .|. (p .<=>. q)) where (p, q) = (Atom (P "p"), Atom (P "q"))
test13 :: Test
test13 = TestCase $ assertEqual "tautology 4 (p. 41)" True (tautology $ (p .|. q) .&. ((.~.)(p .&. q)) .=>. ((.~.)p .<=>. q)) where (p, q) = (Atom (P "p"), Atom (P "q"))

-- | Substitution operation.
psubst :: JustPropositional formula => Map (AtomOf formula) formula -> formula -> formula
psubst subfn fm =
    foldPropositional co ne tf at fm
    where
      co p op q = binop (psubst subfn p) op (psubst subfn q)
      ne p = (.~.) (psubst subfn p)
      tf = fromBool
      at p = fromMaybe (atomic p) (fpf subfn p)

-- Example
test14 :: Test
test14 =
    TestCase $ assertEqual "pSubst (p. 41)" expected input
        where expected = (p .&. q) .&. q .&. (p .&. q) .&. q
              input = psubst ((P "p") |=> (p .&. q)) (p .&. q .&. p .&. q)
              (p, q) = (Atom (P "p"), Atom (P "q"))

-- Surprising tautologies including Dijkstra's "Golden rule".

test15 :: Test
test15 = TestCase $ assertEqual "tautology 5 (p. 43)" expected input
    where input = tautology $ (p .=>. q) .|. (q .=>. p)
          expected = True
          (p, q) = (Atom (P "p"), Atom (P "q"))
test16 :: Test
test16 = TestCase $ assertEqual "tautology 6 (p. 45)" expected input
    where input = tautology $ p .|. (q .<=>. r) .<=>. (p .|. q .<=>. p .|. r)
          expected = True
          (p, q, r) = (Atom (P "p"), Atom (P "q"), Atom (P "r"))
test17 :: Test
test17 = TestCase $ assertEqual "Dijkstra's Golden Rule (p. 45)" expected input
    where input = tautology $ p .&. q .<=>. ((p .<=>. q) .<=>. p .|. q)
          expected = True
          (p, q) = (Atom (P "p"), Atom (P "q"))
test18 :: Test
test18 = TestCase $ assertEqual "Contraposition 1 (p. 46)" expected input
    where input = tautology $ (p .=>. q) .<=>. (((.~.)q) .=>. ((.~.)p))
          expected = True
          (p, q) = (Atom (P "p"), Atom (P "q"))
test19 :: Test
test19 = TestCase $ assertEqual "Contraposition 2 (p. 46)" expected input
    where input = tautology $ (p .=>. ((.~.)q)) .<=>. (q .=>. ((.~.)p))
          expected = True
          (p, q) = (Atom (P "p"), Atom (P "q"))
test20 :: Test
test20 = TestCase $ assertEqual "Contraposition 3 (p. 46)" expected input
    where input = tautology $ (p .=>. q) .<=>. (q .=>. p)
          expected = False
          (p, q) = (Atom (P "p"), Atom (P "q"))

-- Some logical equivalences allowing elimination of connectives.

test21 :: Test
test21 = TestCase $ assertEqual "Equivalences (p. 47)" expected input
    where input =
              List.map tautology
              [ true .<=>. false .=>. false
              , ((.~.)p) .<=>. p .=>. false
              , p .&. q .<=>. (p .=>. q .=>. false) .=>. false
              , p .|. q .<=>. (p .=>. false) .=>. q
              , (p .<=>. q) .<=>. ((p .=>. q) .=>. (q .=>. p) .=>. false) .=>. false ]
          expected = [True, True, True, True, True]
          (p, q) = (Atom (P "p"), Atom (P "q"))

-- | Dualization.
dual :: JustPropositional pf => pf -> pf
dual fm =
    foldPropositional co ne tf (\_ -> fm) fm
    where
      tf True = false
      tf False = true
      ne p = (.~.) (dual p)
      co p (:&:) q = dual p .|. dual q
      co p (:|:) q = dual p .&. dual q
      co _ _ _ = error "Formula involves connectives .=>. or .<=>."

-- Example.
test22 :: Test
test22 = TestCase $ assertEqual "Dual (p. 49)" expected input
    where input = dual (Atom (P "p") .|. ((.~.) (Atom (P "p"))))
          expected = And (Atom (P {pname = "p"})) (Not (Atom (P {pname = "p"})))

-- | Routine simplification.
psimplify :: IsPropositional formula => formula -> formula
psimplify fm =
    foldPropositional' ho co ne tf at fm
    where
      ho _ = fm
      ne p = psimplify1 ((.~.) (psimplify p))
      co p (:&:) q = psimplify1 ((psimplify p) .&. (psimplify q))
      co p (:|:) q = psimplify1 ((psimplify p) .|. (psimplify q))
      co p (:=>:) q = psimplify1 ((psimplify p) .=>. (psimplify q))
      co p (:<=>:) q = psimplify1 ((psimplify p) .<=>. (psimplify q))
      tf _ = fm
      at _ = fm

psimplify1 :: IsPropositional formula => formula -> formula
psimplify1 fm =
    foldPropositional' (\_ -> fm) simplifyCombine simplifyNegate (\_ -> fm) (\_ -> fm) fm
    where
      simplifyNegate p = foldPropositional' (\_ -> fm) simplifyNotCombine simplifyNotNegate (fromBool . not) simplifyNotAtom p
      simplifyCombine l op r =
          case (asBool l, op , asBool r) of
            (Just True,  (:&:), _)            -> r
            (Just False, (:&:), _)            -> fromBool False
            (_,          (:&:), Just True)    -> l
            (_,          (:&:), Just False)   -> fromBool False
            (Just True,  (:|:), _)            -> fromBool True
            (Just False, (:|:), _)            -> r
            (_,          (:|:), Just True)    -> fromBool True
            (_,          (:|:), Just False)   -> l
            (Just True,  (:=>:), _)           -> r
            (Just False, (:=>:), _)           -> fromBool True
            (_,          (:=>:), Just True)   -> fromBool True
            (_,          (:=>:), Just False)  -> (.~.) l
            (Just False, (:<=>:), Just False) -> fromBool True
            (Just True,  (:<=>:), _)          -> r
            (Just False, (:<=>:), _)          -> (.~.) r
            (_,          (:<=>:), Just True)  -> l
            (_,          (:<=>:), Just False) -> (.~.) l
            _                                 -> fm
      simplifyNotNegate f = f
      simplifyNotCombine _ _ _ = fm
      simplifyNotAtom x = (.~.) (atomic x)

-- Example.
test23 :: Test
test23 = TestCase $ assertEqual "psimplify 1 (p. 50)" expected input
    where input = psimplify $ (true .=>. (x .<=>. false)) .=>. ((.~.) (y .|. false .&. z))
          expected = ((.~.) x) .=>. ((.~.) y)
          x = Atom (P "x")
          y = Atom (P "y")
          z = Atom (P "z")

test24 :: Test
test24 = TestCase $ assertEqual "psimplify 2 (p. 51)" expected input
    where input = psimplify $ ((x .=>. y) .=>. true) .|. (.~.) false
          expected = true
          x = Atom (P "x")
          y = Atom (P "y")

-- | Negation normal form.

nnf :: JustPropositional pf => pf -> pf
nnf = nnf1 . psimplify

nnf1 :: JustPropositional pf => pf -> pf
nnf1 fm = foldPropositional nnfCombine nnfNegate fromBool (\_ -> fm) fm
    where
      -- nnfCombine :: (IsPropositional formula atom) => formula -> Combination formula -> formula
      nnfNegate p = foldPropositional nnfNotCombine nnfNotNegate (fromBool . not) (\_ -> fm) p
      nnfCombine p (:=>:) q = nnf1 ((.~.) p) .|. (nnf1 q)
      nnfCombine p (:<=>:) q =  (nnf1 p .&. nnf1 q) .|. (nnf1 ((.~.) p) .&. nnf1 ((.~.) q))
      nnfCombine p (:&:) q = nnf1 p .&. nnf1 q
      nnfCombine p (:|:) q = nnf1 p .|. nnf1 q

      -- nnfNotCombine :: (IsPropositional formula atom) => Combination formula -> formula
      nnfNotNegate p = nnf1 p
      nnfNotCombine p (:&:) q = nnf1 ((.~.) p) .|. nnf1 ((.~.) q)
      nnfNotCombine p (:|:) q = nnf1 ((.~.) p) .&. nnf1 ((.~.) q)
      nnfNotCombine p (:=>:) q = nnf1 p .&. nnf1 ((.~.) q)
      nnfNotCombine p (:<=>:) q = (nnf1 p .&. nnf1 ((.~.) q)) .|. nnf1 ((.~.) p) .&. nnf1 q

-- Example of NNF function in action.

test25 :: Test
test25 = TestCase $ assertEqual "nnf 1 (p. 53)" expected input
    where input = nnf $ (p .<=>. q) .<=>. ((.~.)(r .=>. s))
          expected = Or (And (Or (And p q) (And (Not p) (Not q)))
                        (And r (Not s)))
                        (And (Or (And p (Not q)) (And (Not p) q))
                             (Or (Not r) s))
          p = Atom (P "p")
          q = Atom (P "q")
          r = Atom (P "r")
          s = Atom (P "s")

test26 :: Test
test26 = TestCase $ assertEqual "nnf 1 (p. 53)" expected input
    where input = tautology (Iff fm fm')
          expected = True
          fm' = nnf fm
          fm = (p .<=>. q) .<=>. ((.~.)(r .=>. s))
          p = Atom (P "p")
          q = Atom (P "q")
          r = Atom (P "r")
          s = Atom (P "s")

nenf :: IsPropositional formula => formula -> formula
nenf = nenf' . psimplify

-- | Simple negation-pushing when we don't care to distinguish occurrences.
nenf' :: IsPropositional formula => formula -> formula
nenf' fm =
    foldPropositional' (\_ -> fm) co ne (\_ -> fm) (\_ -> fm) fm
    where
      ne p = foldPropositional' (\_ -> fm) co' ne' (\_ -> fm) (\_ -> fm) p
      co p (:&:) q = nenf' p .&. nenf' q
      co p (:|:) q = nenf' p .|. nenf' q
      co p (:=>:) q = nenf' ((.~.) p) .|. nenf' q
      co p (:<=>:) q = nenf' p .<=>. nenf' q
      ne' p = p
      co' p (:&:) q = nenf' ((.~.) p) .|. nenf' ((.~.) q)
      co' p (:|:) q = nenf' ((.~.) p) .&. nenf' ((.~.) q)
      co' p (:=>:) q = nenf' p .&. nenf' ((.~.) q)
      co' p (:<=>:) q = nenf' p .<=>. nenf' ((.~.) q) -- really?  how is this asymmetrical?

-- Some tautologies remarked on.

test27 :: Test
test27 = TestCase $ assertEqual "tautology 1 (p. 53)" expected input
    where input = tautology $ (p .=>. p') .&. (q .=>. q') .=>. (p .&. q .=>. p' .&. q')
          expected = True
          p = Atom (P "p")
          q = Atom (P "q")
          p' = Atom (P "p'")
          q' = Atom (P "q'")
test28 :: Test
test28 = TestCase $ assertEqual "nnf 1 (p. 53)" expected input
    where input = tautology $ (p .=>. p') .&. (q .=>. q') .=>. (p .|. q .=>. p' .|. q')
          expected = True
          p = Atom (P "p")
          q = Atom (P "q")
          p' = Atom (P "p'")
          q' = Atom (P "q'")

dnfSet :: (JustPropositional pf, Ord pf) => pf -> pf
dnfSet fm =
    list_disj (List.map (mk_lits (Set.map atomic pvs)) satvals)
    where
      satvals = allsatvaluations (eval fm) (\_s -> False) pvs
      pvs = atoms fm

mk_lits :: (JustPropositional pf, Ord pf) => Set pf -> (AtomOf pf -> Bool) -> pf
mk_lits pvs v = list_conj (Set.map (\ p -> if eval p v then p else (.~.) p) pvs)

allsatvaluations :: Ord atom => ((atom -> Bool) -> Bool) -> (atom -> Bool) -> Set atom -> [atom -> Bool]
allsatvaluations subfn v pvs =
    case Set.minView pvs of
      Nothing -> if subfn v then [v] else []
      Just (p, ps) -> (allsatvaluations subfn (\a -> if a == p then False else v a) ps) ++
                      (allsatvaluations subfn (\a -> if a == p then True else v a) ps)

-- | Disjunctive normal form (DNF) via truth tables.
list_conj :: (Foldable t, IsFormula formula, IsPropositional formula) => t formula -> formula
list_conj l | null l = true
list_conj l = foldl1 (.&.) l

list_disj :: (Foldable t, IsFormula formula, IsPropositional formula) => t formula -> formula
list_disj l | null l = false
list_disj l = foldl1 (.|.) l

-- This is only used in the test below, its easier to match lists than sets.
dnfList :: JustPropositional pf => pf -> pf
dnfList fm =
    list_disj (List.map (mk_lits' (List.map atomic (Set.toAscList pvs))) satvals)
     where
       satvals = allsatvaluations (eval fm) (\_s -> False) pvs
       pvs = atoms fm
       mk_lits' :: JustPropositional pf => [pf] -> (AtomOf pf -> Bool) -> pf
       mk_lits' pvs' v = list_conj (List.map (\ p -> if eval p v then p else (.~.) p) pvs')

-- Examples.

test29 :: Test
test29 = TestCase $ assertEqual "dnf 1 (p. 56)" expected input
    where input = (dnfList fm, truthTable fm)
          expected = ((((((.~.) p) .&. q) .&. r) .|. ((p .&. ((.~.) q)) .&. ((.~.) r))) .|. ((p .&. q) .&. ((.~.) r)),
                      TruthTable
                      [P "p", P "q", P "r"]
                      [([False,False,False],False),
                       ([False,False,True],False),
                       ([False,True,False],False),
                       ([False,True,True],True),
                       ([True,False,False],True),
                       ([True,False,True],False),
                       ([True,True,False],True),
                       ([True,True,True],False)])
          fm = (p .|. q .&. r) .&. (((.~.)p) .|. ((.~.)r))
          (p, q, r) = (Atom (P "p"), Atom (P "q"), Atom (P "r"))

test30 :: Test
test30 = TestCase $ assertEqual "dnf 2 (p. 56)" expected input
    where input = dnfList (p .&. q .&. r .&. s .&. t .&. u .|. u .&. v :: PFormula Prop)
          expected = (((((((((((((((((((((((((((((((((((((((.~.) p) .&. ((.~.) q)) .&. ((.~.) r)) .&. ((.~.) s)) .&. ((.~.) t)) .&. u) .&. v) .|.
                                                    ((((((((.~.) p) .&. ((.~.) q)) .&. ((.~.) r)) .&. ((.~.) s)) .&. t) .&. u) .&. v)) .|.
                                                   ((((((((.~.) p) .&. ((.~.) q)) .&. ((.~.) r)) .&. s) .&. ((.~.) t)) .&. u) .&. v)) .|.
                                                  ((((((((.~.) p) .&. ((.~.) q)) .&. ((.~.) r)) .&. s) .&. t) .&. u) .&. v)) .|.
                                                 ((((((((.~.) p) .&. ((.~.) q)) .&. r) .&. ((.~.) s)) .&. ((.~.) t)) .&. u) .&. v)) .|.
                                                ((((((((.~.) p) .&. ((.~.) q)) .&. r) .&. ((.~.) s)) .&. t) .&. u) .&. v)) .|.
                                               ((((((((.~.) p) .&. ((.~.) q)) .&. r) .&. s) .&. ((.~.) t)) .&. u) .&. v)) .|.
                                              ((((((((.~.) p) .&. ((.~.) q)) .&. r) .&. s) .&. t) .&. u) .&. v)) .|.
                                             ((((((((.~.) p) .&. q) .&. ((.~.) r)) .&. ((.~.) s)) .&. ((.~.) t)) .&. u) .&. v)) .|.
                                            ((((((((.~.) p) .&. q) .&. ((.~.) r)) .&. ((.~.) s)) .&. t) .&. u) .&. v)) .|.
                                           ((((((((.~.) p) .&. q) .&. ((.~.) r)) .&. s) .&. ((.~.) t)) .&. u) .&. v)) .|.
                                          ((((((((.~.) p) .&. q) .&. ((.~.) r)) .&. s) .&. t) .&. u) .&. v)) .|.
                                         ((((((((.~.) p) .&. q) .&. r) .&. ((.~.) s)) .&. ((.~.) t)) .&. u) .&. v)) .|.
                                        ((((((((.~.) p) .&. q) .&. r) .&. ((.~.) s)) .&. t) .&. u) .&. v)) .|.
                                       ((((((((.~.) p) .&. q) .&. r) .&. s) .&. ((.~.) t)) .&. u) .&. v)) .|.
                                      ((((((((.~.) p) .&. q) .&. r) .&. s) .&. t) .&. u) .&. v)) .|.
                                     ((((((p .&. ((.~.) q)) .&. ((.~.) r)) .&. ((.~.) s)) .&. ((.~.) t)) .&. u) .&. v)) .|.
                                    ((((((p .&. ((.~.) q)) .&. ((.~.) r)) .&. ((.~.) s)) .&. t) .&. u) .&. v)) .|.
                                   ((((((p .&. ((.~.) q)) .&. ((.~.) r)) .&. s) .&. ((.~.) t)) .&. u) .&. v)) .|.
                                  ((((((p .&. ((.~.) q)) .&. ((.~.) r)) .&. s) .&. t) .&. u) .&. v)) .|.
                                 ((((((p .&. ((.~.) q)) .&. r) .&. ((.~.) s)) .&. ((.~.) t)) .&. u) .&. v)) .|.
                                ((((((p .&. ((.~.) q)) .&. r) .&. ((.~.) s)) .&. t) .&. u) .&. v)) .|.
                               ((((((p .&. ((.~.) q)) .&. r) .&. s) .&. ((.~.) t)) .&. u) .&. v)) .|.
                              ((((((p .&. ((.~.) q)) .&. r) .&. s) .&. t) .&. u) .&. v)) .|.
                             ((((((p .&. q) .&. ((.~.) r)) .&. ((.~.) s)) .&. ((.~.) t)) .&. u) .&. v)) .|.
                            ((((((p .&. q) .&. ((.~.) r)) .&. ((.~.) s)) .&. t) .&. u) .&. v)) .|.
                           ((((((p .&. q) .&. ((.~.) r)) .&. s) .&. ((.~.) t)) .&. u) .&. v)) .|.
                          ((((((p .&. q) .&. ((.~.) r)) .&. s) .&. t) .&. u) .&. v)) .|.
                         ((((((p .&. q) .&. r) .&. ((.~.) s)) .&. ((.~.) t)) .&. u) .&. v)) .|.
                        ((((((p .&. q) .&. r) .&. ((.~.) s)) .&. t) .&. u) .&. v)) .|.
                       ((((((p .&. q) .&. r) .&. s) .&. ((.~.) t)) .&. u) .&. v)) .|.
                      ((((((p .&. q) .&. r) .&. s) .&. t) .&. u) .&. ((.~.) v))) .|.
                     ((((((p .&. q) .&. r) .&. s) .&. t) .&. u) .&. v)
          [p, q, r, s, t, u, v] = List.map (Atom . P) ["p", "q", "r", "s", "t", "u", "v"]

-- | DNF via distribution.
distrib1 :: IsPropositional formula => formula -> formula
distrib1 fm =
    foldCombination (\_ -> fm) (\_ _ -> fm) lhs (\_ _ -> fm) (\_ _ -> fm) fm
    where
      -- p & (q | r) -> (p & q) | (p & r)
      lhs p qr = foldCombination (\_ -> rhs p qr)
                                 (\q r -> distrib1 (p .&. q) .|. distrib1 (p .&. r))
                                 (\_ _ -> rhs p qr)
                                 (\_ _ -> rhs p qr)
                                 (\_ _ -> rhs p qr)
                                 qr
      -- (p | q) & r -> (p & r) | (q & r)
      rhs pq r = foldCombination (\_ -> fm)
                                 (\p q -> distrib1 (p .&. r) .|. distrib1 (q .&. r))
                                 (\_ _ -> fm)
                                 (\_ _ -> fm)
                                 (\_ _ -> fm)
                                 pq
{-
distrib1 :: Formula atom -> Formula atom
distrib1 fm =
  case fm of
    And p (Or q r) -> Or (distrib1 (And p q)) (distrib1 (And p r))
    And (Or p q) r -> Or (distrib1 (And p r)) (distrib1 (And q r))
    _ -> fm
-}

rawdnf :: IsPropositional formula => formula -> formula
rawdnf fm =
    foldPropositional' (\_ -> fm) co (\_ -> fm) (\_ -> fm) (\_ -> fm) fm
    where
      co p (:&:) q = distrib1 (rawdnf p .&. rawdnf q)
      co p (:|:) q = (rawdnf p .|. rawdnf q)
      co _ _ _ = fm
{-
rawdnf :: Ord atom => Formula atom -> Formula atom
rawdnf fm =
  case fm of
    And p q -> distrib1 (And (rawdnf p) (rawdnf q))
    Or p q -> Or (rawdnf p) (rawdnf q)
    _ -> fm
-}

-- Example.

test31 :: Test
test31 = TestCase $ assertEqual "rawdnf (p. 58)" (prettyShow expected) (prettyShow input)
    where input :: PFormula Prop
          input = rawdnf $ (p .|. q .&. r) .&. (((.~.)p) .|. ((.~.)r))
          expected :: PFormula Prop
          expected = ((atomic (P "p")) .&. ((.~.)(atomic (P "p"))) .|.
                      ((atomic (P "q")) .&. (atomic (P "r"))) .&. ((.~.)(atomic (P "p")))) .|.
                     ((atomic (P "p")) .&. ((.~.)(atomic (P "r"))) .|.
                      ((atomic (P "q")) .&. (atomic (P "r"))) .&. ((.~.)(atomic (P "r"))))
          (p, q, r) = (Atom (P "p"), Atom (P "q"), Atom (P "r"))

purednf :: (JustPropositional pf,
            IsLiteral lit, JustLiteral lit, Ord lit) => (AtomOf pf -> AtomOf lit) -> pf -> Set (Set lit)
purednf ca fm =
    foldPropositional co (\_ -> l2f fm) (\_ -> l2f fm) (\_ -> l2f fm) fm
    where
      l2f f = singleton . singleton . convertToLiteral (error $ "purednf failure: " ++ prettyShow f) ca $ f
      co p (:&:) q = distrib (purednf ca p) (purednf ca q)
      co p (:|:) q = union (purednf ca p) (purednf ca q)
      co _ _ _ = l2f fm

-- Example.

test32 :: Test
test32 = TestCase $ assertEqual "purednf (p. 58)" expected (purednf id fm)
    where fm :: PFormula Prop
          fm = (p .|. q .&. r) .&. (((.~.)p) .|. ((.~.)r))
          expected :: Set (Set (LFormula Prop))
          expected = Set.map (Set.map (convertToLiteral (error "test32") id)) $
                     Set.fromList [Set.fromList [p, (.~.) p],
                                   Set.fromList [p, (.~.) r],
                                   Set.fromList [q, r, (.~.) p],
                                   Set.fromList [q, r, (.~.) r]]
          p = atomic (P "p")
          q = atomic (P "q")
          r = atomic (P "r")

-- | Filtering out trivial disjuncts (in this guise, contradictory).
trivial :: (Ord lit, IsLiteral lit) => Set lit -> Bool
trivial lits =
    let (pos, neg) = Set.partition positive lits in
    (not . null . Set.intersection neg . Set.map (.~.)) pos

-- Example.
test33 :: Test
test33 = TestCase $ assertEqual "trivial" expected (Set.filter (not . trivial) (purednf id fm))
    where expected :: Set (Set (LFormula Prop))
          expected = Set.map (Set.map (convertToLiteral (error "test32") id)) $
                     Set.fromList [Set.fromList [p,(.~.) r],
                                   Set.fromList [q,r,(.~.) p]]
          fm :: PFormula Prop
          fm = (p .|. q .&. r) .&. (((.~.)p) .|. ((.~.)r))
          p = atomic (P "p") :: PFormula Prop
          q = atomic (P "q") :: PFormula Prop
          r = atomic (P "r") :: PFormula Prop

-- | With subsumption checking, done very naively (quadratic).
simpdnf :: (JustPropositional pf,
            IsLiteral lit, JustLiteral lit, Ord lit
           ) => (AtomOf pf -> AtomOf lit) -> pf -> Set (Set lit)
simpdnf ca fm =
    foldPropositional (\_ _ _ -> go) (\_ -> go) tf (\_ -> go) fm
    where
      tf False = Set.empty
      tf True = singleton Set.empty
      go = let djs = Set.filter (not . trivial) (purednf ca (nnf fm)) in
           Set.filter (\d -> not (setAny (\d' -> Set.isProperSubsetOf d' d) djs)) djs

-- | Mapping back to a formula.
dnf :: forall pf. (JustPropositional pf, Ord pf) => pf -> pf
dnf fm = (list_disj . Set.toAscList . Set.map list_conj .
          Set.map (Set.map (convertLiteral id :: LFormula (AtomOf pf) -> pf)) . simpdnf id) fm

-- Example. (p. 56)
test34 :: Test
test34 = TestCase $ assertEqual "dnf (p. 56)" expected input
    where input = (prettyShow (dnf fm), tautology (Iff fm (dnf fm)))
          expected = ("(p∧¬r)∨(q∧r∧¬p)",True)
          fm = let (p, q, r) = (Atom (P "p"), Atom (P "q"), Atom (P "r")) in
               (p .|. q .&. r) .&. (((.~.)p) .|. ((.~.)r))

-- | Conjunctive normal form (CNF) by essentially the same code. (p. 60)
purecnf :: (JustPropositional pf, JustLiteral lit, Ord lit) =>
           (AtomOf pf -> AtomOf lit) -> pf -> Set (Set lit)
purecnf ca fm = Set.map (Set.map negate) (purednf ca (nnf ((.~.) fm)))

simpcnf :: (JustPropositional pf, JustLiteral lit, Ord lit) =>
           (AtomOf pf -> AtomOf lit) -> pf -> Set (Set lit)
simpcnf ca fm =
    foldPropositional (\_ _ _ -> go) (\_ -> go) tf (\_ -> go) fm
    where
      tf False = Set.empty
      tf True = singleton Set.empty
      go = let cjs = Set.filter (not . trivial) (purecnf ca fm) in
           Set.filter (\c -> not (setAny (\c' -> Set.isProperSubsetOf c' c) cjs)) cjs

cnf_ :: (IsPropositional pf, Ord pf, JustLiteral lit) => (AtomOf lit -> AtomOf pf) -> Set (Set lit) -> pf
cnf_ ca = list_conj . Set.map (list_disj . Set.map (convertLiteral ca))

cnf' :: forall pf. (JustPropositional pf, Ord pf) => pf -> pf
cnf' fm = (list_conj . Set.map list_disj . Set.map (Set.map (convertLiteral id :: LFormula (AtomOf pf) -> pf)) . simpcnf id) fm

-- Example. (p. 61)
test35 :: Test
test35 = TestCase $ assertEqual "cnf (p. 61)" expected input
    where input = (prettyShow (cnf' fm), tautology (Iff fm (cnf' fm)))
          expected = ("(p∨q)∧(p∨r)∧(¬p∨¬r)", True)
          fm = (p .|. q .&. r) .&. (((.~.)p) .|. ((.~.)r))
          [p, q, r] = [Atom (P "p"), Atom (P "q"), Atom (P "r")]

testProp :: Test
testProp = TestLabel "Prop" $
           TestList [test00, test01, test02, test03, test04, {-test05,-}
                     test06, test07, test08, test09, test10,
                     test11, test12, test13, test14, test15,
                     test16, test17, test18, test19, test20,
                     test21, test22, test23, test24, test25,
                     test26, test27, test28, test29, test30,
                     test31, test32, test33, test34, test35]