{- | Creation of particles in the world.  -}
module Dodge.WorldEvent.SpawnParticle
    ( makeGasCloud
    , aFlameParticle
    , makeFlamelet
    , aStaticBall
    , incBall
    , concBall
    ) where
import Dodge.Data
import Dodge.Zone
import Dodge.Particle.HitEffect.ExpireAndDamage
import Dodge.Particle.Damage
--import Dodge.WorldEvent.HitEffect
import Dodge.WorldEvent.ThingsHit
import Dodge.WorldEvent.Cloud
import Dodge.WorldEvent.Flash
import Dodge.WorldEvent.Shockwave
import Dodge.SoundLogic
import RandomHelp
import Picture
import Geometry
import qualified IntMapHelp as IM
import LensHelp

import Data.Foldable
import Data.Tuple
import StreamingHelp
import qualified Streaming.Prelude as S
aFlameParticle 
    :: Int      -- ^ Timer
    -> Point2   -- ^ Position
    -> Point2   -- ^ Velocity
    -> Maybe Int -- ^ Creature id
    -> Particle
aFlameParticle t pos vel maycid = PtZ
    { _ptDraw       = drawFlame vel
    , _ptUpdate     = moveFlame vel
    , _ptVel        = vel
    , _ptColor      = red
    , _ptPos        = pos
    , _ptCrIgnore   = maycid
    , _ptWidth      = 4
    , _ptTimer      = t
    , _ptHitEff     = expireAndDamage $ simpleDam FLAMING 20
    , _ptZ           = 20
    }
drawFlame
    :: Point2 -- ^ Rotate direction
    -> Particle 
    -> Picture
drawFlame rotd pt = pictures
    [ glow 
    , aPic BloomNoZWrite prot2 25 (V2 (scaleChange + 1)   2  ) $ V4 2 (-1) (-1) 0.5
    , aPic BloomNoZWrite prot  22 (V2 (scaleChange + 0.5) 1  ) $ V4 1 0.5    0  2
    , aPic BloomLayer prot3 20 (V2  scaleChange        0.5) $ V4 1 1      1  1
    ]
  where
    ep = _ptPos pt
    aPic :: Layer -> (Point2 -> Point2) -> Float -> Point2 -> Color -> Picture
    aPic lay offset depth (V2 scalex scaley) col
        = setLayer lay 
        . setDepth depth 
        . uncurryV translate (offset ep)
        . rotate (pi * 0.5 + argV rotd)
        . scale scalex scaley
        . color col                 
        $ circleSolid 5
    glow = setLayer BloomNoZWrite $ setDepth 0.3 $ uncurryV translate ep
        $ circleSolidCol (withAlpha 0 orange) (withAlpha 0.02 orange) 50
    time = _ptTimer pt
    scaleChange 
        | time < 80 = 3
        | otherwise = 3 - (fromIntegral time - 80) * 0.2
    prot  p' = p' +.+ rotateV (fromIntegral time) (V2 0 1)
    prot2 p' = p' +.+ rotateV (negate $ fromIntegral time) (V2 0 1)
    prot3 p' = p' +.+ rotateV (2 + fromIntegral time * 0.1) (V2 0 2)
{- TODO: add generalised area damage particles/hiteffects.  -}
moveFlame :: Point2   -- ^ Rotation direction
    -> World 
    -> Particle
    -> (World, Maybe Particle)
moveFlame rotd w pt 
    | time <= 0 = (makeFlamerSmokeAt (addZ 20 ep) w, Nothing)
    | otherwise = case runIdentity . S.head_ $ thingsHitExceptCr (_ptCrIgnore pt) sp ep w of
        Just (_,Left _)     -> (doSound $ dodamage w             , mvPt' 0.7)
        Just (p,Right wl)   -> (doSound $ dodamage w  , rfl wl p)
        _                   -> (ptFlicker pt $ doSound $ dodamage w    , mvPt' 0.98)
  where 
    time = _ptTimer pt
    doSound = soundContinue Flame (V2 x y) fireLoudS (Just 2)
    sp@(V2 x y) = _ptPos pt
    vel = _ptVel pt
    ep = sp +.+ vel
    mvPt' speed = Just $ pt 
        { _ptTimer = time - 1
        , _ptPos = ep
        , _ptCrIgnore = Nothing
        , _ptVel = speed *.* vel    }
    dodamage = damageInArea closeCrs closeWls pt
    closeWls wl = uncurry segOnCirc (_wlLine wl) ep 5
    closeCrs cr = dist ep (_crPos cr) 
                    < _crRad cr + 10 - min 9 (max 0 (fromIntegral time - 80))
                                + 5 * angleCoeff (angleVV (ep -.- _crPos cr) rotd)
    angleCoeff x' = abs $ 1 - abs ( (x' * 2 - pi) / pi )
    rfl wl p = Just $ pt 
        { _ptTimer = time - 1
        , _ptPos = pOut p
        , _ptVel = reflV wl
        }
    pOut p = p +.+ squashNormalizeV (sp -.- p)
    reflV wall = (0.3 *.* reflectIn (uncurry (-.-) . swap $ _wlLine wall) vel )
        +.+ (0.2 *.* vel)


concBall :: Point2 -> State g Particle
concBall p = return Shockwave
    { _ptDraw      = drawShockwave
    , _ptUpdate    = mvShockwave []
    , _ptColor     = white
    , _ptPos       = p
    , _ptRad       = 15
    , _ptDam       = 4
    , _ptPush      = 1
    , _ptMaxTime   = 10
    , _ptTimer     = 10
    }


aStaticBall :: Point2 -> State g Particle
aStaticBall p = return PtZ 
    { _ptDraw       = drawStaticBall
    , _ptUpdate     = moveStaticBall
    , _ptVel        = 0
    , _ptColor      = blue
    , _ptPos        = p
    , _ptCrIgnore   = Nothing
    , _ptWidth      = 3
    , _ptTimer      = 20
    , _ptHitEff     = expireAndDamage $ simpleDam ELECTRICAL 20
    , _ptZ = 20
    }

makeFlamelet
    :: Point2       -- ^ Position
    -> Float        -- ^ z position
    -> Point2       -- ^ Velocity
    -> Maybe Int    -- ^ Ignore creature id
    -> Float        -- ^ Size
    -> Int          -- ^ Timer
    -> World 
    -> World
makeFlamelet (V2 x y) z vel maycid size time w = w
    & randGen   .~ g
    & instantParticles .:~ PtZ 
        { _ptDraw       = drawFlameletZ rot
        , _ptUpdate     = moveFlamelet
        , _ptVel        = vel
        , _ptColor      = red
        , _ptPos        = V2 x y
        , _ptCrIgnore   = maycid
        , _ptWidth      = size
        , _ptTimer      = time
        , _ptHitEff     = expireAndDamage $ simpleDam FLAMING 20
        , _ptZ = z
        }
  where
    (rot ,g) = randomR (0,3) $ _randGen w

--damageBothTypeAmount :: DamageType
--    -> Int
--    -> Particle 
--    -> [(Point2, Either Creature Wall)] 
--    -> World 
--    -> (World, Maybe Particle)
--damageBothTypeAmount dt amount = destroyOnImpact
--    (\pt p cr -> creatures . ix (_crID cr) . crState . crDamage .:~ thedam pt p)
--    (\pt p -> damageWall (thedam pt p))
--  where
--    thedam pt p = Damage dt amount sp p ep NoDamageEffect
--      where
--        sp = _ptPos pt
--        ep = sp +.+ _ptVel pt

drawStaticBall
    :: Particle
    -> Picture
drawStaticBall pt = setLayer BloomNoZWrite
    . setDepth (_ptZ pt + 20) 
    . uncurryV translate (_ptPos pt)
    $ circleSolidCol (_ptColor pt) (withAlpha 0.5 white) (_ptWidth pt+5)


moveStaticBall :: World -> Particle -> (World, Maybe Particle)
moveStaticBall = moveFlamelet

-- | Note damgeInRadius by itself never destroys the particle
damageInRadius :: Float -> Particle -> World -> World
damageInRadius size pt = damageInArea isClose closeWls pt
  where
    p = _ptPos pt
    closeWls wl = uncurry segOnCirc (_wlLine wl) p size
    isClose cr = dist p (_crPos cr) < _crRad cr + size

damageInArea :: (Creature -> Bool) -> (Wall -> Bool) -> Particle -> World -> World
{-# INLINE damageInArea #-}
damageInArea crt wlt pt w = damwls damcrs
  where
    p = _ptPos pt
    damcrs = foldl' (flip $ \cr -> fst . hiteff (S.yield (p,Left cr))) w $ IM.filter crt $ _creatures w
    damwls w' = runIdentity
        . S.fold_ 
            (flip $ \wl -> fst . hiteff (S.yield (p,Right wl))) 
            w' 
            id
        . S.filter wlt
        $ wlsNearPoint p w'
    hiteff = _ptHitEff pt pt

-- | At writing the radius is half the size of the effect area
makeGasCloud 
    :: Point2   -- ^ Position
    -> Point2   -- ^ Velocity
    -> World 
    -> World
makeGasCloud pos vel w = w
    & clouds    .:~ theCloud
--    & clouds    %~ S.cons theCloud
    & randGen   .~ g
  where 
    theCloud = Cloud 
        {  _clPos = addZ 20 pos
        , _clVel = addZ 0 vel
        , _clPict = \_ -> setLayer MidLayer . setDepth 30 $ color (withAlpha 0.05 col) 
                                          $ circleSolid 20
        , _clRad = 10
        , _clAlt = 25
        , _clTimer = 400
        , _clType = GasCloud
        , _clEffect = cloudPoisonDamage
        }
    (col, g) = runState (takeOne [green,yellow]) $ _randGen w
{- Attach poison cloud damage to creatures near cloud.  -}
cloudPoisonDamage :: Cloud -> World -> World
cloudPoisonDamage c w = w & dodamagesto (S.filter f $ crsNearPoint clpos w)
  where 
    dodamagesto :: StreamOf Creature -> World -> World
    dodamagesto scrs mcrs = runIdentity $ S.fold_ (flip doDam) mcrs id scrs
    doDam cr = creatures . ix (_crID cr) . crState . csDamage 
        .:~ Damage POISONDAM 1 clpos clpos clpos NoDamageEffect
    f cr = dist3 (addZ 20 $ _crPos cr) (_clPos c) < _crRad cr + _clRad c + 10
    clpos = stripZ $ _clPos c

incBall :: RandomGen g => Point2 -> State g Particle
incBall p = do
    rot <- state $ randomR (0,3)
    return PtZ 
        { _ptDraw       = drawFlameletZ rot
        , _ptUpdate     = moveFlamelet
        , _ptVel        = 0
        , _ptColor      = red
        , _ptPos        = p
        , _ptCrIgnore   = Nothing
        , _ptWidth      = 3
        , _ptTimer      = 20
        , _ptHitEff     = expireAndDamage $ simpleDam FLAMING 20
        , _ptZ          = 20
        }
drawFlameletZ
    :: Float    -- ^ Rotation
    -> Particle
    -> Picture
drawFlameletZ rot pt = pictures 
    [ setLayer BloomLayer pic 
    , setLayer BloomNoZWrite piu 
    , setLayer BloomNoZWrite pi2 
    ]
  where
    z = _ptZ pt
    sp = _ptPos pt
    vel = _ptVel pt
    ep = sp +.+ vel
    size = _ptWidth pt
    siz2 = size + 0.2
    time = _ptTimer pt
    piu = setDepth (z + 25)
        .  uncurryV translate ep 
        . color (mixColors (fromIntegral (max 0 (time-2))) (10-fromIntegral time) 
                           (V4 2 0 0 1)                          (V4 2 0 0 0)
                   )
        . rotate (negate (rot - 0.1 * fromIntegral time))
        . scale s1 s1
        $ polygon (rectNSWE siz2 (-siz2) (-siz2) siz2)
    pi2 = setDepth (z + 25)
        . uncurryV translate ep
        . color (mixColors (fromIntegral (max 0 (time-2))) (10-fromIntegral time) 
                           (V4 2 1 0 0.5)                          (V4 0 0 0 0)
                   )
        . rotate (negate (rot + 0.2 * fromIntegral time))
        . scale s2 s2
        $ polygon (rectNSWE siz2 (-siz2) (-siz2) siz2)
    pic = setDepth (z + 20)
        . uncurryV translate ep
        . color (mixColors (fromIntegral (max 0 (time-2))) (10-fromIntegral time) 
                           (V4 1 1 1 3)                           (V4 1 0 0 1)
                )
        . rotate (negate ( 0.1 * fromIntegral time + rot))
        . scale (0.5* sc) (0.5 *sc)
        $ polygon $ map toV2 [(-size,-size),(size,-size),(size,size),(-size,size)]
    sc = (*) 2 $ log $ 1 + fromIntegral time / 20
    s1 = (*) 2 $ log $ 2 + fromIntegral time / 40
    s2 = 0.5 * (sc + s1)

{- Update of a flamelet.
Applies movement and attaches damage to nearby creatures.  -}
-- This should be unified in many ways with moveFlame
moveFlamelet :: World -> Particle -> (World, Maybe Particle)
moveFlamelet w pt 
    | _ptTimer pt <= 0 = (     w, Nothing)
    | otherwise        = (ptFlicker pt $ damageInRadius 5 mvPt' w,  Just mvPt')
  where   
    mvPt' = pt 
        & ptTimer    -~ 1
        & ptPos      .~ _ptPos pt +.+ _ptVel pt
        & ptCrIgnore .~ Nothing
        & ptVel      .*.*~ 0.8
