{-# OPTIONS_HADDOCK hide #-}

{-# LANGUAGE NoImplicitPrelude #-}

module Imj.Physics.Discrete.Collision
    ( -- * Handling collisions
    {- | To give a better realism in the game, if the location
    /before/ a collision is not touching a wall, then the position /after/ the
    collision will be forced to touch a wall. Note that forcing the position
    only happens if the absolute speed of one coordinate is >= 2.
    -}
      mirrorSpeedAndMoveToPrecollisionIfNeeded
    , CollisionStatus(..)
    , firstCollision
    , Location(..)
    ) where

import           Imj.Prelude

import           Imj.Geo.Discrete
import           Imj.Physics.Discrete.Types


-- | Describes if a collision exists.
data Location = InsideWorld
              -- ^ No collision.
              | OutsideWorld
              -- ^ A collision exists.
              deriving(Eq, Show)

data CollisionStatus = NoCollision
                     -- ^ no collision on the trajectory, position is unchanged
                     | PreCollision
                     -- ^ a collision exists on the trajectory,
                     -- position was changed to be just before the collision
                     -- and speed was mirrored

-- | On collision, mirrors speed and moves to the pre-collision position.
mirrorSpeedAndMoveToPrecollisionIfNeeded :: (Coords Pos -> Location)
               -- ^ Interaction function.
               -> PosSpeed
               -- ^ Input position and speed.
               -> (PosSpeed, CollisionStatus)
               -- ^ The speed was potentially mirrored
mirrorSpeedAndMoveToPrecollisionIfNeeded getLocation posspeed@(PosSpeed pos speed) =
  maybe
    (posspeed, NoCollision)
    adjustPosSpeed
    $ firstCollision getLocation trajectory
 where
  trajectory = bresenham $ mkSegment pos $ sumPosSpeed pos speed
  adjustPosSpeed (mirror, newPos) = (PosSpeed newPos $ mirrorSpeed speed mirror, PreCollision)

-- | Handles the first collision on a trajectory, assuming that the first position
-- has no collision.
firstCollision :: (Coords Pos -> Location)
               -- ^ The collision function.
               -> [Coords Pos]
               -- ^ The trajectory (the first position is expected to be collision-free).
               -> Maybe (Mirror, Coords Pos)
               -- ^ On collision, the kind of speed mirroring
               --   that should be applied and the position just before the collision.
firstCollision getLocation (p1:theRest@(p2:_)) =
  mirrorIfNeededAtomic getLocation (PosSpeed p1 (diffPosToSpeed p2 p1)) <|> firstCollision getLocation theRest
firstCollision _ _ = Nothing

-- | Mirrors a speed
mirrorSpeed :: Coords Vel -> Mirror -> Coords Vel
mirrorSpeed (Coords dr dc) m =
  case m of
    MirrorRow -> Coords (negate dr) dc
    MirrorCol -> Coords dr          (negate dc)
    MirrorAll -> Coords (negate dr) (negate dc)

-- | The kind of speed mirroring to apply in reaction to a collision.
data Mirror = MirrorRow
            -- ^ Mirror the y coordinate
            | MirrorCol
            -- ^ Mirror the x coordinate
            | MirrorAll
            -- ^ Mirror x and y coordinates

-- | When continuing with current speed, if at next iteration we encounter a wall
-- (or go through a wall for diagonal case),
-- we change the speed according to the normal of the closest wall before collision
mirrorIfNeededAtomic :: (Coords Pos -> Location) -> PosSpeed -> Maybe (Mirror, Coords Pos)
mirrorIfNeededAtomic getLocation (PosSpeed pos@(Coords r c) (Coords dr dc)) =
  let future = Coords (r+dr) (c+dc)
      isWall coord = getLocation coord == OutsideWorld
      mirror = case getLocation future of
        OutsideWorld
          | dr == 0   -> Just MirrorCol
          | dc == 0   -> Just MirrorRow
          | otherwise -> -- diagonal case
                case (isWall (Coords (r+dr) c),
                      isWall (Coords r (c+dc))) of
                        (True, True)   -> Just MirrorAll
                        (False, False) -> Just MirrorAll
                        (True, False)  -> Just MirrorRow
                        (False, True)  -> Just MirrorCol
        InsideWorld
          | dr == 0   -> Nothing
          | dc == 0   -> Nothing
          | otherwise -> -- diagonal case
                case (isWall (Coords (r+dr) c),
                      isWall (Coords r (c+dc))) of
                        (True, True)   -> Just MirrorAll
                        (False, False) -> Nothing
                        (True, False)  -> Just MirrorRow
                        (False, True)  -> Just MirrorCol
  in maybe Nothing (\m -> Just (m, pos)) mirror