{- |
Module      : Dodge.Update
Description : Simulation update
-}
module Dodge.Update (update) where
import Dodge.Data
import Dodge.Block
import Dodge.Distortion
import Dodge.SoundLogic
import Dodge.Menu
import Dodge.Base
import Dodge.Zone
import Dodge.WallCreatureCollisions
import Dodge.Update.Camera
import Dodge.Inventory
import Sound.Data
import Geometry
import Geometry.Vector3D

import Data.List
import Data.Maybe
import Data.Function
import qualified Data.IntMap.Strict as IM
import qualified Data.Map.Strict as M
import Control.Lens
import Data.Monoid
import System.Random

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
        w
    (_ : _) -> w
    []  -> checkEndGame 
        . updateDistortions
        . updateCreatureSoundPositions
        . ppEvents 
        . updateCamera
        . colCrsWalls
--        . ifConfigWallRotate
        . simpleCrSprings 
        . zoneCreatures 
        . updateDoors
        . resetWorldEvents
        . dbArg _worldEvents
        . updateModifications
        . updateParticles 
        . updateProjectiles 
        . updateLightSources
        . updateClouds
        . zoneClouds
        . updateMachines
        . updateCreatures 
        . updateCreatureGroups
        . updateBlocks
        . updateSeenWalls 
        $ updateCloseObjects w
  where 
    --updatedLightSources = mapMaybe (\b -> _tlsUpdate b w b) $ _tempLightSources w
    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

updateCreatureSoundPositions :: World -> World
updateCreatureSoundPositions w = M.foldrWithKey insertSound w 
    . M.mapMaybeWithKey updateSound 
    $ _playingSounds w
  where
    insertSound k s = toPlaySounds %~ M.insertWith (const id) k s
    updateSound (CrMouth cid) s = case w ^? creatures . ix cid . crPos of
        Just p  -> Just s {_soundPos = p, _soundAngDist = Just (soundAngle p w,0)}
        Nothing -> Just s {_soundTime = Just 0}
    updateSound _ _ = Nothing

updateModifications :: World -> World
updateModifications w = foldr (dbArg _mdUpdate) w (_modifications w)

updateBlocks :: World -> World
updateBlocks w = foldr f w $ _blocks w
  where
    f bl w' = case _blHPs bl of
        (x:_:_) | x < 1 -> splinterBlock bl w'
        [x]     | x < 1 -> destroyBlock bl w'
        _ -> w'

-- | Note the explict use of record syntax. Using lens created 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)

updateDistortions :: World -> World
updateDistortions = distortions %~ mapMaybe updateDistortion

updateLightSources :: World -> World
updateLightSources w = over tempLightSources f w
  where 
    f = mapMaybe (\b -> _tlsUpdate b w b)

updateProjectiles :: World -> World
updateProjectiles w = IM.foldl' (flip $ dbArg _pjUpdate) w $ _props w

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

updateInstantParticles :: World -> World
updateInstantParticles w = case _instantParticles w of
    [] -> w
    ps -> let (w',ps') = mapAccumR (\a b -> _ptUpdate b a b) (w {_instantParticles=[]}) ps
        in updateInstantParticles $ w' & particles %~ (catMaybes ps' ++)

updateMachines :: World -> World
updateMachines w = foldr f w (_machines w)
  where
    f mc = _mcUpdate mc mc

-- 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

updateDoors :: World -> World
updateDoors w = IM.foldl' (\w' dr -> _drMech dr dr w') w (_doors w)

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

updateSeenWalls :: World -> World
updateSeenWalls w = foldl' markWallSeen w wallsToUpdate
  where 
    vPos = _cameraViewFrom w
    wallsToUpdate = concatMap (\p -> visibleWalls vPos (vPos +.+p) $ wallsAlongLine vPos (vPos +.+ p) w)
                             $ nRays 20

markWallSeen :: World -> Wall -> World
markWallSeen w wl = w { _walls = IM.adjust mw (_wlID wl) $ _walls w }
  where
    mw wl' = wl' {_wlSeen = True}

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

updateClouds :: World -> World
updateClouds w = w' & clouds .~ catMaybes mclouds
  where
    (w',mclouds) = mapAccumR updateCloud w (_clouds w)

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 = collidePointAnyWallsReflect 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
        ( over (ix id1 . crPos) (+.+ overlap1)
        . over (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

{- Finds the visible walls from a point to another point.  -}
visibleWalls :: Point2 -> Point2 -> IM.IntMap Wall -> [Wall]
visibleWalls p1 p2 ws 
    = takeUntil theTest
    . map snd 
    . sortOn (dist p1 . fromJust . fst) 
    . filter (isJust . fst) 
    . map f 
    $ IM.elems ws
  where 
    f wl = (uncurry intersectSegSeg (_wlLine wl) p1 p2, wl)
    theTest wl = _wlOpacity wl /= Opaque
