content: {-# 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
  
  