-----------------------------------------------------------------------------
--
-- Module      :  Data.Ranged.Ranges
-- Copyright   :  (c) Paul Johnson 2006
-- License     :  BSD-style
-- Maintainer  :  paul@cogito.org.uk
-- Stability   :  experimental
-- Portability :  portable
--
-----------------------------------------------------------------------------

-- | A range has an upper and lower boundary.
module Data.Ranged.Ranges (
   -- ** Construction
   Range (..),
   emptyRange,
   fullRange,
   -- ** Predicates
   rangeIsEmpty,
   rangeIsFull,
   rangeOverlap,
   rangeEncloses,
   rangeSingletonValue,
   -- ** Membership
   rangeHas,
   rangeListHas,
   -- ** Set Operations
   singletonRange,
   rangeIntersection,
   rangeUnion,
   rangeDifference,
   -- ** QuickCheck properties
   prop_unionRange,
   prop_unionRangeLength,
   prop_intersectionRange,
   prop_differenceRange,
   prop_intersectionOverlap,
   prop_enclosureUnion,
   prop_singletonRangeHas,
   prop_singletonRangeHasOnly,
   prop_singletonRangeConverse,
   prop_emptyNonSingleton,
   prop_fullNonSingleton,
   prop_nonSingleton,
   prop_intSingleton
) where

import Control.Monad
import Data.Ranged.Boundaries
import Data.Maybe
import Test.QuickCheck

-- | A Range has upper and lower boundaries.
data Ord v => Range v = Range {rangeLower, rangeUpper :: Boundary v}

instance (DiscreteOrdered a) => Eq (Range a) where
   r1 == r2   = (rangeIsEmpty r1 && rangeIsEmpty r2) ||
                (rangeLower r1 == rangeLower r2 &&
                 rangeUpper r1 == rangeUpper r2)


instance (DiscreteOrdered a) => Ord (Range a) where
   compare r1 r2
      | r1 == r2       = EQ
      | rangeIsEmpty r1  = LT
      | rangeIsEmpty r2  = GT
      | otherwise      = compare (rangeLower r1, rangeUpper r1)
                                 (rangeLower r2, rangeUpper r2)

instance (Show a, DiscreteOrdered a) => Show (Range a) where
   show r
      | rangeIsEmpty r     = "Empty"
      | rangeIsFull r      = "All x"
      | otherwise          =
         case rangeSingletonValue r of
            Just v  -> "x == " ++ show v
            Nothing -> lowerBound ++ "x" ++ upperBound
      where
         lowerBound = case rangeLower r of
            BoundaryBelowAll -> ""
            BoundaryBelow v  -> show v ++ " <= "
            BoundaryAbove v  -> show v ++ " < "
            BoundaryAboveAll -> error "show Range: lower bound is BoundaryAboveAll"
         upperBound = case rangeUpper r of
            BoundaryBelowAll -> error "show Range: upper bound is BoundaryBelowAll"
            BoundaryBelow v  -> " < " ++ show v
            BoundaryAbove v  -> " <= " ++ show v
            BoundaryAboveAll -> ""


-- | True if the value is within the range.
rangeHas :: Ord v => Range v -> v -> Bool

rangeHas (Range b1 b2) v =
   (v />/ b1) && not (v />/ b2)


-- | True if the value is within one of the ranges.
rangeListHas :: Ord v =>
   [Range v] -> v -> Bool
rangeListHas ls v = or $ map (\r -> rangeHas r v) ls


-- | The empty range
emptyRange :: DiscreteOrdered v => Range v
emptyRange = Range BoundaryAboveAll BoundaryBelowAll


-- | The full range.  All values are within it.
fullRange :: DiscreteOrdered v => Range v
fullRange = Range BoundaryBelowAll BoundaryAboveAll


-- | A range containing a single value
singletonRange :: DiscreteOrdered v => v -> Range v
singletonRange v = Range (BoundaryBelow v) (BoundaryAbove v)


-- | If the range is a singleton, returns @Just@ the value.  Otherwise returns
-- @Nothing@.
--
-- Known bug: This always returns @Nothing@ for ranges including 
-- @BoundaryBelowAll@ or @BoundaryAboveAll@.  For bounded types this can be 
-- incorrect.  For instance, the following range only contains one value:
-- 
-- >    Range (BoundaryBelow maxBound) BoundaryAboveAll
rangeSingletonValue :: DiscreteOrdered v => Range v -> Maybe v
rangeSingletonValue (Range (BoundaryBelow v1) (BoundaryBelow v2))
   | adjacent v1 v2  = Just v1
   | otherwise       = Nothing
rangeSingletonValue (Range (BoundaryBelow v1) (BoundaryAbove v2))
   | v1 == v2        = Just v1
   | otherwise       = Nothing
rangeSingletonValue (Range (BoundaryAbove v1) (BoundaryBelow v2)) = 
   do
      v2' <- adjacentBelow v2
      v2'' <- adjacentBelow v2'
      if v1 == v2'' then return v2' else Nothing
rangeSingletonValue (Range (BoundaryAbove v1) (BoundaryAbove v2))
   | adjacent v1 v2  = Just v2
   | otherwise       = Nothing
rangeSingletonValue (Range _ _) = Nothing

-- | A range is empty unless its upper boundary is greater than its lower
-- boundary.
rangeIsEmpty :: DiscreteOrdered v => Range v -> Bool
rangeIsEmpty (Range lower upper) = upper <= lower


-- | A range is full if it contains every possible value.
rangeIsFull :: DiscreteOrdered v => Range v -> Bool
rangeIsFull = (== fullRange)

-- | Two ranges overlap if their intersection is non-empty.
rangeOverlap :: DiscreteOrdered v => Range v -> Range v -> Bool
rangeOverlap r1 r2 =
   not (rangeIsEmpty r1)
   && not (rangeIsEmpty r2)
   && not (rangeUpper r1 <= rangeLower r2 || rangeUpper r2 <= rangeLower r1)


-- | The first range encloses the second if every value in the second range is
-- also within the first range.  If the second range is empty then this is
-- always true.
rangeEncloses :: DiscreteOrdered v => Range v -> Range v -> Bool
rangeEncloses r1 r2 =
   (rangeLower r1 <= rangeLower r2 && rangeUpper r2 <= rangeUpper r1)
   || rangeIsEmpty r2


-- | Intersection of two ranges, if any.
rangeIntersection :: DiscreteOrdered v => Range v -> Range v -> Range v
rangeIntersection r1@(Range lower1 upper1) r2@(Range lower2 upper2)
    | rangeIsEmpty r1 || rangeIsEmpty r2  = emptyRange
    | otherwise  = Range (max lower1 lower2) (min upper1 upper2)


-- | Union of two ranges.  Returns one or two results.
--
-- If there are two results then they are guaranteed to have a non-empty
-- gap in between, but may not be in ascending order.
rangeUnion :: DiscreteOrdered v => Range v -> Range v -> [Range v]
rangeUnion r1@(Range lower1 upper1) r2@(Range lower2 upper2)
   | rangeIsEmpty r1  = [r2]
   | rangeIsEmpty r2  = [r1]
   | otherwise =
       if touching then [Range lower upper] else [r1, r2]
   where
     touching = (max lower1 lower2) <= (min upper1 upper2)
     lower = min lower1 lower2
     upper = max upper1 upper2


-- | @range1@ minus @range2@.  Returns zero, one or two results.  Multiple
-- results are guaranteed to have non-empty gaps in between, but may not be in
-- ascending order.
rangeDifference :: DiscreteOrdered v => Range v -> Range v -> [Range v]

rangeDifference r1@(Range lower1 upper1) (Range lower2 upper2) =
   -- There are six possibilities
   --    1: r2 completely less than r1
   --    2: r2 overlaps bottom of r1
   --    3: r2 encloses r1
   --    4: r1 encloses r2
   --    5: r2 overlaps top of r1
   --    6: r2 completely greater than r1
   if intersects
      then -- Cases 2,3,4,5
         filter (not . rangeIsEmpty) [Range lower1 lower2, Range upper2 upper1]
      else -- Cases 1, 6
         [r1]
   where
      intersects = (max lower1 lower2) < (min upper1 upper2)


-- QuickCheck generators

instance (Arbitrary v,  DiscreteOrdered v, Show v) =>
   Arbitrary (Range v) where

   arbitrary = frequency [
      (17, do  -- Ordinary range
         b1 <- arbitrary
         b2 <- arbitrary
         if b1 < b2
            then return $ Range b1 b2
            else return $ Range b2 b1
      ),
      (1, do  -- Singleton range
         v <- arbitrary
         return $ singletonRange v
      ),
      (1, return emptyRange),
      (1, return fullRange)
      ]

   coarbitrary (Range lower upper) =
      variant 0 . coarbitrary lower . coarbitrary upper



-- QuickCheck Properties

-- | The union of two ranges has a value iff either range has it.
-- 
-- > prop_unionRange r1 r2 n =
-- >    (r1 `rangeHas` n || r2 `rangeHas` n)
-- >    == (r1 `rangeUnion` r2) `rangeListHas` n
prop_unionRange :: (DiscreteOrdered a) => Range a -> Range a -> a -> Bool
prop_unionRange r1 r2 n =
   (r1 `rangeHas` n || r2 `rangeHas` n)
   == (r1 `rangeUnion` r2) `rangeListHas` n

-- | The union of two ranges always contains one or two ranges.
-- 
-- > prop_unionRangeLength r1 r2 = (n == 1) || (n == 2)
-- >    where n = length $ rangeUnion r1 r2
prop_unionRangeLength :: (DiscreteOrdered a) => Range a -> Range a -> Bool
prop_unionRangeLength r1 r2 = (n == 1) || (n == 2)
   where n = length $ rangeUnion r1 r2

-- | The intersection of two ranges has a value iff both ranges have it.
-- 
-- > prop_intersectionRange r1 r2 n =
-- >    (r1 `rangeHas` n && r2 `rangeHas` n)
-- >    == (r1 `rangeIntersection` r2) `rangeHas` n
prop_intersectionRange :: (DiscreteOrdered a) => Range a -> Range a -> a -> Bool
prop_intersectionRange r1 r2 n =
   (r1 `rangeHas` n && r2 `rangeHas` n)
   == (r1 `rangeIntersection` r2) `rangeHas` n

-- | The difference of two ranges has a value iff the first range has it and
-- the second does not.
-- 
-- > prop_differenceRange r1 r2 n =
-- >    (r1 `rangeHas` n && not (r2 `rangeHas` n))
-- >    == (r1 `rangeDifference` r2) `rangeListHas` n
prop_differenceRange :: (DiscreteOrdered a) => Range a -> Range a -> a -> Bool
prop_differenceRange r1 r2 n =
   (r1 `rangeHas` n && not (r2 `rangeHas` n))
   == (r1 `rangeDifference` r2) `rangeListHas` n

-- | Iff two ranges overlap then their intersection is non-empty.
-- 
-- > prop_intersectionOverlap r1 r2 = 
-- >     (rangeIsEmpty $ rangeIntersection r1 r2) == (rangeOverlap r1 r2)
prop_intersectionOverlap :: (DiscreteOrdered a) => Range a -> Range a -> Bool
prop_intersectionOverlap r1 r2 = 
    (rangeIsEmpty $ rangeIntersection r1 r2) == not (rangeOverlap r1 r2)

-- | Range enclosure makes union an identity function.
-- 
-- > prop_enclosureUnion r1 r2 = 
-- >    rangeEncloses r1 r2 == (rangeUnion r1 r2 == [r1])
prop_enclosureUnion :: (DiscreteOrdered a) => Range a -> Range a -> Bool
prop_enclosureUnion r1 r2 = rangeEncloses r1 r2 == (rangeUnion r1 r2 == [r1])

-- | Range Singleton has its member.
-- 
-- > prop_singletonRangeHas v = singletonRange v `rangeHas` v
prop_singletonRangeHas :: (DiscreteOrdered a) => a -> Bool
prop_singletonRangeHas v = singletonRange v `rangeHas` v

-- | Range Singleton has only its member.
-- 
-- > prop_singletonHasOnly v1 v2 =
-- >    (v1 == v2) == (singletonRange v1 `rangeHas` v2)
prop_singletonRangeHasOnly :: (DiscreteOrdered a) => a -> a -> Bool
prop_singletonRangeHasOnly v1 v2 =
   (v1 == v2) == (singletonRange v1 `rangeHas` v2)

-- | A singleton range can have its value extracted.
-- 
-- > prop_singletonRangeConverse v =
-- >    rangeSingletonValue (singletonRange v) == Just v
prop_singletonRangeConverse:: (DiscreteOrdered a) => a -> Bool
prop_singletonRangeConverse v =
   rangeSingletonValue (singletonRange v) == Just v

-- | The empty range is not a singleton.
-- 
-- > prop_emptyNonSingleton = rangeSingletonValue emptyRange == Nothing
prop_emptyNonSingleton :: Bool
prop_emptyNonSingleton = 
    rangeSingletonValue (emptyRange :: Range Int) == Nothing

-- | The full range is not a singleton.
-- 
-- > prop_fullNonSingleton = rangeSingletonValue fullRange == Nothing
prop_fullNonSingleton :: Bool
prop_fullNonSingleton = 
    rangeSingletonValue (fullRange :: Range Int) == Nothing

-- | For real x and y, @x < y@ implies that any range between them is a
-- non-singleton.
prop_nonSingleton :: Double -> Double -> Property
prop_nonSingleton x y = (x < y) ==> null $ mapMaybe rangeSingletonValue rs
   where rs = [
          Range (BoundaryBelow x) (BoundaryBelow y),
          Range (BoundaryAbove x) (BoundaryBelow y),
          Range (BoundaryBelow x) (BoundaryAbove y),
          Range (BoundaryAbove x) (BoundaryAbove y)]


-- | For all integers x and y, any range formed from boundaries on either side
-- of x and y is a singleton iff it contains exactly one integer.
prop_intSingleton :: Integer -> Integer -> Property
prop_intSingleton x y = forAll (rangeAround x y) $ \r ->
                        case filter (rangeHas r) [x-1 .. y+1] of
                          [v]  -> rangeSingletonValue r == Just v
                          _    -> rangeSingletonValue r == Nothing
    where
      rangeAround v1 v2 = return Range `ap` genBound v1 `ap` genBound v2
      genBound v = elements [BoundaryAbove v, BoundaryBelow v]