content: module Game where
  
  import qualified Data.Map as M
  import qualified Point as P
  
  gameWidth, gameHeight :: Int
  gameWidth = 800
  gameHeight = 600
  
  data Status = Running | Won | Lost deriving (Eq)
  
  data Item = Item
      { _siz :: P.Point
      , _pos :: P.Point
      , _vel :: P.Point }
  
  data Game = Game
      { _status :: Status
      , _inputLeft :: Bool
      , _inputRight :: Bool
      , _inputFire :: Bool
      , _rands :: [Double]
      , _firetime :: Double
      , _paddle :: Item
      , _bullets :: [Item]
      , _invaders :: [Item] }
  
  createGame :: [Double] -> Game
  createGame rands0 = Game Running False False False rands1 0 myPaddle [] myInvaders
      where myPaddle = Item (70, 20) (0, -250) (0, 0)
            ([mag, dir], rands1) = splitAt 2 rands0
            vx = (150 + 200 * mag) * (if dir < 0.5 then 1 else -1)
            myInvaders = [ Item (70, 20) 
                                (fromIntegral x * 100, fromIntegral y * 50 + 150)
                                (vx, 0)
                           | x<-[-2..(2::Int)], y<-[0..(2::Int)] ]
  
  updatePaddle :: Double -> Game -> Game
  updatePaddle time g = firePaddleBullet time g1
      where dx = time * 200
            dl = if _inputLeft g then -dx else 0
            dr = if _inputRight g then dx else 0
            p0 = _paddle g
            (x0, y) = _pos p0
            x1 = max (-400) $ min 400 $ dl + dr + x0
            p1 = p0 { _pos = (x1, y) }
            g1 = g { _paddle = p1 }
  
  firePaddleBullet ::  Double -> Game -> Game
  firePaddleBullet time g = if canFire then mFire else mNofire
      where canFire = _inputFire g && _firetime g > 0.9
            (x, y) = _pos $ _paddle g
            bullet = Item (3, 9) (x, y+20) (0, 200)
            mFire = g { _bullets = bullet : _bullets g, _firetime = 0 }
            mNofire = g { _firetime = time + _firetime g }
  
  updateInvaders :: Double -> Game -> Game
  updateInvaders time g = if null myInvaders then g else g3
      where myInvaders = _invaders g
            i1 = map (autoUpdateItem time) myInvaders
            xs = map (fst . _pos) myInvaders
            x1min = minimum xs
            x1max = maximum xs
            v@(vx, _) = _vel $ head myInvaders
            move v0 v1 i = i { _pos = _pos i P.+ time P.* v0, _vel = v1 }
            i2 | vx>0 && x1max>380 = map (move (380-x1max, 0) (P.negate v)) i1
               | vx<0 && x1min<(-380) = map (move (-380-x1min, 0) (P.negate v)) i1
               | otherwise = i1
            g2 = g { _invaders = i2 }
            g3 = fireInvadersBullets g2
  
  fireInvadersBullets :: Game -> Game
  fireInvadersBullets g = g { _bullets = _bullets g ++ bs, _rands = rands3 }
      where invadersPos = map _pos $ _invaders g
            fInsert pMap (x,y) = M.insertWith min x y pMap
            fighters0 = M.toList $ foldl fInsert M.empty invadersPos
            (rands0, rands1) = splitAt (length fighters0) (_rands g)
            (rands2, rands3) = splitAt (length fighters0) rands1
            difficulty = 0.9 + fromIntegral (length invadersPos) * (0.99 - 0.9) / 15
            fighters1 = [ (p, v) | (p, r, v) <- zip3 fighters0 rands0 rands2, r > difficulty ]
            createBullet ((x, y), v) = Item (3, 9) (x, y-20) (0, -(300-v*200))
            bs = map createBullet fighters1
  
  autoUpdateItem :: Double -> Item -> Item
  autoUpdateItem t i@(Item _ pos vel) = i { _pos = pos P.+ t P.* vel }
  
  updateBullets :: Double -> Game -> Game
  updateBullets time g = g { _bullets = b2 }
      where b1 = map (autoUpdateItem time) (_bullets g)
            b2 = filter (\b -> snd (_pos b) < 300 && snd (_pos b) > -300) b1
  
  updateCollisions :: Game -> Game
  updateCollisions g = g1 { _invaders = i1, _bullets = b2, _status = st }
      where (b1, i1) = runCollisions (_bullets g) (_invaders g)
            (b2, p2) = runCollisions b1 [_paddle g]
            st | null i1 = Won 
               | null p2 = Lost
               | otherwise = Running
            g1 = if st == Running then g 
                 else g { _inputLeft = False, _inputRight = False, _inputFire = False }
  
  testCollision :: Item -> Item -> Bool
  testCollision (Item as ap _) (Item bs bp _) =
      ((bx0 < ax0 && ax0 < bx1) || (bx0 < ax1 && ax1 < bx1)) &&
      ((by0 < ay0 && ay0 < by1) || (by0 < ay1 && ay1 < by1))
      where (ax0, ay0) = ap P.- 0.5 P.* as
            (ax1, ay1) = ap P.+ 0.5 P.* as
            (bx0, by0) = bp P.- 0.5 P.* bs
            (bx1, by1) = bp P.+ 0.5 P.* bs
  
  runCollisions :: [Item] -> [Item] -> ([Item], [Item])
  runCollisions [] is = ([], is)
  runCollisions (b:bs) is = (bs1++bs2, is2)
      where is1 = filter (not . testCollision b) is
            bs1 = [b | length is1 == length is]
            (bs2, is2) = runCollisions bs is1
  
  step :: Double -> Game -> Game
  step time g = if _status g == Running then mRunning else mEnd
      where mRunning = updatePaddle time
                          $ updateInvaders time 
                          $ updateBullets time
                          $ updateCollisions g
            mEnd = if _inputFire g then createGame (_rands g) else g
  
  