{- |
Module      : Dodge.Update
Description : Simulation update
-}
module Dodge.Update 
    ( update
    ) where
import Dodge.Data
import Dodge.Menu
import Dodge.Zone.Data
import Dodge.Config.Data
import Dodge.World.Trigger.Data
--import Dodge.Config.Data
import Dodge.Base
import Dodge.Zone
import Dodge.WallCreatureCollisions
import Dodge.LevelGen.Block
import Dodge.Update.Camera
--import Dodge.Update.UsingInput
import Dodge.SoundLogic
import Dodge.Inventory
import Dodge.Initialisation
import Dodge.Layout
import Dodge.Floor
import Geometry
import Geometry.Vector3D

import Data.List
import Data.Maybe
import Data.Function
import qualified Data.Set as S
import qualified Data.IntMap.Lazy as IM
import qualified Data.Map as M
import Control.Lens
import Data.Monoid
import System.Random
--import Data.Bifunctor

update :: World -> World
update = (frameClock +~ 1) . functionalUpdate

{- | 
 The update step.
For most menus the only way to change the world is using event handling.  -}
functionalUpdate :: World -> World
functionalUpdate w = case _menuLayers w of
    (TerminalScreen t xs:mls) -> w & menuLayers .~ (TerminalScreen (max (t-1) 0) xs:mls)
    (WaitScreen s i : _) 
        | i < 1     -> w & dbArg _worldEvents
        | otherwise -> w & menuLayers %~ ( (WaitScreen s (i-1) :) . tail )
    (OptionScreen {_scOptionFlag = GameOverOptions} : _) -> updateParticles 
        . updateProjectiles 
        . updateLightSources
        . updateClouds
        $ updateSoundQueue
        w
    (_ : _) -> w
    []  -> checkEndGame 
        . updateRadialDistortions
        . updateTriggers
        . ppEvents 
        . updateCamera
        . colCrsWalls
        . ifConfigWallRotate
        . simpleCrSprings 
        . zoneCreatures 
        . updateWalls
        . resetWorldEvents
        . dbArg _worldEvents
        . updateParticles 
        . updateProjectiles 
        . updateLightSources
        . updateClouds
        . zoneClouds
        . updateCreatures 
        . updateCreatureGroups
        . updateBlocks
        . updateSeenWalls 
        . updateSoundQueue
        $ updateCloseObjects w
  where 
    zoneCreatures = set (creaturesZone . znObjects) 
        (IM.foldl' (flip creatureInZone) IM.empty (_creatures w))
    creatureInZone cr = insertIMInZone x y cid cr
      where 
        (x,y) = crZoneOfPoint $ _crPos cr
        cid = _crID cr
    zoneClouds = set (cloudsZone . znObjects) (foldl' (flip cloudInZone) IM.empty (_clouds w))
    cloudInZone cl = insertInZoneWith x y (++) [cl]
      where 
        (x,y) = cloudZoneOfPoint $ stripZ $ _clPos cl

-- | Note the explict use of record syntax. Using lens creates a space leak.
resetWorldEvents :: World -> World
resetWorldEvents w = w {_worldEvents = id}

updateCreatureGroups :: World -> World
updateCreatureGroups w = w & creatureGroups %~ 
    IM.mapMaybe (\cgp -> _crGroupUpdate cgp w cgp)

updateRadialDistortions :: World -> World
updateRadialDistortions = radDistortion %~ mapMaybe decreaseBulge

decreaseBulge 
    :: (Point2,Point2,Point2,Float) 
    -> Maybe (Point2,Point2,Point2,Float)
decreaseBulge (a,b,c,v)
    | v > 1 = Just (a,b,c,max 1 (v - 0.05) )
    | v < 1 = Just (a,b,c,min 1 (v + 0.05) )
    | otherwise = Nothing

updateTriggers :: World -> World
updateTriggers w 
    | ResetLevel 1 `S.member` _worldTriggers w = generateLevelFromRoomList levx $ initialWorld 
        & randGen       .~ _randGen w
        & config        .~ _config w
        & menuLayers    .~ []
        & creatures . ix 0 .~ cr
    | otherwise = w
  where
    cr = _creatures w IM.! 0 & crPos .~ V2 0 0

updateSoundQueue :: World -> World
updateSoundQueue = set sounds M.empty

updateLightSources :: World -> World
updateLightSources w = set tempLightSources tlss w
  where 
    tlss = mapMaybe (\b -> _tlsUpdate b w b) $ _tempLightSources w

updateProjectiles :: World -> World
updateProjectiles w = IM.foldl' (flip $ dbArg _pjUpdate) w $ _projectiles w
{-
Apply internal particle updates, delete 'Nothing's.  -}
updateParticles :: World -> World
updateParticles w = set particles (catMaybes ps) w'
  where 
    (w',ps) = mapAccumR (\a b -> _ptUpdate' b a b) w $ _particles w

-- Note that this updates the randgen
updateCreatures :: World -> World
updateCreatures w = appEndo f $ w 
    & creatures .~ IM.mapMaybe id m
    & randGen   .~ newg
  where
    ((f,newg),m) = mapAccumR crUp (Endo id,_randGen w) (_creatures w)
    crUp (f',g) cr = ((f' <> f'',g'), cr')
      where
        (f'',cr') = _crUpdate cr cr (w & randGen .~ g)
        (_,g') = genWord8 g
{- |
Apply door mechanisms.  -}
updateWalls :: World -> World
updateWalls w = IM.foldl' (flip $ fromMaybe id . (^? doorMech)) w (_walls w)

ppEvents :: World -> World
ppEvents w = IM.foldl' (flip $ \pp w' -> _ppEvent pp pp w') w $ _pressPlates w

updateSeenWalls :: World -> World
updateSeenWalls w = foldl' (flip markSeen) w wallsToUpdate
  where 
    vPos = _cameraViewFrom w
    wallsToUpdate = concatMap (\p -> visibleWalls vPos (vPos +.+p) $ wallsAlongLine vPos (vPos +.+ p) w)
                             $ nRays 20
    markSeen i = set (walls . ix i . wlSeen) True

--setTestStringIO :: IO World -> IO World
--setTestStringIO = fmap (\ w -> set testString (show $ s w) w)
--  where 
--    s w = (-.-) <$> (w ^? creatures . ix 0 . crPos) <*> (w ^? creatures . ix 0 . crOldPos)

checkEndGame :: World -> World
checkEndGame w 
    | _crHP (you w) < 1 = haltSound $ w {_menuLayers = [gameOverMenu]}
    | otherwise         = w

--updateClouds :: World -> World
--updateClouds w = w' & clouds %~ mapMaybe (updateCloud w)
updateClouds :: World -> World
updateClouds w = w' & clouds .~ catMaybes mclouds
  where
    (w',mclouds) = mapAccumR updateCloud w (_clouds w)
-- did i write a mapMaybeAccumR ?

updateCloud :: World -> Cloud -> (World,Maybe Cloud)
updateCloud w c
    | _clTimer c < 1 = (w, Nothing)
    | otherwise = (_clEffect c c w, Just $ c
        & clPos .~ finalPos
        & clVel .~ finalVel
        & clTimer -~ 1
        )
  where
    newVel@(V3 _ _ nvz) = (0.95 *.*.* springVels) +.+.+ V3 0 0 (0.001 * vertVel)
    newVel2 = stripZ newVel
    vertVel = _clAlt c - opz
    springVels = foldl' (clClSpringVel c) (_clVel c) (cloudsNearPoint oldPos2 w)
    oldPos@(V3 _ _ opz) = _clPos c
    oldPos2 = stripZ oldPos
    newPos@(V3 _ _ npz) = oldPos +.+.+ newVel
    newPos2 = stripZ newPos
    -- the following only tests for the first collision with a wall
    hitWl = collidePointAnyWalls oldPos2 newPos2 $ wallsNearPoint newPos2 w
    finalPos = addZ npz $ maybe newPos2 fst hitWl
    finalVel = addZ nvz $ maybe newVel2 snd hitWl

--updateCloud :: World -> Cloud -> Maybe Cloud
--updateCloud w c
--    | _clTimer c < 1 = Nothing
--    | otherwise = Just $ c 
--        & clPos .~ finalPos
--        & clVel .~ finalVel
--        & clTimer -~ 1
--  where
--    newVel = 0.95 *.* springVels
--    springVels = foldl' (clClSpringVel c) (_clVel c) (cloudsNearPoint oldPos w)
--    oldPos = _clPos c
--    newPos = oldPos +.+ newVel
--    hitWl = collideCircWalls' oldPos newPos 5 $ wallsNearPoint newPos w
--    finalPos = maybe newPos fst hitWl
--    finalVel = maybe newVel snd hitWl

clClSpringVel :: Cloud -> Point3 -> Cloud -> Point3
clClSpringVel a v b
    | dist3 pa pb < radDist = v +.+.+ 0.1 *.*.* normalizeV3 (pa -.-.- pb)
    | otherwise = v
  where 
    pa = _clPos a
    pb = _clPos b
    radDist = (_clRad a + _clRad b) / 2

simpleCrSprings :: World -> World
simpleCrSprings w = IM.foldl' (flip crSpring) w $ _creatures w

crSpring :: Creature -> World -> World
crSpring c w = IM.foldl' (flip $ crCrSpring c) w cs
  where 
    cs = creaturesNearPoint (_crPos c) w

crCrSpring :: Creature -> Creature -> World -> World
crCrSpring c1 c2 w 
    | id1 == id2 = w
    | vec == V2 0 0 = w
    | diff >= comRad = w
    | otherwise = over (creatures . ix id1 . crPos) (+.+ overlap1)
                        $ over (creatures . ix id2 . crPos) (-.- overlap2) w
  where 
    id1  = _crID c1
    id2  = _crID c2
    vec  = _crPos c1 -.- _crPos c2
    diff = magV vec
    comRad  = _crRad c1 + _crRad c2
    overlap1 = ((comRad - diff) * _crMass c2 * 0.5 / massT) *.* errorNormalizeV 55 vec
    overlap2 = ((comRad - diff) * _crMass c1 * 0.5 / massT) *.* errorNormalizeV 56 vec
    massT    = _crMass c1 + _crMass c2

ifConfigWallRotate :: World -> World
ifConfigWallRotate w
    | _rotate_to_wall $ _config w = rotateToOverlappingWall w
    | otherwise = w

rotateToOverlappingWall :: World -> World
rotateToOverlappingWall w = case theWall of
    Nothing -> w
    Just wl -> rotateUsing ( (argV . uncurry (-.-) $ _wlLine wl) - camrot)
  where
    camrot = _cameraRot w
    rotateUsing a
        | b - b' > 0.01 = w & cameraRot +~ 0.01
        | b - b' < negate 0.01 = w & cameraRot -~ 0.01
        | otherwise    = w
      where
        b = a * (2 / pi)
        b' = fromIntegral (round b :: Int)
    cr = you w
    p = _crPos cr
    theWall = overlapCircWallsReturnWall p (_crRad cr + 5) (wallsNearPoint p w)

{-
Finds the IDs of visible walls from a point to another point.  -}
visibleWalls :: Point2 -> Point2 -> IM.IntMap Wall -> [Int]
visibleWalls p1 p2 ws 
    = map fst 
    . takeUntil (_wlIsSeeThrough . snd) 
    . map snd 
    . sortOn (dist p1 . fromJust . fst) 
    . filter ((/=) Nothing . fst) 
    . map f 
    $ IM.toList ws
  where 
    f (i,wl) = (uncurry intersectSegSeg (_wlLine wl) p1 p2, (i,wl))
