{- Functions controlling the movement of the screen camera: 
'_cameraCenter', '_cameraZoom', _cameraRot';
and the position that the character sees from: '_cameraViewFrom'.  -}
module Dodge.Update.Camera 
    ( updateCamera
    , farWallPoints
    , streamViewpoints
    , farWallDistDirection
    ) where
import Dodge.Data
import Dodge.Base
import Dodge.Zone
import Dodge.Equipment
import Dodge.Creature.Test
import Geometry
--import Geometry.ConvexPoly
import Dodge.GameRoom
import LensHelp
import Bound

import qualified Data.Map.Strict as M
--import qualified Data.List.NonEmpty as NEL
--import Control.Applicative
--import Data.Foldable
import Data.Maybe
import Control.Monad
import qualified Data.Set as Set
import qualified Data.IntMap.Strict as IM
import qualified SDL
--import Data.Monoid
--import Data.Semigroup
import qualified Streaming.Prelude as S
import Streaming
{- Update the screen camera rotation and position, including any in rold scope/remote camera modifiers; 
update where your avatar's view is from.  -}
updateCamera :: Configuration -> World -> World
updateCamera cfig = setViewDistance cfig 
    . moveZoomCamera cfig
    . updateScopeZoom 
    . rotateCamera cfig

moveZoomCamera :: Configuration -> World -> World
moveZoomCamera cfig w = w
    & cameraCenter .~ newcen
    & cameraViewFrom .~ newvf
    & cameraZoom .~ newzoom
    & defaultZoom .~ newDefaultZoom
    & itemZoom .~ newItemZoom
  where
    cpos =_crPos (you w)
    newvf = cpos +.+ fromMaybe (V2 0 0) vfoffset
    vfoffset = do
        iscam <- yourItem w ^? _Just . itScope . scopeIsCamera
        guard iscam
        guard (SDL.ButtonRight `M.member` _mouseButtons w)
        yourItem w ^? _Just . itScope . scopePos
    mscopeoffset = do
        guard (SDL.ButtonRight `M.member` _mouseButtons w)
        yourItem w ^? _Just . itScope . scopePos
    newcen = cpos +.+ fromMaybe (V2 0 0) mscopeoffset +.+ offset
    offset = rotateV (_cameraRot w) $ ((newzoom - newDefaultZoom)/(newDefaultZoom*newzoom)) *.* _mousePos w
    --newzoom = changeZoom (_cameraZoom w) idealZoom'
    newzoom = case yourItem w ^? _Just . itScope of
        Just zs@ZoomScope {} -> _scopeZoom zs
        _ -> newDefaultZoom * newItemZoom
    idealDefaultZoom = clipZoom wallZoom
    newDefaultZoom = case yourItem w ^? _Just . itScope of
        Just zs@ZoomScope {} -> _scopeZoom zs
        _ -> changeZoom (_defaultZoom w) idealDefaultZoom
    idealItemZoom = fromMaybe 1 $ do
        guard $ crIsAiming (you w)
        zoomFromItem' <$> (yourItem w ^? _Just . itUse . useAim . aimZoom)
    newItemZoom = changeZoom (_itemZoom w) idealItemZoom
    changeZoom curZoom idealZoom
      | curZoom > idealZoom + 0.01 = ((zoomOutSpeed-1)*curZoom + idealZoom) / zoomOutSpeed
      | curZoom < idealZoom - 0.01 = ((zoomInSpeed -1)*curZoom + idealZoom) / zoomInSpeed
      | otherwise                  = idealZoom
    --wallZoom = farWallDist newvf cfig w
    wallZoom = min4 (_boundDist w)
    min4 (a,b,c,d) = minimum [hh/maxd a,hh/maxd (-b),hw/maxd c,hw/maxd (-d)]
    maxd = max distFromEqmnt
    distFromEqmnt = foldr max 1 $ IM.mapMaybe (_eqViewDist . _eqEq. _itUse) $ getCrEquipment $ you w
    hw = halfWidth  cfig
    hh = halfHeight cfig
--            = maybe clipZoom zoomFromItem (yourItem w ^? itZoom) wallZoom
    -- these speeds are inverted, larger means slower
    zoomInSpeed  = 25
    zoomOutSpeed = 15


updateScopeZoom :: World -> World
updateScopeZoom w 
    | SDL.ButtonRight `M.member` _mouseButtons w 
      = case w ^? creatures . ix 0 . crInv . ix (crSel (_creatures w IM.! 0))
                          . itScope . scopeZoomChange of
        Just x 
           | x > 10     -> zoomInLongGun $ zoomInLongGun $ zoomInLongGun w
           | x > 5     -> zoomInLongGun $ zoomInLongGun w
           | x > 0     -> zoomInLongGun w
           | x < -10    -> zoomOutLongGun $ zoomOutLongGun $ zoomOutLongGun w
           | x < -5    -> zoomOutLongGun $ zoomOutLongGun w
           | x < 0     -> zoomOutLongGun w
           | otherwise -> w
        _                  -> w
    | otherwise = w & creatures . ix 0 . crInv . ix (crSel (_creatures w IM.! 0))
                                . itScope %~ updateScope
  where 
    updateScope (ZoomScope _ _ _ defz bl) = ZoomScope (V2 0 0) 0 defz defz bl
    updateScope otherAtt = otherAtt

zoomSpeed :: Float
zoomSpeed = 39/40

zoomInLongGun :: World -> World
zoomInLongGun w 
    | currentZoom < 8 = w
        & wpPointer . itScope . scopePos .+.+~ (1-zoomSpeed) / newzoom *.* mousep
        & wpPointer . itScope . scopeZoom %~ (/ zoomSpeed)
        & decreaseScopeZoomChange
    | otherwise = w & wpPointer . itScope . scopeZoomChange .~ 0
  where 
    decreaseScopeZoomChange = wpPointer . itScope . scopeZoomChange -~ 1
    wpPointer = creatures . ix 0 . crInv . ix (crSel (_creatures w IM.! 0))
    wp = _crInv (_creatures w IM.! 0) IM.! crSel (_creatures w IM.! 0)
    Just currentZoom = wp ^? itScope . scopeZoom
    newzoom = (wp ^?! itScope . scopeZoom) / zoomSpeed
    mousep = rotateV (_cameraRot w) $ _mousePos w

zoomOutLongGun :: World -> World
zoomOutLongGun w 
    | currentZoom > 0.5 = w
--        & wpPointer . itScope . scopePos .+.+~ ((1*zoomSpeed-1)) / currentZoom *.* mousep
        & wpPointer . itScope . scopeZoom *~ zoomSpeed
        & increaseScopeZoomChange
--    | otherwise = increaseScopeZoomChange w
    | otherwise = w & wpPointer . itScope . scopeZoomChange .~ 0
  where 
    --mousep = rotateV (_cameraRot w) $ _mousePos w
    increaseScopeZoomChange = wpPointer . itScope . scopeZoomChange +~ 1
    wpPointer = creatures . ix 0 . crInv . ix (crSel (_creatures w IM.! 0))
    wp = _crInv (_creatures w IM.! 0) IM.! crSel (_creatures w IM.! 0)
    Just currentZoom = wp ^? itScope . scopeZoom

ifConfigWallRotate :: Configuration -> World -> World
ifConfigWallRotate cfig w
    | _gameplay_rotate_to_wall cfig && not (SDL.ButtonRight `M.member` _mouseButtons w)
                = rotateToOverlappingWall w
    | otherwise = w

rotateToOverlappingWall :: World -> World
rotateToOverlappingWall w = maybe 
    id 
    (doWallRotate . snd)
    (overlapCircWallsClosest p (_crRad cr + 5) (S.filter _wlRotateTo $ wlsNearPoint p w))
    w
  where
    cr = you w
    p = _crPos (you w)

doWallRotate :: Wall -> World -> World
doWallRotate wl' w = rotateUsing $ (argV . uncurry (-.-) $ _wlLine wl') - _cameraRot w
  where
    rotateUsing a
        | b - b' > 0.01        = w & cameraRot +~ 0.01
        | b - b' < negate 0.01 = w & cameraRot -~ 0.01
        | otherwise            = w
      where
        --b = a * (2 / pi)
        b = a * (4 / pi)
        b' = fromIntegral (round b :: Int)

rotateCameraBy :: Float -> World -> World
rotateCameraBy x w = w 
    & cameraRot +~ x
    & creatures . ix 0 . crInv . ix (crSel (_creatures w IM.! 0))
         . itScope . scopePos %~ rotateV x

rotateCamera :: Configuration -> World -> World
rotateCamera cfig w
    | keyl && keyr = w
    | keyl = rotateCameraBy   0.025  w
    | keyr = rotateCameraBy (-0.025) w
    | otherwise = ifConfigWallRotate cfig w
  where
    keyl = SDL.ScancodeQ `Set.member` _keys w && notAtTerminal w
    keyr = SDL.ScancodeE `Set.member` _keys w && notAtTerminal w

-- TODO check where/how this is used
notAtTerminal :: World -> Bool
notAtTerminal w = isNothing $ w ^? hud . hudElement . subInventory . termID

--zoomCamBy :: Float -> World -> World
--zoomCamBy x w = w {_cameraZoom = max (_cameraZoom w + x) 0.01}
zoomFromItem'
    :: ItZoom
    -> Float
zoomFromItem' ItZoom {_itZoomMax = zMax, _itZoomMin = zMin, _itZoomFac = zFac}
    = min zMax $ max zMin zFac

clipZoom 
    :: Float -- ^ Furthest viewable distance
    -> Float
clipZoom = min 20 . max 0.2

setViewDistance :: Configuration -> World -> World
setViewDistance cfig w = w & viewDistance 
    .~ sqrt (halfWidth cfig ** 2 + halfHeight cfig ** 2) / _cameraZoom w
--    .~ max (halfWidth cfig) (halfHeight cfig) / _cameraZoom w

streamViewpoints :: Monad m => Point2 -> World -> Stream (Of Point2) m ()
{-# INLINE streamViewpoints #-}
streamViewpoints p w = flip S.for (gameRoomViewpoints p)
    $ S.filter (pointInOrOnPolygon p . _grBound) $ S.each (_gameRooms w)

gameRoomViewpoints :: Monad m => Point2 -> GameRoom -> Stream (Of Point2) m ()
gameRoomViewpoints p gr = S.each (_grViewpoints gr) 
    <> S.map extend (S.each (_grViewpointsEx gr) <> S.map addDir (S.each $ _grLinkDirs gr))
  where
    extend outp = p +.+ maxViewDistance *.* squashNormalizeV (outp -.- p)
    addDir a = p +.+ unitVectorAtAngle a

farWallDistDirection :: Point2 -> World -> Maybe (Float,Float,Float,Float)
farWallDistDirection p w = boundPoints
    $ S.map f vps
  where
    f q = runIdentity (S.fold_ findPoint q (rotateV (negate $ _cameraRot w) . (-.- p)) (wls q))
    wls q = S.filter wlIsOpaque $ wlsNearSeg p q w
    findPoint q = fromMaybe q . uncurry (intersectSegSeg p q) . _wlLine
    vps = streamViewpoints p w


extendedViewPoints :: Point2 -> [GameRoom] -> [Point2]
extendedViewPoints p grs = map extend 
    (concatMap _grViewpointsEx grs
    ++ map addDir (concatMap _grLinkDirs grs)
    )
  where
    extend outp = p +.+ maxViewDistance *.* squashNormalizeV (outp -.- p)
    addDir a = p +.+ unitVectorAtAngle a

farWallPoints :: Point2 -> World -> [Point2]
farWallPoints p w = concatMap _grViewpoints grs ++ extendedViewPoints p grs
  where
    grs = filter (pointInOrOnPolygon p . _grBound) (_gameRooms w)

maxViewDistance :: Float
maxViewDistance = 800
