{-# LANGUAGE BangPatterns #-}
{- |
Module      : Dodge.Update
Description : Simulation update
-}
module Dodge.Update ( updateUniverse ) where
import Dodge.Data
import Dodge.Menu
import Dodge.CullBox
--import Dodge.Block
import Dodge.Distortion
import Dodge.SoundLogic
import Dodge.Wall.Delete
import Dodge.Update.WallDamage
--import Dodge.Menu
import Dodge.Base
import Dodge.Zone
import Dodge.Hammer
import Dodge.WallCreatureCollisions
import Dodge.Update.Camera
import Dodge.Inventory
import Sound.Data
import Geometry
import LensHelp
import FoldableHelp

--import qualified Data.Text as T
--import System.Random
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.Applicative
--import qualified Data.Set as S
--import Data.Monoid
--import System.Random
import qualified Streaming.Prelude as S
import SDL
--import Streaming

{- For most menus the only way to change the world is using event handling.  -}
updateUniverse :: Universe -> Universe
updateUniverse u = case _menuLayers u of
    (WaitScreen s i : _) 
        | i < 1     -> u & over uvWorld (dbArg _worldEvents)
        | otherwise -> u & menuLayers %~ ( (WaitScreen s (i-1) :) . tail )
    (OptionScreen {_scOptionFlag = GameOverOptions} : _) -> u & uvWorld %~
        ( updateParticles 
        . updateIMl _props _pjUpdate 
        . updateLightSources
        . updateClouds )
    (_ : _) -> u
    []  -> over uvWorld (functionalUpdate (_config u)) u 

{- | The update step. -}
functionalUpdate :: Configuration -> World -> World
functionalUpdate cfig w = checkEndGame 
--    . updateRandGen
    . (mouseButtons . each .~ True)  -- to determine if the mouse button is held
    . (worldClock +~ 1)
    . updateWorldSelect
    . doRewind
    . (hammers . each %~ moveHammerUp)
    . updateDistortions
    . updateCreatureSoundPositions
    . ppEvents
    . updateCamera cfig
    . colCrsWalls cfig
    . simpleCrSprings 
    . zoneCreatures 
    . updateIMl _doors _drMech
    . updateDelayedEvents
    . resetWorldEvents
    . dbArg _worldEvents
    . updateIMl _modifications _mdUpdate
    . updateParticles 
    . updateBeams
    . updateIMl _props _pjUpdate  
    . updateLightSources
    . updateClouds
    . updateGusts
    . zoneClouds
    . updateMIM magnets _mgUpdate
    . updateIMl' _terminals tmUpdate
    . updateIMl _machines mcChooseUpdate
    . updateIMl _creatures _crUpdate 
        -- creatures should be updated early so that crOldPos is set before any position change
    . over creatures (fmap setOldPos)
    . updateCreatureGroups
--    . updateBlocks
    . updateWallDamages
    . updateSeenWalls 
--    . (youHammerPosition %~ moveHammerUp)
    . updateTerminal
    . updateRBList
    . updateBounds cfig  -- where should this go? next to update camera?
    $ updateCloseObjects w

zoneClouds :: World -> World
zoneClouds w = w 
    & clZoning . znObjects .~ mempty
    & clZoning %~ \zn ->
        foldl' (flip $ updateZoning (:)) zn (_clouds w)
    --runIdentity (S.fold_ (flip $ updateZoning (:)) (zn & znObjects .~ mempty) id (_clouds w))

updateWorldSelect :: World -> World
updateWorldSelect w = f . g $ case (w ^? mouseButtons . ix ButtonLeft, w ^? mouseButtons . ix ButtonRight) of
    (Nothing   ,Nothing)  -> w
    (Just False,Nothing)  -> w & lLine . _1 .~ mwp
    (Just True ,Nothing)  -> w & lLine . _2 .~ mwp
    (Nothing   ,_)        -> w
    (Just False,_)        -> w & rLine . _1 .~ mwp
    (Just True ,_)        -> w & rLine . _2 .~ mwp
  where
    mwp = mouseWorldPos w
    f   | ButtonLeft `M.member` _mouseButtons w = lSelect .~ mwp
        | otherwise = id
    g   | ButtonRight `M.member` _mouseButtons w = rSelect .~ mwp
        | otherwise = id

mcChooseUpdate :: Machine -> Machine -> World -> World
mcChooseUpdate mc mc'
    | _mcHP mc > 0  = _mcUpdate mc mc'
    | otherwise     = (machines %~ IM.delete (_mcID mc))
        . deleteWallIDs (_mcWallIDs mc)
        . _mcDeath mc mc'

tmUpdate :: Terminal -> World -> World
tmUpdate tm w = case w ^? terminals . ix (_tmID tm) . tmFutureLines . ix 0 of
    Nothing -> w
    Just tl | _tlPause tl > 0 -> w & pointTermParams . tmFutureLines . ix 0 . tlPause -~ 1
    Just (TerminalLineDisplay _ f) -> w & pointTermParams %~
        ( ( tmFutureLines %~ tail )
        . ( tmDisplayedLines .:~ f w )
        )
    Just (TerminalLineEffect _ eff) -> w
        & pointTermParams . tmFutureLines %~ tail
        & eff tm
    Just (TerminalLineTerminalEffect _ eff) -> w
        & pointTermParams . tmFutureLines %~ tail
        & pointTermParams %~ eff
  where
    pointTermParams = terminals . ix (_tmID tm)

setOldPos :: Creature -> Creature
setOldPos cr = cr
    & crOldPos .~ _crPos cr
    & crOldDir .~ _crDir cr

-- hack
--updateRandGen :: World -> World
--updateRandGen = randGen %~ (snd . (uniform :: StdGen -> (Int,StdGen)))

doRewind :: World -> World
doRewind w = case _maybeWorld w of
    Just' w' -> w' & timeFlow .~ RewindingLastFrame
    Nothing' -> w  & timeFlow .~ NormalTimeFlow

zoneCreatures :: World -> World
zoneCreatures w = w 
    & crZoning . znObjects .~ mempty
    & crZoning %~ \zn ->
        foldl' (flip $ updateZoning (\cr -> IM.insert (_crID cr) cr)) 
            zn (_creatures w)

updateCreatureSoundPositions :: World -> World
updateCreatureSoundPositions w = M.foldlWithKey' insertSound w 
    . M.mapMaybeWithKey updateSound 
    $ _playingSounds w
  where
    insertSound w' k s = w' & 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

--updateIMr :: (World -> IM.IntMap a) -> (a -> a -> World -> World) -> World -> World
--updateIMr fim fup w = foldr (dbArg fup) w (fim w)
updateIMl :: (World -> IM.IntMap a) -> (a -> a -> World -> World) -> World -> World
updateIMl fim fup w = foldl' (flip $ dbArg fup) w (fim w)

updateIMl' :: (World -> IM.IntMap a) -> (a -> World -> World) -> World -> World
updateIMl' fim fup w = foldl' (flip fup) w (fim 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)

{- 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
updateBeams :: World -> World
updateBeams w = w
    & newBeams .~ WorldBeams [] [] [] []
    & beams    .~ thebeams
    & combineBeams thebeams
  where
    thebeams = _newBeams w

combineBeams :: WorldBeams -> World -> World
combineBeams wbeams w = w'' 
    & beams . positronBeams .~ pbeams
    & beams . electronBeams .~ ebeams
  where
    (w',pbeams) = mapAccumR (combineBeamBeams (_electronBeams wbeams)) w $ _positronBeams wbeams
    (w'',ebeams) = mapAccumR (combineBeamBeams (_positronBeams wbeams)) w' $ _electronBeams wbeams

combineBeamBeams :: [Beam] -> World -> Beam -> (World,Beam)
combineBeamBeams bms w bm = case intersectBeamBeams bm bms of
    (ps,Nothing) -> (w, bm & bmFirstPoints .~ ps)
    (ps,Just a)  -> (_beamCombine (_bmType bm) a w, bm & bmFirstPoints .~ ps)

-- intersect a beam with a list of beams.
-- returns the (reversed) travel of the beam up to maybe an intersection point
intersectBeamBeams :: Beam -> [Beam] 
    -> ([Point2], Maybe (Point2,(Point2,Point2,Beam),(Point2,Point2,Beam)))
intersectBeamBeams bm bms = f [] $ _bmPoints bm
  where
    f ps (x:y:ys) = case intersectSegBeams x y bms of
        Just (z,a) -> (z:x:ps, Just (z,(x,y,bm),a))
        Nothing -> f (x:ps) (y:ys)
    f ps [x] = (x:ps, Nothing)
    f _ _ = error "made an empty beam"

intersectSegBeams :: Point2 -> Point2 -> [Beam] -> Maybe (Point2,(Point2,Point2,Beam))
intersectSegBeams sp ep (bm:bms) = case intersectSegBeam sp ep bm of
    Nothing -> intersectSegBeams sp ep bms
    --Just a@(z,_) -> maybe (Just a) Just $ intersectSegBeams sp z bms
    Just a@(z,_) -> intersectSegBeams sp z bms <|> Just a
intersectSegBeams _ _ _ = Nothing

intersectSegBeam :: Point2 -> Point2 -> Beam -> Maybe (Point2,(Point2,Point2,Beam))
intersectSegBeam sp ep bm = case intersectSegSegs' sp ep (_bmPoints bm) of
    Nothing -> Nothing
    Just (z,x,y) -> Just (z,(x,y,bm))

intersectSegSegs' :: Point2 -> Point2 -> [Point2] -> Maybe (Point2,Point2,Point2)
intersectSegSegs' sp ep (x:y:ys) = case intersectSegSeg sp ep x y of
    --Just z  -> maybe (Just (z,x,y)) Just $ intersectSegSegs' sp z (y:ys)
    Just z  -> intersectSegSegs' sp z (y:ys) <|> Just (z,x,y)
    Nothing -> intersectSegSegs' sp ep (y:ys)
intersectSegSegs' _ _ _ = Nothing

--intersectSegSegs :: Point2 -> Point2 -> [Point2] -> Maybe Point2
--intersectSegSegs sp ep (x:y:ys) = case intersectSegSeg sp ep x y of
--    Just z  -> maybe (Just z) Just $ intersectSegSegs sp z (y:ys)
--    Nothing -> intersectSegSegs sp ep (y:ys)
--intersectSegSegs _ _ _ = Nothing
--
--intersectSegSegss :: Point2 -> Point2 -> [[Point2]] -> Maybe Point2
--intersectSegSegss sp ep (as:ass) = case intersectSegSegs sp ep as of
--    Just z  -> maybe (Just z) Just $ intersectSegSegss sp z ass
--    Nothing -> intersectSegSegss sp ep ass
--intersectSegSegss _ _ _ = Nothing
--
--intersectSegsSegss :: [Point2] -> [[Point2]] -> Maybe Point2
--intersectSegsSegss (x:y:ys) ass = maybe 
--    (intersectSegsSegss (y:ys) ass)
--    Just
--    (intersectSegSegss x y ass)
--intersectSegsSegss _ _ = Nothing

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'

updateMIM :: ASetter' World (IM.IntMap a) -> (a -> a -> Maybe a) -> World -> World
updateMIM f up = f %~ IM.mapMaybe (dbArg up)

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

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

updateSeenWalls :: World -> World
updateSeenWalls w = runIdentity $ S.fold_ markWallSeen w id (S.map (_wlID . snd) $ allVisibleWalls w)
--  where 
--    f w' !i -- = w' & walls . ix i . wlSeen .~ True 
--        = w' { _walls = IM.adjust mw i $ _walls w' }
--    mw wl = wl {_wlSeen = True}

markWallSeen :: World -> Int -> World
markWallSeen !w !i = w { _walls = IM.adjust markSeen i $ _walls w }

markSeen :: Wall -> Wall
markSeen wl = wl {_wlSeen = True}

checkEndGame :: World -> World
checkEndGame w = case _deathDelay w of
    Just x | x < 0 -> w & sideEffects %~ ( . (menuLayers .~ [gameOverMenu]))
        & deathDelay .~ Nothing
    Just _ -> w & deathDelay . _Just -~ 1
    _ | _crHP (you w) < 1 -> w & deathDelay ?~ 50
    _ -> w

updateGusts :: World -> World
updateGusts w = w
    & gusts %~ IM.mapMaybe (mvGust w)

mvGust :: World -> Gust -> Maybe Gust
mvGust _ gu
    | _guTime gu < 0 = Nothing
    | otherwise      = Just $ gu
        & guPos .+.+~ _guVel gu
        & guTime -~ 1
updateClouds :: World -> World
updateClouds w = w' & clouds .~ catMaybes mclouds
  where
--    cls = _clouds w
    (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 = runIdentity $ S.fold_ (clClSpringVel c) (_clVel c) id (clsNearPoint oldPos2 w)
    oldPos@(V3 _ _ opz) = _clPos c
    oldPos2 = stripZ oldPos
    newPos@(V3 _ _ npz) = oldPos +.+.+ newVel
    newPos2 = stripZ newPos
    hitWl = bouncePoint (const True) 1 oldPos2 newPos2 w
    finalPos = addZ (min 74 npz) $ maybe newPos2 fst hitWl
        -- allowing clouds at/above height 75 causes graphical glitches 22.05.23
    finalVel = addZ nvz $ maybe newVel2 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

-- note that this may in rare cases not push creatures away from each other
crSpring :: Creature -> World -> World
crSpring c w = runIdentity $ S.fold_ (flip $ crCrSpring c) w id cs
  where 
    cs = crsNearPoint (_crPos c) w

crCrSpring :: Creature -> Creature -> World -> World
crCrSpring c1 c2
    | id1 == id2    = id
    | vec == V2 0 0 = id
    | diff >= comRad = id
    | otherwise = creatures %~
        ( over (ix id1 . crPos) (+.+ overlap1)
        . over (ix id2 . crPos) (-.- overlap2)
        )
  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

updateDelayedEvents :: World -> World
updateDelayedEvents w = let (neww,newde) = mapAccumR f w (_delayedEvents w)
    in neww & delayedEvents .~ catMaybes newde
  where
    f w' (i,g) 
        | i <= 0    = (g w', Nothing)
        | otherwise = (w', Just (i-1,g))
