{-# OPTIONS_HADDOCK hide #-}

{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}

module Imj.Game.Hamazed.Loop.Update
      ( update
      ) where

import           Imj.Prelude

import           Data.Maybe( catMaybes, isNothing )

import           Imj.Game.Hamazed.Infos
import           Imj.Game.Hamazed.Level.Types
import           Imj.Game.Hamazed.Loop.Create
import           Imj.Game.Hamazed.Loop.Event
import           Imj.Game.Hamazed.Loop.Timing
import           Imj.Game.Hamazed.Parameters
import           Imj.Game.Hamazed.Types
import           Imj.Game.Hamazed.World
import           Imj.Game.Hamazed.World.Number
import           Imj.Game.Hamazed.World.Ship
import           Imj.Game.Hamazed.World.Space.Types
import           Imj.GameItem.Weapon.Laser
import           Imj.Geo.Continuous
import           Imj.Geo.Discrete
import           Imj.Graphics.Animation.Design.Types
import           Imj.Graphics.Animation.Design.Update
import           Imj.Graphics.Animation
import           Imj.Graphics.UI.RectContainer
import           Imj.Util


-- | Updates the state. It needs IO just to generate random numbers in case
-- 'Event' is 'StartLevel'
{-# INLINABLE update #-}
update :: GameParameters
       -- ^ 'World' creation parameters, used in case the 'Event' is 'StartLevel'.
       -> GameState
       -- ^ The current state
       -> Event
       -- ^ The 'Event' that should be handled here.
       -> IO GameState
update
 params
 state@(GameState b world@(World c d space animations e)
                  futWorld f h@(Level level target mayLevelFinished) anim) = \case
  StartLevel nextLevel ->
    mkInitialState params nextLevel (Just state) >>= \case
      Left err -> error err
      Right s -> return s
  (Timeout (Deadline gt AnimateUI)) ->
    return $ updateAnim gt state
  (Timeout (Deadline _ DisplayContinueMessage)) ->
    return $ case mayLevelFinished of
      Just (LevelFinished stop finishTime _) ->
        let newLevel = Level level target (Just $ LevelFinished stop finishTime ContinueMessage)
        in GameState b world futWorld f newLevel anim
      Nothing -> state
  (Timeout (Deadline k Animate)) -> do
    let newAnimations = mapMaybe (\a -> if shouldUpdate a k
                                                then updateAnimation a
                                                else Just a) animations
    return $ GameState b (World c d space newAnimations e) futWorld f h anim
  (Timeout (Deadline gt MoveFlyingItems)) -> do
    let movedState = GameState (Just $ addDuration gameMotionPeriod gt) (moveWorld gt world) futWorld f h anim
    return $ onHasMoved movedState gt
  Action Laser dir ->
    if isFinished anim
      then
        getSystemTime >>= return . (onLaser state dir)
      else
        return $ state
  Action Ship dir ->
    return $ accelerateShip' dir state
  (Interrupt _) ->
    return state
  EndGame -> -- TODO instead, go back to game configuration ?
    return state


onLaser :: GameState
        -> Direction
        -> SystemTime
        -> GameState
onLaser
  (GameState b world@(World _ (BattleShip posspeed ammo safeTime collisions)
                     space animations e)
             futureWorld g (Level i target finished)
             (UIAnimation (UIEvolutions j upDown left) k l))
  dir t =
  let (remainingBalls, destroyedBalls, maybeLaserRay, newAmmo) =
        laserEventAction dir world
      outerSpaceAnims_ =
         if null destroyedBalls
           then
             maybe [] (outerSpaceAnims t world) maybeLaserRay
           else
            []
      newAnimations =
        destroyedNumbersAnimations (Left t) dir world destroyedBalls
        ++ maybe [] (\r -> laserAnims world r t) maybeLaserRay
        ++ outerSpaceAnims_

      newWorld = World remainingBalls (BattleShip posspeed newAmmo safeTime collisions)
                       space (newAnimations ++ animations) e
      destroyedNumbers = map (\(Number _ n) -> n) destroyedBalls
      allShotNumbers = g ++ destroyedNumbers
      newLeft =
        if null destroyedNumbers && ammo == newAmmo
          then
            left
          else
            let frameSpace = mkWorldContainer world
                infos = mkLeftInfo Normal newAmmo allShotNumbers
                (_, _, leftMiddle, _) = getSideCentersAtDistance frameSpace 3 2
            in mkTextAnimRightAligned leftMiddle leftMiddle infos 0 -- 0 duration, since animation is over anyway
      newFinished = finished <|> checkTargetAndAmmo newAmmo (sum allShotNumbers) target t
      newLevel = Level i target newFinished
      newAnim = UIAnimation (UIEvolutions j upDown newLeft) k l
  in assert (isFinished newAnim) $ GameState b newWorld futureWorld allShotNumbers newLevel newAnim

-- | The world has moved, so we update it.
onHasMoved :: GameState
           -> KeyTime
           -> GameState
onHasMoved
  (GameState b world@(World balls ship@(BattleShip _ _ safeTime collisions) space animations e)
             futureWorld shotNums (Level i target finished) anim)
  keyTime@(KeyTime t) =
  let newAnimations = shipAnims world keyTime
      remainingBalls =
        if isNothing safeTime
          then
            filter (`notElem` collisions) balls
          else
            balls
      newWorld = World remainingBalls ship space (newAnimations ++ animations) e
      finishIfShipCollides =
        maybe
          (case map (\(Number _ n) -> n) collisions of
            [] -> Nothing
            l  -> Just $ LevelFinished (Lost $ "collision with " <> showListOrSingleton l) t InfoMessage )
          (const Nothing)
            safeTime
      newLevel = Level i target (finished <|> finishIfShipCollides)
  in assert (isFinished anim) $ GameState b newWorld futureWorld shotNums newLevel anim


outerSpaceAnims :: SystemTime
                -> World
                -> LaserRay Actual
                -> [Animation]
outerSpaceAnims t world@(World _ _ (Space _ sz _) _ _) ray@(LaserRay dir _) =
  let laserTarget = afterEnd ray
  in case onOuterBorder laserTarget sz of
       Just outDir -> outerSpaceAnims' t world laserTarget $ assert (dir == outDir) dir
       Nothing -> []

outerSpaceAnims' :: SystemTime
                 -> World
                 -> Coords Pos
                 -> Direction
                 -> [Animation]
outerSpaceAnims' t@(MkSystemTime _ nanos) world fronteerPoint dir =
  let char = niceChar $ fromIntegral nanos -- cycle character every nano second
      speed = scalarProd 0.8 $ speed2vec $ coordsForDirection dir
      outerSpacePoint = translateInDir dir fronteerPoint
      interaction = environmentInteraction world TerminalWindow
  in fragmentsFreeFall speed outerSpacePoint interaction (Speed 1) (Left t) char


laserAnims :: World
           -> LaserRay Actual
           -> SystemTime
           -> [Animation]
laserAnims world ray t =
  let interaction = environmentInteraction world WorldFrame
  in catMaybes [laserAnimation ray interaction (Left t)]


accelerateShip' :: Direction -> GameState -> GameState
accelerateShip' dir (GameState c (World wa ship wc wd we) b f g h) =
  let newShip = accelerateShip dir ship
      world = World wa newShip wc wd we
  in GameState c world b f g h

updateAnim :: KeyTime -> GameState -> GameState
updateAnim kt (GameState _ curWorld futWorld j k (UIAnimation evolutions _ it)) =
  let nextIt@(Iteration _ nextFrame) = nextIteration it
      (world, gameDeadline, worldAnimDeadline) =
        maybe
          (futWorld , Just kt, Nothing)
          (\dt ->
           (curWorld, Nothing, Just $ addDuration (floatSecondsToDiffTime dt) kt))
          $ getDeltaTime evolutions nextFrame
      wa = UIAnimation evolutions worldAnimDeadline nextIt
  in GameState gameDeadline world futWorld j k wa