{- |
Creation of particles in the world.
 -}
module Dodge.WorldEvent.SpawnParticle
  where
import Dodge.Data
import Dodge.Data.DamageType
import Dodge.Data.SoundOrigin
import Dodge.Default
import Dodge.Creature.State.Data
import Dodge.Base
import Dodge.Zone
import Dodge.Base.Collide
import Dodge.Picture
--import Dodge.Picture.Layer
import Dodge.WorldEvent.HitEffect
import Dodge.WorldEvent.ThingsHit
import Dodge.WorldEvent.Cloud
--import Dodge.WorldEvent.Flash
import Dodge.Particle.Bullet.Draw
import Dodge.Particle.Bullet.Update
import Dodge.SoundLogic
import Dodge.SoundLogic.LoadSound
import Dodge.RandomHelp
--import Dodge.Debug
import Picture
import Geometry
import Geometry.Vector3D
--import Geometry.Data
import qualified IntMapHelp as IM

import Control.Lens
import System.Random
import Control.Monad.State
import Data.Function (on)
import Data.List
import Data.Maybe
import Data.Tuple
aFlameParticle 
    :: Int      -- ^ Timer
    -> Point2   -- ^ Position
    -> Point2   -- ^ Velocity
    -> Maybe Int -- ^ Creature id
    -> Particle
aFlameParticle t pos vel maycid = PtZ
    { _ptDraw         = drawFlame vel
    , _ptUpdate'      = moveFlame vel
    , _btVel'         = vel
    , _btColor'       = red
    , _btPos'         = pos
    , _btPassThrough' = maycid
    , _btWidth'       = 4
    , _btTimer'       = t
    , _btHitEffect'   = destroyOnImpact (doFlameDam 1) noEff noEff
    , _ptZ            = 20
    }
drawFlame 
    :: Point2 -- ^ Rotate direction
    -> Particle 
    -> Picture
drawFlame rotd pt = thePic
  where
    ep = _btPos' pt
    thePic = pictures 
        [ glow 
        , aPic 3 prot2 25 (V2 (scaleChange + 1)   2  ) $ V4 2 (-1)  (-1) 0.5
        , aPic 3 prot  22 (V2 (scaleChange + 0.5) 1) $ V4 1 0.5 0 2
        , aPic 1 prot3 20 (V2  scaleChange        0.5  ) $ V4 1 1   1 1
        ]
    aPic :: Int -> (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 1 $ setDepth 0.3 $ uncurryV translate ep
        $ circleSolidCol (withAlpha 0 orange) (withAlpha 0.02 orange) 50
    time = _btTimer' 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 = (smokeGen w, Nothing)
    | otherwise = case thingsHitExceptCr (_btPassThrough' pt) sp ep w of
        ((_,E3x1 _):_)       -> (doSound damcrs        , mvPt')
        (thing@(p,E3x2 wl):_) -> (fst $ hiteff [thing] damcrs , rfl wl p)
        _                     -> (glare $ doSound damcrs        , mvPt)
  where 
    time = _btTimer' pt
    doSound = soundContinue Flame (V2 x y) fireLoudS (Just 2) -- . over worldEvents ((.) $ flameGlareAt ep)
    glare 
        | time `mod` 5 == 0
                    = tempLightSources %~ (theTLS :)
        | otherwise = id
    theTLS = defaultTLS
        { _tlsPos = V3 x y 10
        , _tlsRad = 70
        , _tlsIntensity = V3 0.5 0 0
        }
    sp@(V2 x y) = _btPos' pt
    vel = _btVel' pt
    ep = sp +.+ vel
    mvPt = Just $ pt {_btTimer' = time - 1, _btPos' = ep
                     , _btPassThrough' = Nothing
                     ,_btVel' = 0.98 *.* vel}
    mvPt' = Just $ pt {_btTimer' = time - 1, _btPos' = ep
                     , _btPassThrough' = Nothing
                     ,_btVel' = 0.7 *.* vel}
    damcrs = foldr (\cr -> fst . hiteff [(ep,E3x1 cr)]) w 
        $ filter closeCrs 
        $ IM.elems $ _creatures w
    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 )
    hiteff = _btHitEffect' pt pt
    rfl wl p = Just $ pt 
        { _btTimer' = time -1
        , _btPos' = pOut p
        , _btVel' = reflV wl
        }
    pOut p = p +.+ safeNormalizeV (sp -.- p)
    reflV wall = (0.3 *.* reflectIn (uncurry (-.-) . swap $ _wlLine wall) vel )
        +.+ (0.2 *.* vel)
    smokeCol = fst $ randomR (0.5,0.8) (_randGen w)
    smokeGen = makeFlamerSmokeAt smokeCol (addZ 20 ep)

makeFlameletTimed 
    :: Point2       -- ^ Position
    -> Float        -- ^ z position
    -> Point2       -- ^ Velocity
    -> Maybe Int    -- ^ Creature id
    -> Float        -- ^ Size
    -> Int          -- ^ Timer
    -> World 
    -> World
makeFlameletTimed (V2 x y) z vel maycid size time w = w
    & randGen   .~ g 
    & particles %~ (theFlamelet :)
  where   
    theFlamelet = PtZ 
        { _ptDraw         = drawFlameletZ rot
        , _ptUpdate'      = moveFlamelet
        , _btVel'         = vel
        , _btColor'       = red
        , _btPos'         = V2 x y
        , _btPassThrough' = maycid
        , _btWidth'       = size
        , _btTimer'       = time
        , _btHitEffect'   = destroyOnImpact (doFlameDam 1) noEff noEff
        , _ptZ = z
        }
    (rot ,g) = randomR (0,3) $ _randGen w

drawFlameletZ
    :: Float    -- ^ Rotation
    -> Particle
    -> Picture
drawFlameletZ rot pt = pictures 
    [ setLayer 1 pic 
    , setLayer 3 piu 
    , setLayer 3 pi2 
    ]
  where
    z = _ptZ pt
    sp = _btPos' pt
    vel = _btVel' pt
    ep = sp +.+ vel
    size = _btWidth' pt
    siz2 = size + 0.2
    time = _btTimer' pt
    piu = setDepth (z + 25)
        .  uncurryV translate ep 
        -- . color (V4 2 (-0.5) (-0.5) 1) 
        . 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)

drawFlamelet 
    :: Float    -- ^ Rotation
    -> Particle
    -> Picture
drawFlamelet rot pt = setLayer 1 $ pictures [pic , piu , pi2 , glow]
  where
    sp = _btPos' pt
    vel = _btVel' pt
    ep = sp +.+ vel
    size = _btWidth' pt
    siz2 = size + 0.2
    time = _btTimer' pt
    glow = setDepth 19 $ uncurryV translate ep
                $ circleSolidCol (withAlpha 0 red) (withAlpha 0.05 red) 30
    piu = setDepth 20
        .  uncurryV translate ep 
        . color (dark red) 
        . rotate (negate (rot - 0.1 * fromIntegral time))
        . scale s1 s1
        . polygon 
        $ rectNSWE siz2 (-siz2) (-siz2) siz2
    pi2 = setDepth 20.2
        . uncurryV translate ep
        . color (mixColors (fromIntegral (max 0 (time-2))) (10-fromIntegral time) 
                           orange                          (dark red)
                   )
        . rotate (negate (rot + 0.2 * fromIntegral time))
        . scale s2 s2
        . polygon 
        $ rectNSWE siz2 (-siz2) (-siz2) siz2
    pic = setDepth 20.4
        . uncurryV translate ep
        . color (mixColors (fromIntegral (max 0 (time-2))) (10-fromIntegral time) 
                           white                           (dark red)
                )
        . rotate (negate ( 0.1 * fromIntegral time + rot))
        . scale sc 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.  -}
moveFlamelet :: World -> Particle -> (World, Maybe Particle)
moveFlamelet w pt 
    | _btTimer' pt <= 0 = (     w, Nothing)
    | otherwise         = (damcrs,    mvPt)
  where   
    sp = _btPos' pt
    vel = _btVel' pt
    ep = sp +.+ vel
    size = _btWidth' pt
    mvPt = Just $ pt & btTimer'       -~ 1
                     & btPos'         .~ ep
                     & btPassThrough' .~ Nothing
                     & btVel'         .~ 0.8 *.* vel
    damcrs = w & creatures %~ IM.map damifclose
    isClose cr = dist ep (_crPos cr) < _crRad cr + size
    damifclose cr 
        | isClose cr = cr & crState . crDamage %~ ( Flaming 3 sp ep ep : )
        | otherwise  = cr
-- | 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 :)
    & randGen   .~ g
  where 
    theCloud = Cloud 
        {  _clPos = addZ 20 pos
        , _clVel = addZ 0 vel
        , _clPict = \_ -> setLayer 2 . 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 & creatures %~ flip (foldr (IM.adjust doDam)) damagedCrs
  where 
    damagedCrs = IM.keys $ IM.filter f $ creaturesNearPoint (stripZ $ _clPos c) w
    f cr = dist3 (addZ 20 $ _crPos cr) (_clPos c) < _crRad cr + _clRad c + 10
    doDam cr = cr & crState . crDamage %~ (:) (PoisonDam 1)
makeTeslaArcAt :: Color -> Point2 -> Float -> Particle
makeTeslaArcAt col pos dir = LinearParticle
    { _ptPoints = [pos]
    , _ptDraw = drawTeslaArc
    , _ptUpdate' = moveTeslaArc pos dir
    , _ptTimer = 2
    , _ptColor = brightX 100 1.5 col
    }
drawTeslaArc :: Particle -> Picture
drawTeslaArc pt = pic
  where
    ps' = _ptPoints pt
    pic = setLayer 1 $ pictures 
             [ setDepth 20.5 $ color (brightX 2 2 $ _ptColor pt) $ lineOfThickness 3 ps'
--             , setDepth 20 $ color (withAlpha 0.02 cyan) $ lineOfThickness 20 ps'
--             , setDepth 20 $ color (withAlpha 0.02 cyan) $ lineOfThickness 25 ps'
--             , setDepth 20 $ color (withAlpha 0.02 cyan) $ lineOfThickness 30 ps'
             ]
-- todo: fix electrical damage location
moveTeslaArc 
    :: Point2   -- ^ Emmission position
    -> Float    -- ^ Emmission direction
    -> World 
    -> Particle
    -> (World, Maybe Particle)
moveTeslaArc p d w pt
    | t == 2 = 
        (foldr damCrs w hitCrs & randGen .~ g
            & createSparkCol 8 nc q2 (argV sv + d1)
        , Just $ pt 
            & ptTimer -~ 1
            & ptPoints .~ ps'
        )
    | t < 1 = (w , Nothing)
    | otherwise = (w , Just $ pt & ptTimer -~ 1)
 where 
    t = _ptTimer pt
    ps = take 25 $ p : map f (crsLightChain p d 0 w)
    f (E3x1 cr) = _crPos cr
    f (E3x2 p1) = p1
    f (E3x3 p1) = p1
    ps' = lightningMids d pers ps
    pers = evalState (sequence $ repeat $ randInCirc 5) $ _randGen w
    nc = brightX 100 1.5 $ numColor colid
    (colid,g) = randomR (0::Int,5) $ _randGen w
    f1 (E3x1 cr) = Just $ _crID cr
    f1 _         = Nothing
    hitCrs       = mapMaybe f1 $ take 14 $ crsLightChain p d 0 w
    damCrs cid = over (creatures . ix cid . crState . crDamage) (Electrical 5 cpos cpos cpos :)
      where 
        cpos = _crPos (_creatures w IM.! cid)
    q1 = last $ init ps'
    q2 = last ps'
    hitWall = reflectPointWalls q1 ((2 *.* q2) -.- q1) $ wallsNearPoint q1 w
    (d1,_)  = randomR (-0.7,0.7) $ _randGen w
    sv = maybe (q2 -.- q1) snd hitWall

{-
Finds a point somewhere roughly inbetween two points.
 -}
lightningMid :: Float -> Point2 -> Point2 -> Point2
lightningMid d p1 p2 = (0.25 *.* (p1 +.+ p2)) +.+ 0.5 *.* p3
    where p3 = errorClosestPointOnLine 1 p1 (p1 +.+ unitVectorAtAngle d) p2

{-
Finds extra middle points between successive points in a list of points.
-}
lightningMids :: Float -> [Point2] -> [Point2] -> [Point2]
lightningMids d1 (p:pers) (p1:p3:ps)
    = let p2 = p +.+ lightningMid d1 p1 p3
          d2 = argV $ p3 -.- p2
      in  p1 : p2 : lightningMids d2 pers (p3:ps)
lightningMids _ _ ps = ps

{-
Finds a list of hit things from a given point.
'E3x1' objects are creatures, 'E3x2' objects are points on walls, 'E3x3' objects are points in space.
 -}
crsLightChain :: Point2 -> Float -> Float -> World -> [Either3 Creature Point2 Point2]
crsLightChain p d wlAttract w
    = case crOrWallSensitive p d wlAttract w of
        E3x1 cr -> E3x1 cr : crsLightChain (_crPos cr) (argV (_crPos cr -.- p)) 
                                           (min 1 (wlAttract + 0.3)) w
        E3x2 p1 -> [E3x2 p1]
        E3x3 p1 -> E3x3 p1 : crsLightChain p1 (dChange + argV (p1 -.- p)) (min 1 (wlAttract + 0.3)) (set randGen g w)
    -- where (dChange, g) = (0, _randGen w)
    where (dChange, g) = randomR (-0.5,0.5) $ _randGen w
{-
Finds whether a creature or wall is in front of a given point and direction.
Evaluates to a creature as an 'E3x1' or a wall as an 'E3x2'.
Has a parameter to tweak how attracted the test is to walls.
 -}
crOrWallSensitive 
    :: Point2   -- ^ Start point
    -> Float    -- ^ Direction (radians)
    -> Float    -- ^ Wall attraction parameter
    -> World 
    -> Either3 Creature Point2 Point2
crOrWallSensitive p dir wlAttract w = 
    fromMaybe (E3x3 $ p +.+ rotateV dir (V2 arcLen 0))
    . listToMaybe 
    . sortBy (compare `on` g)
    $ catMaybes [cr,wlp]
  where 
    cr  = E3x1 <$> nearestCrInFront p dir 100 w
    wlp = fmap E3x2 
        . listToMaybe
        . sortBy (compare `on` dist p)
        $ mapMaybe 
            ( fmap fst
            . (\p1 -> reflectPointWalls p p1 $ wallsNearPoint p w)
            . (+.+) p
            . (\d -> rotateV d (V2 100 0))
            . (+ dir)
            . (* wlAttract)
            )
            [-(3*pi/8),-pi/4,-pi/8,0,pi/8,pi/4,3*pi/8]
    g (E3x2 p1)  = 100 + dist p p1 -- tweak makes it more likely to hit crs first
    --g (E3x2 p1)  = dist p p1 - 100 -- tweak makes it more likely to hit walls first
    g (E3x1 cr1) = dist p $ _crPos cr1
    g _ = 0
    (arcLen,_) = randomR (25,50) $ _randGen w
-- BUG: can hit crs through walls
{- Finds whether a creature or wall is in front of a given point and direction.
Evaluates to a creature as an 'E3x1' or a wall as an 'E3x2'.  -}
crOrWall :: Point2 -> Float -> World -> Either3 Creature Point2 Point2
crOrWall p dir w = fromMaybe (E3x3 $ p +.+ rotateV dir (V2 arcLen 0))
                        $ listToMaybe $ sortBy (compare `on` g)
                                        $ catMaybes [cr,wlp]
  where 
    cr  = E3x1 <$> nearestCrInFront p dir 100 w
    wlp = fmap E3x2 
        $ listToMaybe
        $ sortBy (compare `on` dist p)
        $ mapMaybe 
            ( fmap fst
            . (\p1 -> reflectPointWalls p p1 $ wallsNearPoint p w)
            . (+.+) p
            . (\d -> rotateV d (V2 100 0))
            . (+) dir
            )
            [-(3*pi/8),-pi/4,-pi/8,0,pi/8,pi/4,3*pi/8]
            --[-pi/4,-pi/8,0,pi/8,pi/4]
    g (E3x2 p1)  = 100 + dist p p1 -- tweak makes it more likely to hit crs first
    --g (E3x2 p1)  = dist p p1 - 100 -- tweak makes it more likely to hit walls first
    g (E3x1 cr1) = dist p $ _crPos cr1
    g _ = 0
    (arcLen,_) = randomR (25,50) $ _randGen w
-- | Create a spark. 
-- If the spark is created by another Particle, it cannot be directly added to
-- the list, hence the redirect through worldEvents.
createSparkCol :: Int -> Color -> Point2 -> Float -> World -> World
createSparkCol time col pos dir w 
    = w & worldEvents %~ ( over particles (spark :) . )
  where 
    spark = BulletPt
        { _ptDraw = drawBul
        , _ptUpdate' = mvGenBullet
        , _btVel' = rotateV dir (V2 5 0)
        , _btColor' = col
        , _btTrail' = [pos]
        , _btPassThrough' = Nothing
        , _btWidth' = 1
        , _btTimer' = time
        , _btHitEffect' = destroyOnImpact sparkEff noEff noEff
        }
    sparkEff bt p cr
        = creatures . ix (_crID cr) . crState . crDamage %~ ( SparkDam 1 sp p ep : )
      where 
        sp = head (_btTrail' bt)
        ep = sp +.+ _btVel' bt

