module Data.Animate
  ( Seconds
  , DeltaSeconds
  , Frame(..)
  , Animations
  , animations
  , framesByAnimation
  , Loop(..)
  , Position(..)
  , FrameStep(..)
  , stepFrame
  , stepAnimation
  , isAnimationComplete
  , positionHasLooped
  ) where

import qualified Data.Vector as V (Vector, (!), length, fromList)

-- | Avoided newtype wrapper for convenience (tentative)
type Seconds = Float

-- | Type aliased seconds (tentative)
type DeltaSeconds = Seconds

data Frame loc = Frame
  { _fLocation :: loc -- ^ User defined reference to the location of a sprite. For example, a sprite sheet clip.
  , _fDelay :: Seconds -- ^ Minimium amount of time for the frame to last.
  } deriving (Show, Eq)

-- | Type safe animation set. Use an sum type with an `Enum` and `Bounded` instance for the animation `a`.
newtype Animations a loc = Animations { unAnimations :: V.Vector (V.Vector (Frame loc)) }
  deriving (Show, Eq)

-- | Generate animations given each constructor
animations :: (Enum a, Bounded a) => (a -> [Frame loc]) -> Animations a loc
animations getFrames = Animations $ V.fromList $ map (V.fromList . getFrames) [minBound..maxBound]

-- | Lookup the frames of an animation
framesByAnimation :: Enum a => Animations a loc -> a -> V.Vector (Frame loc)
framesByAnimation (Animations as) a = as V.! fromEnum a

data Loop
  = LoopForever -- ^ Never stop looping. Animation can never be completed.
  | LoopCount Int -- ^ Count down loops to below zero. 0 = no loop. 1 = one loop. 2 = two loops. etc.
  deriving (Show, Eq)

-- | State for progression through an animation
data Position a = Position
  { _pAnimation :: a -- ^ Index for the animation.
  , _pFrameIndex :: Int -- ^ Index wihin the animation. WARNING: Modifying to below zero or equal-to-or-greater-than-the-frame-count will throw out of bounds errors.
  , _pCounter :: Seconds -- ^ Accumulated seconds to end of the frame. Will continue to compound if animation is completed.
  , _pLoop :: Loop -- ^ How to loop through an animation. LoopCount is a count down.
  } deriving (Show, Eq)

-- | You can ignore. An intermediate type for `stepAnimation` to judge how to increment the current frame.
data FrameStep
  = FrameStepCounter Seconds -- ^ New counter to compare against the frame's delay.
  | FrameStepDelta DeltaSeconds -- ^ How much delta to carry over into the next frame.
  deriving (Show, Eq)

-- | Intermediate function for how a frame should be step through.
stepFrame :: Frame loc -> Position a -> DeltaSeconds -> FrameStep
stepFrame Frame{_fDelay} Position{_pCounter} delta = 
  if _pCounter + delta >= _fDelay
    then FrameStepDelta $ _pCounter + delta - _fDelay
    else FrameStepCounter $ _pCounter + delta

-- | Step through the animation resulting in a new position.
stepAnimation :: Enum a => Animations a loc -> Position a -> DeltaSeconds -> Position a
stepAnimation as p d =
  case frameStep of
    FrameStepCounter counter -> p{_pCounter = counter }
    FrameStepDelta delta -> stepAnimation as p' delta
  where
    frameStep = stepFrame f p d
    fs = unAnimations as V.! fromEnum (_pAnimation p)
    f = fs V.! _pFrameIndex p
    p'= case _pLoop p of
      LoopForever -> p{_pFrameIndex = (_pFrameIndex p + 1) `mod` V.length fs, _pCounter = 0}
      LoopCount n -> let
        index = (_pFrameIndex p + 1) `mod` V.length fs
        n' = if index == 0 then n - 1 else n
        in p
          { _pFrameIndex = if n' < 0 then _pFrameIndex p else index
          , _pCounter = 0
          , _pLoop = LoopCount n' }

-- | The animation has finished all its frames. Useful for signalling into switching to another animation.
--   With a LoopForever, the animation will never be completed.
isAnimationComplete :: Enum a => Animations a loc -> Position a -> Bool
isAnimationComplete as p = case _pLoop p of
  LoopForever -> False
  LoopCount n -> n < 0 && _pFrameIndex p == lastIndex && _pCounter p >= _fDelay lastFrame
  where
    frames = framesByAnimation as (_pAnimation p)
    lastIndex = V.length frames - 1
    lastFrame = frames V.! lastIndex


-- | Simple function diff'ing the position for loop change (tentative)
positionHasLooped
  :: Position a -- ^ Previous
  -> Position a -- ^ Next
  -> Bool
positionHasLooped Position{ _pLoop = LoopCount c } Position{ _pLoop = LoopCount c' } = c > c'
positionHasLooped Position{ _pLoop = LoopForever } _ = False
positionHasLooped _ Position{ _pLoop = LoopForever } = False