{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module Network.QUIC.Recovery.Metrics (
    updateRTT,
    updateCC,
    metricsUpdated,
    setInitialCongestionWindow,
) where

import Data.Sequence (Seq)
import UnliftIO.STM

import Network.QUIC.Imports
import Network.QUIC.Qlog
import Network.QUIC.Recovery.Constants
import Network.QUIC.Recovery.Misc
import Network.QUIC.Recovery.Persistent
import Network.QUIC.Recovery.Types
import Network.QUIC.Types

updateRTT :: LDCC -> EncryptionLevel -> Microseconds -> Microseconds -> IO ()
updateRTT ldcc@LDCC{..} lvl latestRTT0 ackDelay0 = metricsUpdated ldcc $ do
    firstTime <- atomicModifyIORef' recoveryRTT update
    when firstTime $ do
        setPktNumPersistent ldcc
        qlogDebug ldcc $ Debug "RTT first sample"
  where
    -- don't use latestRTT, use latestRTT0 instead
    --
    -- First time:
    -- Overwriting the initial value with the first sample.
    -- Initial value was used to calculate PTO.
    --
    -- smoothed_rtt = rtt_sample
    -- rttvar = rtt_sample / 2
    update rtt@RTT{..}
        | latestRTT == Microseconds 0 =
            ( rtt
                { latestRTT = latestRTT0
                , minRTT = latestRTT0
                , smoothedRTT = latestRTT0
                , rttvar = latestRTT0 `unsafeShiftR` 1
                }
            , True
            )
    -- Others:
    update rtt@RTT{..} =
        ( rtt
            { latestRTT = latestRTT0
            , minRTT = minRTT'
            , smoothedRTT = smoothedRTT'
            , rttvar = rttvar'
            }
        , False
        )
      where
        -- minRTT ignores ack delay.
        minRTT' = min minRTT latestRTT0
        -- Limit ack_delay by max_ack_delay
        -- ack_delay = min(Ack Delay in ACK Frame, max_ack_delay)
        ackDelay = min ackDelay0 $ getMaxAckDelay (Just lvl) maxAckDelay1RTT
        -- Adjust for ack delay if plausible.
        -- adjusted_rtt = latest_rtt
        -- if (latest_rtt >= min_rtt + ack_delay):
        --   adjusted_rtt = latest_rtt - ack_delay
        adjustedRTT
            | latestRTT0 >= minRTT + ackDelay = latestRTT0 - ackDelay
            | otherwise = latestRTT0
        -- rttvar_sample = abs(smoothed_rtt - adjusted_rtt)
        -- rttvar = 3/4 * rttvar + 1/4 * rttvar_sample
        rttvar' =
            rttvar
                - (rttvar !>>. 2)
                + (abs (smoothedRTT - adjustedRTT) !>>. 2)
        -- smoothed_rtt = 7/8 * smoothed_rtt + 1/8 * adjusted_rtt
        smoothedRTT' =
            smoothedRTT
                - (smoothedRTT !>>. 3)
                + (adjustedRTT !>>. 3)

updateCC :: LDCC -> Seq SentPacket -> Bool -> IO ()
updateCC ldcc@LDCC{..} lostPackets isRecovery = do
    persistent <- inPersistentCongestion ldcc lostPackets
    when (persistent || not isRecovery) $ do
        minWindow <- kMinimumWindow ldcc
        now <- getTimeMicrosecond
        metricsUpdated ldcc $ atomically $ modifyTVar' recoveryCC $ \cc@CC{..} ->
            let halfWindow = max minWindow $ kLossReductionFactor congestionWindow
                cwin
                    | persistent = minWindow
                    | otherwise = halfWindow
                sst = halfWindow
                mode
                    | cwin < sst = SlowStart -- persistent
                    | otherwise = Recovery
             in cc
                    { congestionRecoveryStartTime = Just now
                    , congestionWindow = cwin
                    , ssthresh = sst
                    , ccMode = mode
                    , bytesAcked = 0
                    }
        CC{ccMode} <- readTVarIO recoveryCC
        qlogContestionStateUpdated ldcc ccMode

setInitialCongestionWindow :: LDCC -> Int -> IO ()
setInitialCongestionWindow ldcc@LDCC{..} pktSiz = metricsUpdated ldcc $
    atomically $ do
        modifyTVar' recoveryCC $ \cc ->
            cc
                { congestionWindow = kInitialWindow pktSiz
                }

----------------------------------------------------------------

metricsUpdated :: LDCC -> IO () -> IO ()
metricsUpdated ldcc@LDCC{..} body = do
    rtt0 <- readIORef recoveryRTT
    cc0 <- readTVarIO recoveryCC
    body
    rtt1 <- readIORef recoveryRTT
    cc1 <- readTVarIO recoveryCC
    let ~diff =
            catMaybes
                [ time "min_rtt" (minRTT rtt0) (minRTT rtt1)
                , time "smoothed_rtt" (smoothedRTT rtt0) (smoothedRTT rtt1)
                , time "latest_rtt" (latestRTT rtt0) (latestRTT rtt1)
                , time "rtt_variance" (rttvar rtt0) (rttvar rtt1)
                , numb "pto_count" (ptoCount rtt0) (ptoCount rtt1)
                , numb "bytes_in_flight" (bytesInFlight cc0) (bytesInFlight cc1)
                , numb "congestion_window" (congestionWindow cc0) (congestionWindow cc1)
                , numb "ssthresh" (ssthresh cc0) (ssthresh cc1)
                ]
    unless (null diff) $ qlogMetricsUpdated ldcc $ MetricsDiff diff
  where
    time tag (Microseconds v0) (Microseconds v1)
        | v0 == v1 = Nothing
        | otherwise = Just (tag, v1)
    numb tag v0 v1
        | v0 == v1 = Nothing
        | otherwise = Just (tag, v1)
