module Test where

import Debug.Trace
import Array
import Control.Arrow ((***))
import AFRP
import AFRPEvent
import AFRPGeometry
import AFRPUtilities
import AFRPForceable
import Animate
import Parser
import Command
import WorldGeometry
import PhysicalDimensions
import Colors
import IdentityList

import qualified Graphics.HGL as HGL


test = animate 50 "test window" worldSizeX worldSizeY
               (drawObjects)
               (const [])
               (parseWinInput >>> mainLoop)

mainLoop :: SF GameInput (IL ObsObjState)
mainLoop = proc gi -> do 
    rec oos <- compute objs0 -< (gi, oos)
    returnA -< fmap ooObsObjState oos

objs0 = listToIL [ball0 ,barr0,barr1, barr3,diam0] 
  where
    ball0 = ball ballP0 ballRadius
    barr0 = oscillate (barrier barrWid barrLen) 
                      barrP0  
                      (Point2 0 $ worldYMax - barrLen)
                      (Point2 0 $ worldYMin + barrLen)
    barr1 = oscillate (barrier barrLen barrWid) 
                      barrP0  
                      (Point2 (worldXMax - barrLen) (worldYMax / 2))
                      (Point2 (worldXMin + barrLen) (worldYMax / 2))
    barr3 = oscillate (barrier barrWid barrLen) 
                      barrP0  
                      (Point2 (worldXMax - barrWid) (worldYMax - barrLen))
                      (Point2 (worldXMin + barrLen) (worldYMin + barrLen))
    diam0 = wrap (diamond diamWid diamLen) diamV0 (Point2 0 0)

compute :: IL Object -> SF (GameInput, IL ObjOut) (IL ObjOut)
compute objs = dpSwitch route objs (checkup >>> notYet) (\sfs f -> compute $ f sfs)


checkup :: SF (a, IL ObjOut) (Event (IL Object -> IL Object))
checkup = proc (_,oos) -> do
    let
      es :: [Event (IL Object -> IL Object)]
      es = [ooKillReq oo `tag` (deleteIL k) | (k, oo) <- assocsIL oos]
    returnA -< foldl (mergeBy (.)) noEvent es


route :: (GameInput, IL ObjOut) -> IL sf -> IL (ObjIn, sf)
route (gi, oos) objs = mapIL routeAux objs where
    routeAux (k, obj) =
	(ObjIn {oiHit = foldl (mergeBy $ (^+^) |*| max) noEvent $ lookupAll k hs,
		oiGameInput = gi},
	 obj)
    hs = hits (assocsIL (fmap ooObsObjState oos))

(f |*| g) (x1, y1) (x2, y2) = (f x1 x2, g y1 y2)
lookupAll k [] = []
lookupAll k ((i,v):vs) = if k == i then Event v:lookupAll k vs else lookupAll k vs

hits :: [(ILKey, ObsObjState)] -> [(ILKey, Correction)]
hits kooss = map fromEvent $ filter isEvent (hitsAux kooss) where
    hitsAux [] = []
    hitsAux ((koos):kooss) =
	[ koos `colliding` koos' | (koos') <- kooss]
	++ hitsAux kooss

--
-- Corrective velocity vector, and size of puncture for an object which has been hit.
--
type Correction = ((Velocity, Velocity), Length)	

--
-- Objects
--
type Object = SF ObjIn ObjOut

data ObjIn = ObjIn {
    oiHit :: Event Correction,
    oiGameInput :: GameInput
}

data ObjOut = ObjOut {
    ooObsObjState :: !ObsObjState,
    ooKillReq :: Event (),
    ooSpawnReq :: Event [Object]
}

data ObsObjState = 
    OOSBall {
        oosPos :: !Position2,
        oosVel :: !Velocity2,
        oosRadius :: !Length
    }
  | OOSBarrier {
        oosPos :: !Position2,
        oosVel :: !Velocity2,
        oosWidth :: !Length,
        oosHeight :: !Length
    }
  | OOSDiamond {
        oosPos :: !Position2,
        oosVel :: !Velocity2,
        oosWidth :: !Length,
        oosHeight :: !Length
    }

instance Forceable ObsObjState where
    -- If non-strict fields: oosNonStrict1 obj `seq` ... `seq` obj 
    force obj = obj



oscillate :: (Position2 -> Position2 -> Object) -> Position2 -> Position2 -> Position2 -> Object
oscillate obj p0 p1 p2 =  
    switch (obj p0 p1 >>> arr id &&& arr isDone) (\p -> oscillate obj p p2 p1) 
  where
    isDone (ObjOut {ooObsObjState = oos}) = let p = oosPos oos in Event p `gate` eq p p1 
    eq p1@(Point2 x1 y1) p2@(Point2 x2 y2) = abs (x1 - x2) < eps && abs (y1 - y2) < eps
    eps = 0.1



wrap :: (Velocity2 -> Position2 -> Object) -> Velocity2 -> Position2 -> Object
wrap obj v0 p0 = switch (obj v0 p0 >>> arr id &&& arr wrapped) (uncurry $ wrap obj)
  where
    wrapped (ObjOut {ooObsObjState = oos}) = 
        Event (oosVel oos, Point2 x' y') `gate` (x < worldXMin || x > worldXMax || y < worldYMin || y > worldYMax)
      where 
        (Point2 x y) = oosPos oos
        x' = if x < worldXMin then worldXMax else if x > worldXMax then worldXMin else x
        y' = if y < worldYMin then worldYMax else if y > worldYMax then worldYMin else y

--
-- ball object
--
ballP0 :: Position2
ballP0 = Point2 worldXMin worldYMin
ballRadius = 50

ball :: Position2 -> Length -> Object

maxBallA = 300
maxBallV = 300
bound l x = if x < -l then -l else if x > l then l else x 

check maxv v a = if -maxv <= v && v <= maxv || v < -maxv && a > 0 || v > maxv && a < 0 then a else 0 

ball p0 r = proc oi -> do
    let gi = oiGameInput oi
        ((dvx, dvy), puncture) = event ((0,0),0) (id) (oiHit oi)
    Point2 xd yd <- ptrPos -< gi  -- desired position
    rec let ax = check maxBallV (vector2X v) $ bound maxBallA $ 10 * (xd - x) - 5 * vector2X v -- x acceleration
            ay = check maxBallV (vector2Y v) $ bound maxBallA $ 10 * (yd - y) - 5 * vector2Y v -- y acceleration
        v <- integral -< vector2 ax ay  -- velocity
        p@(Point2 x y) <- (p0 .+^) ^<< integral -< v ^+^ (vector2 dvx dvy) -- position
    returnA -< ObjOut (OOSBall p v r) (Event () `gate` (puncture > 30)) noEvent


--
-- barrier object
--
barrP0 :: Position2
barrP0 = Point2 (0) (worldYMax / 2.0)
barrWid = 50
barrLen = 150
maxBarrA = 500
maxBarrV = 500

barrier :: Length -> Length -> Position2 -> Position2 -> Object
barrier w h p0 (Point2 xd yd) = proc oi -> do
    rec let ax = check maxBarrV (vector2X v) $ bound maxBarrA $ 10 * (xd - x) - 5 * vector2X v
            ay = check maxBarrV (vector2Y v) $ bound maxBarrA $ 10 * (yd - y) - 5 * vector2Y v
        v <- integral -< vector2 ax ay
        p@(Point2 x y) <- (p0 .+^) ^<< integral -< v
    returnA -< ObjOut (OOSBarrier p v w h) noEvent noEvent


--
-- diamond object
--
diamWid = 50
diamLen = 75
diamV0 = vector2 100 100
diamond :: Length -> Length -> Velocity2 -> Position2 -> Object
diamond w h v0 p0 = proc oi -> do
    rec vp <- iPre v0 -< v  
        v <- hold v0 -< fmap ((vp ^+^) . uncurry vector2 . fst) (oiHit oi)
        p <- (p0 .+^) ^<< integral -< v 
    returnA -< ObjOut (OOSDiamond p vp w h) noEvent noEvent

--
-- Check if two objects touch each other.
--
intersects :: (ILKey, ObsObjState) -> (ILKey, ObsObjState) -> Event (ILKey, Correction)

--oos1@(OOSBall{}) `intersects` oos2@(OOSBall{}) =
--    if norm ((oosPos oos2) .-. (oosPos oos1)) < (oosRadius oos2 + oosRadius oos1)

(k, oos1@OOSBall{}) `intersects` (_, oos2@OOSBarrier{}) =
    let xdiff = point2X (oosPos oos1) - point2X (oosPos oos2)
        ydiff = point2Y (oosPos oos1) - point2Y (oosPos oos2)
        xinter = abs xdiff - (oosRadius oos1) - (oosWidth oos2 / 2) 
        yinter = abs ydiff - (oosRadius oos1) - (oosHeight oos2 / 2)
        (vx1,vy1) = vector2XY $ oosVel oos1
        (vx2,vy2) = vector2XY $ oosVel oos2
        vx1' = if (xdiff < 0) == (vx1 > 0) then -2*vx1 else 0 
        vy1' = if (ydiff < 0) == (vy1 > 0) then -2*vy1 else 0
        vx2' = if (xdiff < 0) == (vx2 < 0) then vx2 else 0
        vy2' = if (ydiff < 0) == (vy2 < 0) then vy2 else 0
        collision = xinter <= 0 && yinter <= 0
    in if collision then Event (k, ((vx1'+vx2', vy1'+vy2'), abs $ max xinter yinter))
       else noEvent

koos1@(_, OOSBarrier{}) `intersects` koos2@(_, OOSBall{}) = koos2 `intersects` koos1

koos1@(k, oos1@OOSDiamond{}) `intersects` (_, oos2@OOSBarrier{}) =
    let pDm = oosPos oos1;             pBa = oosPos oos2
        wDm = oosWidth oos1 / 2;       wBa = oosWidth oos2 / 2
        hDm = oosHeight oos1 / 2;      hBa = oosHeight oos2 / 2
        d1 = pDm .-^ vector2 wDm 0;    b1 = pBa .-^ vector2 wBa hBa
        d2 = pDm .-^ vector2 (-wDm) 0; b2 = pBa .-^ vector2 wBa (-hBa)
        d3 = pDm .-^ vector2 0 hDm;    b3 = pBa .-^ vector2 (-wBa) hBa
        d4 = pDm .-^ vector2 0 (-hDm);  b4 = pBa .-^ vector2 (-wBa) (-hBa)
        inside (Point2 cx cy) w h (Point2 x y) = abs (cx - x) <= w && abs (cy - y) <= h
        collision = any (inside pDm wDm hDm) [b1,b2,b3,b4] || any (inside pBa wBa hBa) [d1,d2,d3,d4]
        (vx1,vy1) = vector2XY $ oosVel oos1
        (vx2,vy2) = vector2XY $ oosVel oos2
        xdiff = point2X pDm - point2X pBa
        ydiff = point2Y pDm - point2Y pBa
        vx1' = if (xdiff < 0) == (vx1 > 0) then -2*vx1 else 0 
        vy1' = if (ydiff < 0) == (vy1 > 0) then -2*vy1 else 0
    in if collision then Event (k, ((vx1', vy1'), 0))
       else noEvent
       
koos1@(_, oos1@OOSBarrier{}) `intersects` koos2@(_, oos2@OOSDiamond{}) = koos2 `intersects` koos1

_ `intersects` _ = noEvent

--
-- Check if two objects are colliding.
--
colliding :: (ILKey, ObsObjState) -> (ILKey, ObsObjState) -> Event (ILKey, Correction)
koos1 `colliding` koos2 = koos1 `intersects` koos2


drawObjects oos = HGL.overGraphics $ map drawObj (elemsIL oos)

drawObj (OOSBall {oosPos = p, oosRadius = r}) = circle Yellow p r
drawObj (OOSBarrier {oosPos = p, oosWidth = w, oosHeight = h}) = polygon Blue [p1,p2,p3,p4]
  where
    delta = vector2 (w/2) (h/2)
    p1@(x1,y1) = position2ToGPoint $ p .-^ delta
    p2 = (x1, y2)
    p3@(x2,y2) = position2ToGPoint $ p .+^ delta
    p4 = (x2, y1)
drawObj (OOSDiamond {oosPos = p, oosWidth = w, oosHeight = h}) = polygon Green [p1,p2,p3,p4]
  where
    p1 = position2ToGPoint $ p .-^ (vector2 (w/2) 0)
    p3 = position2ToGPoint $ p .-^ (vector2 (-w/2) 0)
    p2 = position2ToGPoint $ p .-^ (vector2 0 (h/2))
    p4 = position2ToGPoint $ p .-^ (vector2 0 (-h/2))


--
-- Drawing Shapes
--
circle :: Color -> Position2 -> Length -> HGL.Graphic
circle c p r = 
    HGL.mkBrush (colorTable ! c) $ \brush ->
    HGL.withBrush brush	        $
    HGL.ellipse gp11 gp22
    where
        d   = vector2 r r
        gp11 = position2ToGPoint (p .-^ d)
	gp22 = position2ToGPoint (p .+^ d)

polygon :: Color -> [HGL.Point] -> HGL.Graphic
polygon c ps =
    HGL.mkBrush (colorTable ! c) $ \brush ->
    HGL.withBrush brush	      $
    HGL.polygon ps
