{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Algebra.Boolean
  ( BooleanAlgebra
  , (==>)
  , not
  , iff
  , iff'
    -- * Adjunction between Boolean and Heyting algebras
  , Boolean
  , runBoolean
  , boolean

    -- * Properties
    -- $properties
  , prop_not
  , prop_BooleanAlgebra
  ) where

import Prelude hiding (not)

import Control.Applicative    (Const (..))
import Data.Data              (Data, Typeable)
import Data.Functor.Identity  (Identity (..))
import Data.Proxy             (Proxy (..))
import Data.Semigroup         (All (..), Any (..), Endo (..))
import Data.Tagged            (Tagged (..))
import Data.Universe.Class    (Finite)
import qualified Data.Set as S
import GHC.Generics          (Generic)
#ifdef EXPORT_PROPERTIES
import Test.QuickCheck hiding ((==>))
#endif

import Algebra.Lattice ( Lattice
                       , BoundedLattice
                       , JoinSemiLattice (..)
                       , BoundedJoinSemiLattice
                       , MeetSemiLattice (..)
                       , BoundedMeetSemiLattice
                       , bottom
                       , top
                       )

import Algebra.Heyting ( HeytingAlgebra (..)
                       , iff
                       , iff'
                       , not
                       , toBoolean
                       , prop_HeytingAlgebra
                       )

-- |
-- Boolean algebra is a Heyting algebra which negation satisfies the law of
-- excluded middle, i.e. either of the following:
--
-- prop> not . not == not
--
-- or
--
-- prop> x ∨ not x == top
--
-- Another characterisation of Boolean algebras is as
-- [complemented](https://en.wikipedia.org/wiki/Complemented_lattice)
-- [distributive lattices](https://ncatlab.org/nlab/show/distributive+lattice)
-- where the complement satisfies the following three properties:
--
-- prop> (not a) ∧ a == bottom and (not a) ∨ a == top -- excluded middle law
-- prop> not (not a) == a                             -- involution law
-- prop> a ≤ b ⇒ not b ≤ not a                        -- order-reversing
class HeytingAlgebra a => BooleanAlgebra a

-- |
-- @'Boolean'@ is the left adjoint functor from the category of Heyting algebras
-- to the category of Boolean algebras; its right adjoint is the inclusion.
newtype Boolean a = Boolean
    { runBoolean :: a -- ^ extract value from @'Boolean'@
    }
  deriving
    ( JoinSemiLattice, BoundedJoinSemiLattice, MeetSemiLattice
    , BoundedMeetSemiLattice, Lattice, BoundedLattice, HeytingAlgebra
    , Eq, Ord, Read, Show, Bounded, Typeable, Data, Generic
    )

instance HeytingAlgebra a => BooleanAlgebra (Boolean a)

-- TODO: move to tests
instance (Arbitrary a, HeytingAlgebra a) => Arbitrary (Boolean a) where
  arbitrary = boolean <$> arbitrary
  shrink (Boolean a) = [ boolean a' | a' <- shrink a ]

-- |
-- Smart constructro of the @'Boolean'@ type.
boolean :: HeytingAlgebra a => a -> Boolean a
boolean = Boolean . toBoolean

--
-- Instances
--

instance BooleanAlgebra Bool

instance BooleanAlgebra All

instance BooleanAlgebra Any

instance BooleanAlgebra ()

instance BooleanAlgebra (Proxy a)

instance BooleanAlgebra a => BooleanAlgebra (Tagged t a)

instance BooleanAlgebra b => BooleanAlgebra (a -> b)

instance BooleanAlgebra a => BooleanAlgebra (Identity a)

instance BooleanAlgebra a => BooleanAlgebra (Const a b)

instance BooleanAlgebra a => BooleanAlgebra (Endo a)

instance (BooleanAlgebra a, BooleanAlgebra b) => BooleanAlgebra (a, b)

--
-- containers
--

instance (Ord a, Finite a) => BooleanAlgebra (S.Set a)

-- 
-- $properties
--
-- /Properties are exported only if @export-properties@ cabal flag is defined./
#ifdef EXPORT_PROPERTIES

-- |
-- Test that @'not'@ satisfies Boolean algebra axioms.
prop_not :: (HeytingAlgebra a, Eq a, Show a) => a -> Property
prop_not a =
       counterexample "not (not a) /= a" (not (not a) === a)
  .&&. counterexample "not a ∧ a /= bottom" (not a /\ a === bottom)
  .&&. counterexample "not a ∨ a /= top" (not a \/ a === top)

-- |
-- Test that @a@ is satisfy both @'Algebra.Heyting.prop_HeytingAlgebra'@ and
-- @'prop_not'@.
prop_BooleanAlgebra :: (BooleanAlgebra a, Eq a, Show a)
                    => a -> a -> a -> Property
prop_BooleanAlgebra a b c =
       prop_HeytingAlgebra a b c
  .&&. prop_not a
#endif