{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module Network.QUIC.Recovery.Types (
    SentPacket (..),
    mkSentPacket,
    fixSentPacket,
    LostPacket (..),
    SentPackets (..),
    emptySentPackets,
    RTT (..),
    initialRTT,
    CCMode (..),
    CC (..),
    initialCC,
    LossDetection (..),
    initialLossDetection,
    MetricsDiff (..),
    TimerType (..),
    TimerInfo (..),
    TimerInfoQ (..),
    TimerCancelled,
    TimerExpired,
    makeSentPackets,
    makeLossDetection,
    LDCC (..),
    newLDCC,
    qlogSent,
    qlogMetricsUpdated,
    qlogPacketLost,
    qlogContestionStateUpdated,
    qlogLossTimerUpdated,
    qlogLossTimerCancelled,
    qlogLossTimerExpired,
) where

import Data.IORef
import Data.List (intersperse)
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import Network.QUIC.Event
import System.Log.FastLogger
import UnliftIO.STM

import Network.QUIC.Connector
import Network.QUIC.Imports
import Network.QUIC.Qlog
import Network.QUIC.Types

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

data SentPacket = SentPacket
    { spPlainPacket :: PlainPacket
    , spTimeSent :: TimeMicrosecond
    , spSentBytes :: Int
    , spEncryptionLevel :: EncryptionLevel
    , spPacketNumber :: PacketNumber
    , spPeerPacketNumbers :: PeerPacketNumbers
    , spAckEliciting :: Bool
    }
    deriving (Eq, Show)

instance Ord SentPacket where
    x <= y = spPacketNumber x <= spPacketNumber y

newtype LostPacket = LostPacket SentPacket

mkSentPacket
    :: PacketNumber
    -> EncryptionLevel
    -> PlainPacket
    -> PeerPacketNumbers
    -> Bool
    -> SentPacket
mkSentPacket mypn lvl ppkt ppns ackeli =
    SentPacket
        { spPlainPacket = ppkt
        , spTimeSent = timeMicrosecond0
        , spSentBytes = 0
        , spEncryptionLevel = lvl
        , spPacketNumber = mypn
        , spPeerPacketNumbers = ppns
        , spAckEliciting = ackeli
        }

fixSentPacket :: SentPacket -> Int -> Int -> SentPacket
fixSentPacket spkt bytes padLen =
    spkt
        { spPlainPacket =
            if padLen /= 0
                then addPadding padLen $ spPlainPacket spkt
                else spPlainPacket spkt
        , spSentBytes = bytes
        }

addPadding :: Int -> PlainPacket -> PlainPacket
addPadding n (PlainPacket hdr plain) = PlainPacket hdr plain'
  where
    plain' =
        plain
            { plainFrames = plainFrames plain ++ [Padding n]
            }

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

newtype SentPackets = SentPackets (Seq SentPacket) deriving (Eq)

emptySentPackets :: SentPackets
emptySentPackets = SentPackets Seq.empty

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

data RTT = RTT
    { latestRTT :: Microseconds
    -- ^ The most recent RTT measurement made when receiving an ack for
    --   a previously unacked packet.
    , smoothedRTT :: Microseconds
    -- ^ The smoothed RTT of the connection.
    , rttvar :: Microseconds
    -- ^ The RTT variation.
    , minRTT :: Microseconds
    -- ^ The minimum RTT seen in the connection, ignoring ack delay.
    , maxAckDelay1RTT :: Microseconds
    -- ^ The maximum amount of time by which the receiver intends to
    --   delay acknowledgments for packets in the ApplicationData packet
    --   number space.  The actual ack_delay in a received ACK frame may
    --   be larger due to late timers, reordering, or lost ACK frames.
    , ptoCount :: Int
    -- ^ The number of times a PTO has been sent without receiving
    --  an ack.
    }
    deriving (Show)

-- | The RTT used before an RTT sample is taken.
kInitialRTT :: Microseconds
kInitialRTT = Microseconds 333000

initialRTT :: RTT
initialRTT =
    RTT
        { latestRTT = Microseconds 0
        , smoothedRTT = kInitialRTT
        , rttvar = kInitialRTT !>>. 1
        , minRTT = Microseconds 0
        , maxAckDelay1RTT = Microseconds 0
        , ptoCount = 0
        }

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

data CCMode
    = SlowStart
    | Avoidance
    | Recovery
    deriving (Eq)

instance Show CCMode where
    show SlowStart = "slow_start"
    show Avoidance = "avoidance"
    show Recovery = "recovery"

data CC = CC
    { bytesInFlight :: Int
    -- ^ The sum of the size in bytes of all sent packets that contain
    --   at least one ack-eliciting or PADDING frame, and have not been
    --   acked or declared lost.  The size does not include IP or UDP
    --   overhead, but does include the QUIC header and AEAD overhead.
    --   Packets only containing ACK frames do not count towards
    --   bytes_in_flight to ensure congestion control does not impede
    --   congestion feedback.
    , congestionWindow :: Int
    -- ^ Maximum number of bytes-in-flight that may be sent.
    , congestionRecoveryStartTime :: Maybe TimeMicrosecond
    -- ^ The time when QUIC first detects congestion due to loss or ECN,
    --   causing it to enter congestion recovery.  When a packet sent
    --   after this time is acknowledged, QUIC exits congestion
    --   recovery.
    , ssthresh :: Int
    -- ^ Slow start threshold in bytes.  When the congestion window is
    --   below ssthresh, the mode is slow start and the window grows by
    --   the number of bytes acknowledged.
    , bytesAcked :: Int
    -- ^ Records number of bytes acked, and used for incrementing
    --   the congestion window during congestion avoidance.
    , numOfAckEliciting :: Int
    , ccMode :: CCMode
    }
    deriving (Show)

initialCC :: CC
initialCC =
    CC
        { bytesInFlight = 0
        , congestionWindow = 0
        , congestionRecoveryStartTime = Nothing
        , ssthresh = maxBound
        , bytesAcked = 0
        , numOfAckEliciting = 0
        , ccMode = SlowStart
        }

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

data LossDetection = LossDetection
    { largestAckedPacket :: PacketNumber
    , previousAckInfo :: AckInfo
    , timeOfLastAckElicitingPacket :: TimeMicrosecond
    , lossTime :: Maybe TimeMicrosecond
    }
    deriving (Show)

initialLossDetection :: LossDetection
initialLossDetection = LossDetection (-1) ackInfo0 timeMicrosecond0 Nothing

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

newtype MetricsDiff = MetricsDiff [(String, Int)]

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

data TimerType
    = LossTime
    | PTO
    deriving (Eq)

instance Show TimerType where
    show LossTime = "loss_time"
    show PTO = "pto"

data TimerExpired = TimerExpired

data TimerCancelled = TimerCancelled

data TimerInfo = TimerInfo
    { timerTime :: TimeMicrosecond
    , timerLevel :: EncryptionLevel
    , timerType :: TimerType
    }
    deriving (Eq, Show)

data TimerInfoQ
    = Empty
    | Next TimerInfo
    deriving (Eq)

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

makeSpaceDiscarded :: IO (Array EncryptionLevel (IORef Bool))
makeSpaceDiscarded = do
    i1 <- newIORef False
    i2 <- newIORef False
    i3 <- newIORef False
    i4 <- newIORef False
    let lst = [(InitialLevel, i1), (RTT0Level, i2), (HandshakeLevel, i3), (RTT1Level, i4)]
        arr = array (InitialLevel, RTT1Level) lst
    return arr

makeSentPackets :: IO (Array EncryptionLevel (IORef SentPackets))
makeSentPackets = do
    i1 <- newIORef emptySentPackets
    i2 <- newIORef emptySentPackets
    i3 <- newIORef emptySentPackets
    let lst = [(InitialLevel, i1), (HandshakeLevel, i2), (RTT1Level, i3)]
        arr = array (InitialLevel, RTT1Level) lst
    return arr

makeLossDetection :: IO (Array EncryptionLevel (IORef LossDetection))
makeLossDetection = do
    i1 <- newIORef initialLossDetection
    i2 <- newIORef initialLossDetection
    i3 <- newIORef initialLossDetection
    let lst = [(InitialLevel, i1), (HandshakeLevel, i2), (RTT1Level, i3)]
        arr = array (InitialLevel, RTT1Level) lst
    return arr

data LDCC = LDCC
    { ldccState :: ConnState
    , ldccQlogger :: QLogger
    , putRetrans :: PlainPacket -> IO ()
    , recoveryRTT :: IORef RTT
    , recoveryCC :: TVar CC
    , spaceDiscarded :: Array EncryptionLevel (IORef Bool)
    , sentPackets :: Array EncryptionLevel (IORef SentPackets)
    , lossDetection :: Array EncryptionLevel (IORef LossDetection)
    , -- The current timer key
      timerKey :: IORef (Maybe TimeoutKey)
    , -- The current timer value
      timerInfo :: IORef (Maybe TimerInfo)
    , lostCandidates :: TVar SentPackets
    , ptoPing :: TVar (Maybe EncryptionLevel)
    , speedingUp :: IORef Bool
    , pktNumPersistent :: IORef PacketNumber
    , peerPacketNumbers :: Array EncryptionLevel (IORef PeerPacketNumbers)
    , previousRTT1PPNs :: IORef PeerPacketNumbers -- for RTT1
    -- Pending timer value
    , timerInfoQ :: TVar TimerInfoQ
    }

makePPN :: IO (Array EncryptionLevel (IORef PeerPacketNumbers))
makePPN = do
    ref1 <- newIORef emptyPeerPacketNumbers
    ref2 <- newIORef emptyPeerPacketNumbers
    ref3 <- newIORef emptyPeerPacketNumbers
    -- using the ref for RTT0Level and RTT1Level
    let lst =
            [ (InitialLevel, ref1)
            , (RTT0Level, ref3)
            , (HandshakeLevel, ref2)
            , (RTT1Level, ref3)
            ]
        arr = array (InitialLevel, RTT1Level) lst
    return arr

newLDCC :: ConnState -> QLogger -> (PlainPacket -> IO ()) -> IO LDCC
newLDCC cs qLog put =
    LDCC cs qLog put
        <$> newIORef initialRTT
        <*> newTVarIO initialCC
        <*> makeSpaceDiscarded
        <*> makeSentPackets
        <*> makeLossDetection
        <*> newIORef Nothing
        <*> newIORef Nothing
        <*> newTVarIO emptySentPackets
        <*> newTVarIO Nothing
        <*> newIORef False
        <*> newIORef maxBound
        <*> makePPN
        <*> newIORef emptyPeerPacketNumbers
        <*> newTVarIO Empty

instance KeepQlog LDCC where
    keepQlog = ldccQlogger

instance Connector LDCC where
    getRole = role . ldccState
    getEncryptionLevel = readTVarIO . encryptionLevel . ldccState
    getMaxPacketSize = readIORef . maxPacketSize . ldccState
    getConnectionState = readTVarIO . connectionState . ldccState
    getPacketNumber = readIORef . packetNumber . ldccState
    getAlive = readIORef . connectionAlive . ldccState

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

instance Qlog SentPacket where
    qlog SentPacket{..} =
        "{\"raw\":{\"length\":"
            <> sw spSentBytes
            <> "},\"header\":{\"packet_type\":\""
            <> toLogStr (packetType hdr)
            <> "\",\"packet_number\":\""
            <> sw plainPacketNumber
            <> "\",\"dcid\":\""
            <> sw (headerMyCID hdr)
            <> "\"},\"frames\":"
            <> "["
            <> foldr (<>) "" (intersperse "," (map qlog plainFrames))
            <> "]"
            <> "}"
      where
        PlainPacket hdr Plain{..} = spPlainPacket

instance Qlog LostPacket where
    qlog (LostPacket SentPacket{..}) =
        "{\"header\":{\"packet_type\":\""
            <> toLogStr (packetType hdr)
            <> "\""
            <> ",\"packet_number\":"
            <> sw spPacketNumber
            <> "}}"
      where
        PlainPacket hdr _ = spPlainPacket

instance Qlog MetricsDiff where
    qlog (MetricsDiff []) = "{}"
    qlog (MetricsDiff (x : xs)) = "{" <> tv0 x <> foldr tv "" xs <> "}"
      where
        tv0 (tag, val) = "\"" <> toLogStr tag <> "\":" <> sw val
        tv (tag, val) pre = ",\"" <> toLogStr tag <> "\":" <> sw val <> pre

instance Qlog CCMode where
    qlog mode = "{\"new\":\"" <> sw mode <> "\"}"

instance Qlog TimerCancelled where
    qlog TimerCancelled = "{\"event_type\":\"cancelled\"}"

instance Qlog TimerExpired where
    qlog TimerExpired = "{\"event_type\":\"expired\"}"

instance Qlog (TimerInfo, Microseconds) where
    qlog (TimerInfo{..}, us) =
        "{\"event_type\":\"set\""
            <> ",\"timer_type\":\""
            <> sw timerType
            <> "\""
            <> ",\"packet_number_space\":\""
            <> packetNumberSpace timerLevel
            <> "\""
            <> ",\"delta\":"
            <> delta us
            <> "}"

packetNumberSpace :: EncryptionLevel -> LogStr
packetNumberSpace InitialLevel = "initial"
packetNumberSpace RTT0Level = "application_data"
packetNumberSpace HandshakeLevel = "handshake"
packetNumberSpace RTT1Level = "application_data"

delta :: Microseconds -> LogStr
delta (Microseconds n) = sw n

qlogSent :: (KeepQlog q, Qlog pkt) => q -> pkt -> TimeMicrosecond -> IO ()
qlogSent q pkt tim = keepQlog q $ QSent (qlog pkt) tim

qlogMetricsUpdated :: KeepQlog q => q -> MetricsDiff -> IO ()
qlogMetricsUpdated q m = do
    tim <- getTimeMicrosecond
    keepQlog q $ QMetricsUpdated (qlog m) tim

qlogPacketLost :: KeepQlog q => q -> LostPacket -> IO ()
qlogPacketLost q lpkt = do
    tim <- getTimeMicrosecond
    keepQlog q $ QPacketLost (qlog lpkt) tim

qlogContestionStateUpdated :: KeepQlog q => q -> CCMode -> IO ()
qlogContestionStateUpdated q mode = do
    tim <- getTimeMicrosecond
    keepQlog q $ QCongestionStateUpdated (qlog mode) tim

qlogLossTimerUpdated :: KeepQlog q => q -> (TimerInfo, Microseconds) -> IO ()
qlogLossTimerUpdated q tmi = do
    tim <- getTimeMicrosecond
    keepQlog q $ QLossTimerUpdated (qlog tmi) tim

qlogLossTimerCancelled :: KeepQlog q => q -> IO ()
qlogLossTimerCancelled q = do
    tim <- getTimeMicrosecond
    keepQlog q $ QLossTimerUpdated (qlog TimerCancelled) tim

qlogLossTimerExpired :: KeepQlog q => q -> IO ()
qlogLossTimerExpired q = do
    tim <- getTimeMicrosecond
    keepQlog q $ QLossTimerUpdated (qlog TimerExpired) tim
