{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DefaultSignatures #-}
module Animate
  ( Color
  , FrameIndex
  , Frame(..)
  , Animations(..)
  , Loop(..)
  , Position(..)
  , FrameStep(..)
  , KeyName(..)
  , SpriteClip(..)
  , SpriteSheet(..)
  , SpriteSheetInfo(..)
  , animations
  , framesByAnimation
  , initPosition
  , initPositionLoops
  , initPositionWithLoop
  , stepFrame
  , stepPosition
  , isAnimationComplete
  , positionHasLooped
  , currentFrame
  , currentLocation
  , nextKey
  , prevKey
  , readSpriteSheetInfoJSON
  , readSpriteSheetInfoYAML
  , readSpriteSheetJSON
  , readSpriteSheetYAML
  ) where

import qualified Data.Vector as V (Vector, (!), length, fromList)
import qualified Data.Map as Map
import qualified Data.ByteString.Lazy as BL
import qualified Data.Yaml as Y
import Control.Applicative ((<|>))
import Control.Monad (mzero)
import Data.Aeson (FromJSON(..), ToJSON(..), (.:), eitherDecode, object, (.=), Value(..))
import Data.Map (Map)
import Data.Word (Word8)
import Data.Text (Text, pack)
import GHC.Generics (Generic)


-- | Alias for RGB (8bit, 8bit, 8bit)
type Color = (Word8, Word8, Word8)

type FrameIndex = Int

data Frame loc delay = Frame
  { fLocation :: loc -- ^ User defined reference to the location of a sprite. For example, a sprite sheet clip.
  , fDelay :: delay -- ^ Minimium amount of units for the frame to last.
  } deriving (Show, Eq, Generic)

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

-- class (Ord key, Bounded key, Enum key) => Key key

-- | Animation Keyframe. `keyName` is used for JSON parsing.
class KeyName key where
  keyName :: key -> Text
  default keyName :: Show key => key -> Text
  keyName = pack . dropTickPrefix . show
    where
      dropTickPrefix :: String -> String
      dropTickPrefix = drop 1 . dropWhile (/= '\'')

-- | Describe the boxed area of the 2d sprite inside a sprite sheet
data SpriteClip key = SpriteClip
  { scX :: Int
  , scY :: Int
  , scW :: Int
  , scH :: Int
  , scOffset :: Maybe (Int, Int)
  } deriving (Show, Eq, Generic)

instance ToJSON (SpriteClip key) where
  toJSON SpriteClip{scX,scY,scW,scH,scOffset} = case scOffset of
    Nothing -> toJSON (scX, scY, scW, scH)
    Just (ofsX, ofsY) -> toJSON (scX, scY, scW, scH, ofsX, ofsY)

instance FromJSON (SpriteClip key) where
  parseJSON v =
    (do
      (x,y,w,h) <- parseJSON v
      return SpriteClip { scX = x, scY = y, scW = w, scH = h, scOffset = Nothing })
    <|>
    (do
      (x,y,w,h,ofsX,ofsY) <- parseJSON v
      return SpriteClip { scX = x, scY = y, scW = w, scH = h, scOffset = Just (ofsX, ofsY) })

-- | Generalized sprite sheet data structure
data SpriteSheet key img delay = SpriteSheet
  { ssAnimations :: Animations key (SpriteClip key) delay
  , ssImage :: img
  } deriving (Generic)

-- | One way to represent sprite sheet information.
--   JSON loading is included.
data SpriteSheetInfo key delay = SpriteSheetInfo
  { ssiImage :: FilePath
  , ssiAlpha :: Maybe Color
  , ssiClips :: [SpriteClip key]
  , ssiAnimations :: Map Text [(FrameIndex, delay)]
  } deriving (Show, Eq, Generic)

instance ToJSON delay => ToJSON (SpriteSheetInfo key delay) where
  toJSON SpriteSheetInfo{ssiImage,ssiAlpha,ssiClips,ssiAnimations} = object
    [ "image" .= ssiImage
    , "alpha" .= ssiAlpha
    , "clips" .= ssiClips
    , "animations" .= ssiAnimations
    ]

instance FromJSON delay => FromJSON (SpriteSheetInfo key delay) where
  parseJSON (Object o) = do
    image <- o .: "image"
    alpha <- o .: "alpha"
    clips <- o .: "clips"
    anis <- o .: "animations"
    return SpriteSheetInfo { ssiImage = image, ssiAlpha = alpha, ssiClips = clips, ssiAnimations = anis }
  parseJSON _ = mzero

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

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

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

-- | State for progression through an animation
--
-- > example = Position minBound 0 0 Loop'Always
data Position key delay = Position
  { pKey :: key -- ^ Index for the animation.
  , pFrameIndex :: FrameIndex -- ^ 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 :: delay -- ^ Accumulated units to end of the frame. Will continue to compound if animation is completed.
  , pLoop :: Loop -- ^ How to loop through an animation. Loop'Count is a count down.
  } deriving (Show, Eq, Generic)

-- | New `Position` with its animation key to loop forever
initPosition :: (Num delay) => key -> Position key delay
initPosition key = initPositionWithLoop key Loop'Always

-- | New `Position` with its animation key with a limited loop
initPositionLoops :: (Num delay) => key -> Int -> Position key delay
initPositionLoops key count = initPositionWithLoop key (Loop'Count count)

-- | New `Position`
initPositionWithLoop :: (Num delay) => key -> Loop -> Position key delay
initPositionWithLoop key loop = Position
  { pKey = key
  , pFrameIndex = 0
  , pCounter = 0
  , pLoop = loop
  }

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

-- | Intermediate function for how a frame should be step through.
stepFrame :: (Num delay, Ord delay) => Frame loc delay -> Position key delay -> delay -> FrameStep delay
stepFrame Frame{fDelay} Position{pCounter} delta =
  if pCounter + delta >= fDelay
    then FrameStep'Delta $ pCounter + delta - fDelay
    else FrameStep'Counter $ pCounter + delta

-- | Step through the animation resulting a new position.
stepPosition :: (Enum key, Num delay, Ord delay) => Animations key loc delay -> Position key delay -> delay -> Position key delay
stepPosition as p d =
  case frameStep of
    FrameStep'Counter counter -> p{pCounter = counter }
    FrameStep'Delta delta -> stepPosition as p' delta
  where
    frameStep = stepFrame f p d
    fs = unAnimations as V.! fromEnum (pKey p)
    f = fs V.! pFrameIndex p
    p'= case pLoop p of
      Loop'Always -> p{pFrameIndex = (pFrameIndex p + 1) `mod` V.length fs, pCounter = 0}
      Loop'Count 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 = Loop'Count n' }

-- | Use the position to find the current frame of the animation.
currentFrame :: (Enum key, Num delay) => Animations key loc delay -> Position key delay -> Frame loc delay
currentFrame anis Position{pKey,pFrameIndex} = (framesByAnimation anis pKey) V.! pFrameIndex

-- | Use the position to find the current location, lik a sprite sheet clip, of the animation.
currentLocation :: (Enum key, Num delay) => Animations key loc delay -> Position key delay -> loc
currentLocation anis p = fLocation (currentFrame anis p)

-- | The animation has finished all its frames. Useful for signalling into switching to another animation.
--   With a Loop'Always, the animation will never be completed.
isAnimationComplete :: (Enum key, Num delay, Ord delay) => Animations key loc delay -> Position key delay -> Bool
isAnimationComplete as p = case pLoop p of
  Loop'Always -> False
  Loop'Count n -> n < 0 && pFrameIndex p == lastIndex && pCounter p >= fDelay lastFrame
  where
    frames = framesByAnimation as (pKey p)
    lastIndex = V.length frames - 1
    lastFrame = frames V.! lastIndex

-- | Cycle through the next animation key.
nextKey :: (Bounded key, Enum key, Eq key) => key -> key
nextKey key = if key == maxBound then minBound else succ key

-- | Cycle through the previous animation key.
prevKey :: (Bounded key, Enum key, Eq key) => key -> key
prevKey key = if key == minBound then maxBound else pred key

-- | Simple function diff'ing the position for loop change.
positionHasLooped
  :: Position key delay -- ^ Previous
  -> Position key delay -- ^ Next
  -> Bool
positionHasLooped Position{ pLoop = Loop'Count c } Position{ pLoop = Loop'Count c' } = c > c'
positionHasLooped Position{ pLoop = Loop'Always } _ = False
positionHasLooped _ Position{ pLoop = Loop'Always } = False

-- | Quick function for loading `SpriteSheetInfo`.
--   Check the example.
readSpriteSheetInfoJSON
  :: FromJSON delay
  => FilePath -- ^ Path of the sprite sheet info JSON file
  -> IO (SpriteSheetInfo key delay)
readSpriteSheetInfoJSON = readSpriteSheetInfo eitherDecode

readSpriteSheetInfoYAML
  :: FromJSON delay
  => FilePath -- ^ Path of the sprite sheet info JSON file
  -> IO (SpriteSheetInfo key delay)
readSpriteSheetInfoYAML = readSpriteSheetInfo eitherDecodeYAML

eitherDecodeYAML :: FromJSON a => BL.ByteString -> Either String a
eitherDecodeYAML = Y.decodeEither . BL.toStrict

readSpriteSheetInfo
  :: FromJSON delay
  => (BL.ByteString -> Either String (SpriteSheetInfo key delay))
  -> FilePath -- ^ Path of the sprite sheet info JSON file
  -> IO (SpriteSheetInfo key delay)
readSpriteSheetInfo decoder path = do
  metaBytes <- BL.readFile path
  case decoder metaBytes of
    Left _err -> error $ "Cannot parse Sprite Sheet Info \"" ++ path ++ "\""
    Right ssi -> return ssi

-- | Quick function for loading `SpriteSheetInfo`, then using it to load its image for a `SpriteSheet`.
--   Check the example.
readSpriteSheetJSON
  :: (KeyName key, Ord key, Bounded key, Enum key, FromJSON delay)
  => (FilePath -> Maybe Color -> IO img) -- ^ Inject an image loading function
  -> FilePath -- ^ Path of the sprite sheet info JSON file
  -> IO (SpriteSheet key img delay)
readSpriteSheetJSON = readSpriteSheet eitherDecode

readSpriteSheetYAML
  :: (KeyName key, Ord key, Bounded key, Enum key, FromJSON delay)
  => (FilePath -> Maybe Color -> IO img) -- ^ Inject an image loading function
  -> FilePath -- ^ Path of the sprite sheet info JSON file
  -> IO (SpriteSheet key img delay)
readSpriteSheetYAML = readSpriteSheet eitherDecodeYAML

readSpriteSheet
  :: (KeyName key, Ord key, Bounded key, Enum key, FromJSON delay)
  => (BL.ByteString -> Either String (SpriteSheetInfo key delay))
  -> (FilePath -> Maybe Color -> IO img)
  -> FilePath
  -> IO (SpriteSheet key img delay)
readSpriteSheet decoder loadImage infoPath = do
  SpriteSheetInfo{ssiImage, ssiClips, ssiAnimations, ssiAlpha} <- readSpriteSheetInfo decoder infoPath
  i <- loadImage ssiImage ssiAlpha
  let frame key = (key, map (\a -> Frame (ssiClips !! fst a) (snd a)) (ssiAnimations Map.! keyName key))
  let animationMap = Map.fromList $ map frame [minBound..maxBound]
  return $ SpriteSheet (animations $ (Map.!) animationMap) i