{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Symantic.HTTP.Pipes where

import Control.Arrow (first, right)
import Control.Monad (Monad(..))
import Control.Monad.Trans.Class (MonadTrans(..))
import Data.Bool
import Data.Either (Either(..))
import Data.Eq (Eq(..))
import Data.Function (($), (.))
import Data.Functor ((<$>))
import Data.Maybe (Maybe(..))
import Data.Ord (Ord(..))
import Data.Semigroup (Semigroup(..))
import Data.String (String, IsString(..))
import Data.Word (Word8)
import Prelude (fromIntegral, Num(..))
import System.IO (IO)
import Text.Show (Show(..))
import qualified Control.Monad.Classes as MC
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Lazy.Char8 as BSL8
import qualified Data.Char as Char
import qualified Data.List as List
import qualified Lens.Family as Lens
import qualified Lens.Family.State.Strict as Lens
import qualified Pipes as P
import qualified Pipes.ByteString as Pbs
import qualified Pipes.Group as Pg
import qualified Pipes.Parse as Pp
import qualified Pipes.Safe as Ps

import Symantic.HTTP.API

instance IsString () where
	fromString _ = ()

-- | Pass any executable effect to the underlying 'Monad'.
type instance MC.CanDo (P.Effect m) (MC.EffExec w) = 'False
-- | Pass any executable effect to the underlying 'Monad'.
type instance MC.CanDo (P.Proxy a' a b' b m) (MC.EffExec w) = 'False

type instance FramingMonad  (P.Producer a m r) = m
type instance FramingYield  (P.Producer a m r) = a
type instance FramingReturn (P.Producer a m r) = r

type instance FramingMonad  (P.ListT m a) = m
type instance FramingYield  (P.ListT m a) = a
type instance FramingReturn (P.ListT m a) = ()

-- | Produce 'BS.ByteString' from a 'Monad'.
produceBS ::
 IsString r =>
 Monad m =>
 m BS.ByteString -> P.Producer' BS.ByteString m r
produceBS mbs = go
	where
	go = do
		bs <- lift mbs
		if BS.null bs
		then return ""
		else do
			P.yield bs
			go

-- * 'NoFraming'
instance FramingEncode NoFraming (P.Producer a IO r) where
	framingEncode _framing mimeEnc p =
		right (first mimeEnc) <$> P.next p
instance FramingEncode NoFraming (P.Producer a (Ps.SafeT IO) r) where
	framingEncode _framing mimeEnc p =
		right (first mimeEnc) <$> Ps.runSafeT (P.next p)
instance FramingEncode NoFraming (P.ListT IO a) where
	framingEncode _framing mimeEnc p =
		right (\(a,n) -> (mimeEnc a, P.Select n)) <$> P.next (P.enumerate p)
instance IsString r => FramingDecode NoFraming (P.Producer a m r) where
	framingDecode _framing mimeDec mbs =
		-- TODO: use drawAll
		produceBS mbs P.>-> go
		where go = do
			bs <- P.await
			case mimeDec $ BSL.fromStrict bs of
			 Left err -> return $ fromString err
			 Right a -> P.yield a >> go

-- * 'NewlineFraming'
-- TODO: see how to use Pbs._unlines?
instance FramingEncode NewlineFraming (P.Producer a IO r) where
	framingEncode _framing mimeEnc p =
		right (first (newlineEncode mimeEnc))
		 <$> P.next p
instance FramingEncode NewlineFraming (P.Producer a (Ps.SafeT IO) r) where
	framingEncode _framing mimeEnc p =
		right (first (newlineEncode mimeEnc))
		 <$> Ps.runSafeT (P.next p)
instance IsString r => FramingDecode NewlineFraming (P.Producer a m r) where
	framingDecode _framing mimeDec mbs =
		Pg.concats $
		Pg.maps
		 (\p -> P.for p $ \bs ->
			case mimeDec $ BSL.fromStrict bs of
			 Left _err -> return ()
			 Right a  -> P.yield a) $
		Lens.view Pbs.lines $
		produceBS mbs

newlineEncode :: (a -> BSL.ByteString) -> a -> BSL.ByteString
newlineEncode mimeEnc a = mimeEnc a <> BSL.singleton (fromIntegral (Char.ord '\n'))

instance FramingEncode NetstringFraming (P.Producer a IO r) where
	framingEncode _framing mimeEnc p =
		right (first (encodeNetstring mimeEnc))
		 <$> P.next p
instance FramingEncode NetstringFraming (P.Producer a (Ps.SafeT IO) r) where
	framingEncode _framing mimeEnc p =
		right (first (encodeNetstring mimeEnc))
		 <$> Ps.runSafeT (P.next p)
instance IsString r => FramingDecode NetstringFraming (P.Producer a m r) where
	framingDecode _framing mimeDec mbs =
		Pg.concats $
		parseMany
		 (Pp.execStateT $ decodeNetstring @r mimeDec)
		 (produceBS mbs)

digit0, digit9 :: Word8
colon, comma :: Word8
newline :: Word8
digit0  = fromIntegral (Char.ord '0')
digit9  = fromIntegral (Char.ord '9')
colon   = fromIntegral (Char.ord ':')
comma   = fromIntegral (Char.ord ',')
newline = fromIntegral (Char.ord '\n')

encodeNetstring :: (a -> BSL.ByteString) -> a -> BSL.ByteString
encodeNetstring mimeEnc a =
	let bs = mimeEnc a in
	BSL8.pack (show (BSL8.length bs))
	 <> ":" <> bs <> ","

-- TODO: write something like Pbs._lines to form a Lens'
decodeNetstring ::
 IsString r =>
 Monad m =>
 (BSL.ByteString -> Either String a) ->
 ParserP BS.ByteString a m r
decodeNetstring mimeDec = do
	lenBSs <- Lens.zoom (Pbs.span (\b -> digit0 <= b && b <= digit9)) drawAllP
	case lenBSs >>= BS.unpack of
	 [] -> return "empty length"
	 w0:_:_ | w0 == digit0 -> return "leading zero"
	 lenWs -> do
		let len = List.foldl' (\acc d -> acc * 10 + (fromIntegral d - digit0)) 0 lenWs
		colonW <- drawByteP
		if colonW /= Just colon
		then return "colon expected"
		else do
			-- TODO: make mimeDecode directly able to use Pipes?
			dataBS <- BSL.fromChunks <$> Lens.zoom (Pbs.splitAt len) drawAllP
			commaW <- drawByteP
			if commaW /= Just comma
			then return "comma expected"
			else do
				case mimeDec dataBS of
				 Left err -> return $ fromString err
				 Right a -> do
					yieldP a
					decodeNetstring mimeDec

-- * Type 'P.Parser'

-- | A 'P.Parser', which is itself a 'P.Producer',
-- and thus can 'yieldP' immediately.
type ParserP inp out m r =
 forall x. Pp.StateT (P.Producer inp m x) (P.Producer out m) r

yieldP :: Monad m => out -> ParserP inp out m ()
yieldP = lift . P.yield

drawP :: Monad m => ParserP inp out m (Maybe inp)
drawP = P.hoist lift Pp.draw

drawAllP :: Monad m => ParserP inp out m [inp]
drawAllP = P.hoist lift Pp.drawAll

drawByteP :: Monad m => ParserP BS.ByteString out m (Maybe Word8)
drawByteP = P.hoist lift Pbs.drawByte

unDrawP :: Monad m => inp -> ParserP inp out m ()
unDrawP = P.hoist lift . Pp.unDraw

-- | @'parseMany' f@ groups a 'P.Producer' of @(inp)@
-- into a series of 'P.Producer's of @(a)@ delimited by 'f'
-- (which must drop the delimiter).
parseMany ::
 forall m inp r a.
 Monad m =>
 (P.Producer inp m r -> P.Producer a m (P.Producer inp m r)) ->
 P.Producer inp m r ->
 Pg.FreeT (P.Producer a m) m r
parseMany f = go0
	where
	go0 ::
	 P.Producer inp m r ->
	 Pg.FreeT (P.Producer a m) m r
	go0 p = Pg.FreeT $ do
		P.next p >>= \case
		 Left r -> return $ Pg.Pure r
		 Right (inp, p') -> return $ Pg.Free $ go1 $ P.yield inp >> p'
	
	go1 ::
	 P.Producer inp m r ->
	 P.Producer a m (Pg.FreeT (P.Producer a m) m r)
	go1 p = go0 <$> f p

{-
-- * Type |Lens'|
-- | Package agnostic lens.
type Lens' a b = forall f . Functor f => (b -> f b) -> (a -> f a)
(^.) :: a -> ((b -> Constant b b) -> (a -> Constant b a)) -> b
a ^. lens = getConstant (lens Constant a)
-}