{- | Creation, update and destruction of destructible walls.  -}
module Dodge.LevelGen.Block where
import Dodge.Data
import Dodge.Data.SoundOrigin
import Dodge.Base
import Dodge.Zone
import Dodge.Zone.Data
import Dodge.SoundLogic
import Dodge.SoundLogic.LoadSound
import Dodge.WorldEvent.Sound
import Dodge.LevelGen.Pathing
import Dodge.RandomHelp
import Geometry
import qualified IntMapHelp as IM
import Color

import Control.Lens
import Control.Monad.State
import Data.List
import Data.Maybe
import Data.Function
import System.Random

updateBlocks :: World -> World
updateBlocks w = (\w' -> seq (_wallsZone w') w') $ flip (foldr removeFromZone) deadPanes 
                    $ over walls (\wls -> wls `seq` IM.filter (not . blockIsDead) wls)
                 degradeBlocks 
 where  
    degradeBlocks = deadBlocks `seq` foldr killBlock w deadBlocks
    removeFromZone :: Wall -> World -> World
    removeFromZone bl = over (wallsZone . znObjects . ix x . ix y) (IM.delete (_wlID bl))
      where 
        (x,y) = zoneOfPoint $ uncurry pHalf (_wlLine bl)
    deadPanes = filter blockIsDead (IM.elems $ _walls w)
    deadBlocks = nubBy ((==) `on` _blIDs) deadPanes
    blockIsDead wl = case wl ^? blHP of 
        Just x  -> x <= 0
        Nothing -> False
{- | Destroy a block.
Plays destruction sound and sets to be displayed other walls that were shadowed by this wall. -}
killBlock :: Wall -> World -> World
killBlock bl w =  f bl . flip (foldr unshadow) (_blShadows bl) $ w
  where
    f :: Wall -> World -> World
    f bl'
        | isJust (bl ^? blDegrades . ix 0) = degradeBlock bl' . breakHitSound bl
        | otherwise = hitSound' bl'
    pos = fst $ _wlLine bl
    breakHitSound bl' 
        | _wlIsSeeThrough bl' = mkSoundBreakGlass pos
        | otherwise = soundMultiFrom sos (fst $ _wlLine bl') impactSound Nothing
    hitSound' bl' 
        | _wlIsSeeThrough bl' = mkSoundSplinterGlass pos
        | otherwise = soundMultiFrom sos (fst $ _wlLine bl') impactSound Nothing
    sos = [BlockDegradeSound 0,BlockDegradeSound 1]
    impactSound = evalState (takeOne [impact1S,impact2S,impact3S,impact4S]) $ _randGen w
    unshadow :: Int -> World -> World
    unshadow bid w' = case w' ^? walls . ix bid of 
        Just b -> 
            let (x,y) = zoneOfPoint $ uncurry pHalf (_wlLine b)
            in w & wallsZone . znObjects . ix x . ix y . ix bid . blVisible %~ const True
                 & walls . ix bid . blVisible %~ const True
        Nothing -> w

degradeBlock :: Wall -> World -> World
degradeBlock bl w = 
    let blid = _wlID bl
        bls = map (\i -> _walls w IM.! i) (_blIDs $ _walls w IM.! blid)
        ps = reverse $ orderPolygon $ nub $ concatMap ((\(a,b) -> [a,b]) . _wlLine) bls
        (newPs,g) = runState (shrinkPolygon 0.5 ps) $ _randGen w
        (x:xs) = _blDegrades bl
    in addBlock newPs (x + _blHP bl) (_wlColor bl) (_wlIsSeeThrough bl) xs $ set randGen g w

{-
This does not have clear behaviour in my mind, and should probably be replaced with something more obvious...
 -}
pushPointTowardsBy 
    :: RandomGen g 
    => Float 
    -> Point2 
    -> [Point2] 
    -> State g Point2
pushPointTowardsBy x p ps = do
    xs <- replicateM (length ps) $ state $ randomR (0, x / fromIntegral (length ps))
    let toAdd p' y = y *.* (p' -.- p)
    return $ p +.+ foldr1 (+.+) (zipWith toAdd ps xs)

shrinkPolygon 
    :: RandomGen g 
    => Float        -- ^ Shrink parameter
    -> [Point2] 
    -> State g [Point2]
shrinkPolygon x ps = mapM (flip (pushPointTowardsBy x) ps) ps

addBlock 
    :: [Point2]     -- ^ Block polygon
    -> Int          -- ^ First layer of health
    -> Color 
    -> Bool         -- ^ Is the block see through?
    -> [Int]        -- ^ Extra layers of health
    -> World 
    -> World
addBlock (p:ps) hp col isSeeThrough degradability w 
    | hp <= 0 && null degradability = w
    | hp <= 0 = addBlock (p:ps) (head degradability + hp) col isSeeThrough (tail degradability) w
    | otherwise = w  
        & wallsZone . znObjects %~ flip (IM.foldr wallInZone) panes
        & walls     %~ IM.union panes
  where 
    lns = zip (p:ps) (ps ++ [p])
    i = IM.newKey $ _walls w
    is = [i.. i + length lns-1]
    panes = IM.fromList $ zip is $ zipWith 
        (\j (a,b) -> Block 
            { _wlLine = (a,b)
            , _wlID = j
            , _wlColor = col
            , _wlSeen = False
            , _blIDs = is
            , _blHP = hp
            , _wlIsSeeThrough = isSeeThrough
            , _blVisible = True
            , _blShadows = []
            , _blDegrades = degradability
            }
        ) is lns
    wallInZone wl 
        | uncurry dist (_wlLine wl) <= 2*zoneSize 
            = insertIMInZone x y wlid wl
        | otherwise = flip (foldr (\(a,b) -> insertIMInZone a b wlid wl)) ips
      where 
        (x,y) = zoneOfPoint $ uncurry pHalf (_wlLine wl)
        wlid  = _wlID wl
        ips = map zoneOfPoint $ uncurry (divideLine (2*zoneSize)) (_wlLine wl)
addBlock _ _ _ _ _ _ = error "Trying to add a block with incomplete polygon"
        
putBlock :: [Point2] -> Int -> Color -> Bool -> [Int] -> World -> World
putBlock (p:ps) i c b is w = foldr (uncurry removePathsCrossing) wWithBlock pairs
  where 
    pairs = zip (p:ps) (ps ++ [p])
    wWithBlock = addBlock (p:ps) i c b is w
putBlock _ _ _ _ _ _ = error "Trying to put a block with incomplete polygon"

