{-# LANGUAGE OverloadedStrings #-}

module Main where

import           Helper ((-), log)
import           Prelude hiding ((-), log)
import qualified Prelude as Prelude

import           Control.Concurrent.Async (async, waitAnyCatchCancel, waitEitherCancel)
import           Control.Lens
import           Control.Monad (replicateM_, join, when)
import           Control.Monad.IO.Class (MonadIO(..))
import           Control.Monad.Trans.State.Strict (runStateT)
import           Data.Attoparsec.ByteString (parse, maybeResult)
import           Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Builder as Builder
import qualified Data.ByteString.Char8 as BC
import           Data.Monoid ((<>))
import qualified Data.Text as T
import           Data.Text.Lens
import           Data.Text.Strict.Lens (utf8)
import           Network.Simple.TCP (send)
import qualified Network.Socket as NS
import           Pipes (runEffect, (>->), Pipe, for, yield, cat, Producer, Consumer)
import qualified Pipes.Attoparsec as PA
import           Pipes.Network.TCP.Safe
import qualified Pipes.Prelude as P
import           Pipes.Safe
import           System.Random (randomIO)
import           Options.Generic
import           System.IO (hSetBuffering, stdin, stdout, BufferMode(LineBuffering))

import           NetworkHelper (setSocketCommon)
import           Options (toConfig)
import           Parser
import           Type


obfsEncode :: (MonadIO m) => Bool -> Integer -> Pipe ByteString ByteString m a
obfsEncode debug randomness = do
  for cat - \x -> do
    let payloadLen = (fromIntegral - B.length x)
    if (payloadLen >= fromIntegral randomness)
      then do
        let paddingLen = 0
        let chunk =
              view strict - Builder.toLazyByteString -
                Builder.word32BE paddingLen
                <> Builder.byteString
                      (BC.replicate (fromIntegral paddingLen) - '.')
                <> Builder.word32BE payloadLen
                <> Builder.byteString x

        when debug - log - "E: " <> (T.pack - show - B.length chunk)
        yield chunk
      else
        randomLenCheckLoop x
  where
    randomLenCheckLoop payload = do
      let payloadLen = (fromIntegral - B.length payload)
      randomLen <- liftIO - randomIO <&> (`mod` fromIntegral randomness)
      if (payloadLen <= randomLen)
        then
          do
            let paddingLen = randomLen Prelude.- payloadLen
            let chunk =
                  view strict - Builder.toLazyByteString -
                    Builder.word32BE paddingLen
                    <> Builder.byteString
                        (BC.replicate (fromIntegral paddingLen) - '.')
                    <> Builder.word32BE payloadLen
                    <> Builder.byteString payload

            when debug - log - "E: " <> (T.pack - show - B.length chunk)
            yield chunk
        else
          do
            let paddingLen = 0
                (now, next) = B.splitAt (fromIntegral randomLen) payload

            let chunk =
                  view strict - Builder.toLazyByteString -
                    Builder.word32BE paddingLen
                    <> Builder.byteString
                        (BC.replicate (fromIntegral paddingLen) - '.')
                    <> Builder.word32BE (fromIntegral - B.length now)
                    <> Builder.byteString now

            when debug - log - "E: " <> (T.pack - show - B.length chunk)
            yield chunk

            randomLenCheckLoop next


obfsDecode :: (MonadIO m) => Producer ByteString m a -> (ByteString -> m ()) -> m ()
obfsDecode pull sink = loop pull
  where
    loop x = do
      (r, next) <- runStateT (PA.parse obfsParser) x
      case r of
        Just (Right result@(_,_,_,payload)) -> do
          -- log - view packed - show result
          sink payload
          loop next
        _ -> pure ()

main :: IO ()
main = do
  hSetBuffering stdin LineBuffering
  hSetBuffering stdout LineBuffering

  config <- getRecord "a TCP tunnel with packet length obfuscation" <&> toConfig

  log - T.pack - show config

  let _mtu = fromIntegral - config ^. mtu + (-8) -- 2 word32be
      _timeout = fromIntegral - config ^. timeoutInSeconds * 1000000

  let _localHost = config ^. localHost . re packed
      _localPort = show - config ^. localPort

      _remoteHost = config ^. remoteHost . re packed
      _remotePort = show - config ^. remotePort

      _fromSocket = fromSocketTimeout _timeout
      _toSocket = toSocketTimeout _timeout

      _debug = config ^. debug

  localThread <- pure -
    runSafeT . runEffect - serve (Host _localHost) _localPort - \(localSock, localSockAddr) -> do
      log - "local accepted: " <> view packed (show localSockAddr)
      setSocketCommon localSock

      runSafeT . runEffect -
        connect _remoteHost _remotePort - \(remoteSock, remoteSockAddr) -> do
          liftIO - setSocketCommon remoteSock

          let
              localPull = _fromSocket localSock _mtu

              remotePull = _fromSocket remoteSock _mtu
              remotePush = _toSocket remoteSock


          sendThread <- liftIO - async - runEffect - do
            localPull >-> obfsEncode _debug (config ^. randomnessInBytes)
                      >-> remotePush
            liftIO - NS.shutdown remoteSock NS.ShutdownSend

          recvThread <- liftIO - async - do
            obfsDecode remotePull - send localSock
            liftIO - NS.shutdown localSock NS.ShutdownSend

          liftIO - waitEitherCancel sendThread recvThread

          pure ()

  remoteThread <- pure -
    runSafeT . runEffect - serve (Host _remoteHost) _remotePort - \(remoteSock, remoteSockAddr) -> do
      log - "remote accepted: " <> view packed (show remoteSockAddr)
      setSocketCommon remoteSock

      let _forwardHost = config ^. forwardHost . re packed
          _forwardPort = show - config ^. forwardPort

      runSafeT . runEffect -
        connect _forwardHost _forwardPort - \(forwardSock, forwardSockAddr) -> do
          liftIO - setSocketCommon forwardSock

          let
              remotePull = _fromSocket remoteSock _mtu
              remotePush = _toSocket remoteSock

              forwardPull = _fromSocket forwardSock _mtu

          sendThread <- liftIO - async - do
            obfsDecode remotePull - send forwardSock
            liftIO - NS.shutdown forwardSock NS.ShutdownSend

          recvThread <- liftIO - async - runEffect - do
            forwardPull >-> obfsEncode _debug (config ^. randomnessInBytes)
                        >-> remotePush
            liftIO - NS.shutdown remoteSock NS.ShutdownSend

          liftIO - waitEitherCancel sendThread recvThread

          pure ()


  case config ^. role of
    Local -> join localThread
    Remote -> join remoteThread
    Both -> do
      local <- async - join localThread
      remote <- async - join remoteThread
      waitEitherCancel local remote
      pure ()

  pure ()
