{-# LANGUAGE ScopedTypeVariables #-}
-- | This internal module provides functions used to define the various
-- @enumFrom*@ functions of 'Enum'.
--
-- We expect 'fromEnum' to be an ordering homomorphism, that is:
--
-- @
-- forall a b. Enum a b
-- succ a == b => fromEnum a < fromEnum b
-- @
--
-- Note that this homomorphism is most likely not surjective.  Note further that
-- one cannot assume:
--
-- @
-- CANNOT BE ASSUMED !
-- succ a == b => fromEnum a + 1 == fromEnum b
-- @
--
-- The 'succ' essor of a given message enum value @A@ that's not 'maxBound' is
-- the enum value @B@ whose 'fromEnum' value is the one immediately after @A@'s
-- 'fromEnum' value.  That is, 'fromEnum' determines order, but not distance.
--
-- As an example, consider the enum in the test suite:
--
-- @
-- enum Baz {
--     BAZ1 = 1; BAZ2 = 2; BAZ3 = 4; BAZ4 = 6;
--     BAZ5 = 7; BAZ6 = 9; BAZ7 = 10; BAZ8 = 12;
-- }
-- @
--
-- In this case, @succ BAZ2@ is @BAZ3@ despite their fromEnum values differing
-- by 2.  Further, @[BAZ2, BAZ4 ..]@ or equivalently
-- @messageEnumFromThen BAZ2 BAZ4@ is every other enum (i.e. a distance of 2)
-- when taken as a list, i.e.  @[BAZ2, BAZ4, BAZ6, BAZ8]@ despite the
-- 'fromEnum' distances being @[4, 3, 3]@.
--
-- That said, it is highly unwise to use any of the @[a,b ..*]@ patterns or
-- @enumFromThen*@ functions since adding or removing enums values can cause
-- previously functioning code to fail.  I.e. removing @BAZ3@ in the above
-- example makes the result equivalent @fromEnum BAZ2@ and the sequence now
-- includes every enum value save @BAZ1@.  This is all despite the fact that
-- @BAZ3@ was never referenced.
module Data.ProtoLens.Message.Enum
    ( messageEnumFrom
    , messageEnumFromTo
    , messageEnumFromThen
    , messageEnumFromThenTo
    ) where

import Data.List (unfoldr)
import Data.Ord (comparing)

messageEnumFromTo :: Enum a => a -> a -> [a]
messageEnumFromTo start stop = case comparing fromEnum start stop of
    LT -> messageEnumFromThenTo start (succ start) stop
    -- The only time we can't call 'succ' on @start@ is if it's 'maxBound' which
    -- is >= all values by definition, so the below cases cover all possible
    -- cases where succ cannot be called.
    EQ -> [start]
    GT -> []

messageEnumFrom :: (Enum a, Bounded a) => a -> [a]
messageEnumFrom = flip messageEnumFromTo maxBound

messageEnumFromThen :: (Enum a, Bounded a) => a -> a -> [a]
messageEnumFromThen start step = case comparing fromEnum start step of
    LT -> messageEnumFromThenTo start step maxBound
    EQ -> repeat start
    GT -> messageEnumFromThenTo start step minBound

messageEnumFromThenTo :: forall a . Enum a => a -> a -> a -> [a]
messageEnumFromThenTo start step stop = case comparing fromEnum start step of
    LT -> helper succ GT
    EQ -> if stopInt >= stepInt then repeat start else []
    GT -> helper pred LT
  where
    stopInt = fromEnum stop
    stepInt = fromEnum step
    helper iter isAfter
        | comparing fromEnum start stop == isAfter = []
        | compare stepInt stopInt == isAfter = [start]
        | otherwise = start : unfoldr (fmap unfoldIter) (Just step)
      where
        -- This applies @iter@ (which is either succ or pred depending on our
        -- direction) @n@ times.  This returns @Just@ the result unless we've
        -- passed @stop@, in which case we return @Nothing@.
        jump 0 a = Just a
        jump n a
            | stopInt == fromEnum a = Nothing
            | otherwise = jump (n-1) $ iter a
        unfoldIter a = (a, jump skipCount a)
        countSkips :: Integer -> a -> Integer
        countSkips n start'
            | stepInt == fromEnum start' = n
            | otherwise = countSkips (n+1) $ iter start'
        -- This is the number of applications of @iter@ (again, either @succ@
        -- or @pred@) needed to get from @start@ to @step@.
        skipCount = countSkips 0 start