-- Keyring.hs: OpenPGP (RFC4880) transferable keys parsing
-- Copyright © 2012-2014  Clint Adams
-- This software is released under the terms of the Expat license.
-- (See the LICENSE file).

module Data.Conduit.OpenPGP.Keyring (
   conduitToTKs
 , conduitToTKsDropping
 , sinkKeyringMap
) where

import Control.Applicative (many, (<$>), (<|>))
import Control.Monad.Trans.Resource (MonadResource)
import Data.Conduit
import qualified Data.Conduit.List as CL
import Data.IxSet (empty, insert)
import Data.Monoid (Monoid, (<>), mconcat)

import Codec.Encryption.OpenPGP.Types
import Data.Conduit.OpenPGP.Keyring.Instances ()
import Text.ParserCombinators.Incremental.LeftBiasedLocal (concatMany, feed, feedEof, inspect, satisfy, Parser)

data Phase = MainKey | Revs | Uids | UAts | Subs | SkippingBroken
    deriving (Eq, Ord, Show)

conduitToTKs :: MonadResource m => Conduit Pkt m TK
conduitToTKs = conduitToTKs' True

conduitToTKsDropping :: MonadResource m => Conduit Pkt m TK
conduitToTKsDropping = conduitToTKs' False

fakecmAccum :: MonadResource m => (accum -> (accum, [b])) -> (a -> accum -> (accum, [b])) -> accum -> Conduit a m b
fakecmAccum finalizer f =
    loop
  where
    loop accum =
        await >>= maybe (Prelude.mapM_ yield (snd (finalizer accum))) go
      where
        go a = do
            let (accum', bs) = f a accum
            Prelude.mapM_ yield bs
            loop accum'

conduitToTKs' :: MonadResource m => Bool -> Conduit Pkt m TK
conduitToTKs' intolerant = CL.filter notTrustPacket =$= CL.map (:[]) =$= fakecmAccum finalizeParsing (parseAChunk (parseTK intolerant)) ([], Just (Nothing, parseTK intolerant)) =$= CL.catMaybes
    where
        notTrustPacket (TrustPkt _) = False
        notTrustPacket _ = True

parseAChunk :: (Monoid s, Show s) => Parser s r -> s -> ([(r, s)], Maybe (Maybe (r -> r), Parser s r)) -> (([(r, s)], Maybe (Maybe (r -> r), Parser s r)), [r])
parseAChunk _ a ([], Nothing) = error $ "Failure before " ++ show a
parseAChunk op a (cr, Nothing) = (inspect (feed (mconcat (map snd cr) <> a) op), map fst cr)
parseAChunk _ a (_, Just (_, p)) = (inspect (feed a p), [])

finalizeParsing :: Monoid s => ([(r, s)], Maybe (Maybe (r -> r), Parser s r)) -> (([(r, s)], Maybe (Maybe (r -> r), Parser s r)), [r])
finalizeParsing ([], Nothing) = error $ "Unexpected finalization failure"
finalizeParsing (cr, Nothing) = (([], Nothing), map fst cr)
finalizeParsing (_, Just (_, p)) = finalizeParsing (inspect (feedEof p))

parseTK :: Bool -> Parser [Pkt] (Maybe TK)
parseTK True = publictk True <|> secrettk True
parseTK False = publictk False <|> secrettk False <|> brokentk 6 <|> brokentk 5

data UidOrUat = I String | A [UserAttrSubPacket]
    deriving Show

splitUs :: [(UidOrUat, [SignaturePayload])] -> ([(String, [SignaturePayload])], [([UserAttrSubPacket], [SignaturePayload])])
splitUs us = (is, as)
    where
        is = map unI (filter isI us)
        as = map unA (filter isA us)
	isI ((I _), _) = True
	isI _ = False
	isA ((A _), _) = True
	isA _ = False
	unI ((I x), y) = (x, y)
	unI x = error $ "unI should never be called on " ++ show x
	unA ((A x), y) = (x, y)
	unA x = error $ "unA should never be called on " ++ show x

publictk, secrettk :: Bool -> Parser [Pkt] (Maybe TK)
publictk intolerant = do
    pkp <- pkpayload
    pkpsigs <- concatMany (signature intolerant [KeyRevocationSig,SignatureDirectlyOnAKey])
    (uids, uats) <- fmap splitUs (many (signeduid intolerant <|> signeduat intolerant)) -- FIXME: require >=1 uid if intolerant
    subs <- concatMany (pubsub intolerant)
    return $! Just (TK pkp pkpsigs uids uats subs)
        where
            pubsub True = signedorrevokedpubsubkey True
            pubsub False = signedorrevokedpubsubkey False <|> brokenpubsubkey
secrettk intolerant = do
    skp <- skpayload
    skpsigs <- concatMany (signature intolerant [KeyRevocationSig,SignatureDirectlyOnAKey])
    (uids, uats) <- fmap splitUs (many (signeduid intolerant <|> signeduat intolerant)) -- FIXME: require >=1 uid if intolerant?
    subs <- concatMany (secsub intolerant)
    return $! Just (TK skp skpsigs uids uats subs)
        where
            secsub True = raworsignedorrevokedsecsubkey True
            secsub False = raworsignedorrevokedsecsubkey False <|> brokensecsubkey

brokentk :: Int -> Parser [Pkt] (Maybe TK)
brokentk 6 = do
    _ <- broken 6
    _ <- many (signature False [KeyRevocationSig,SignatureDirectlyOnAKey])
    _ <- many (signeduid False <|> signeduat False)
    _ <- concatMany (signedorrevokedpubsubkey False <|> brokenpubsubkey)
    return Nothing
brokentk 5 = do
    _ <- broken 5
    _ <- many (signature False [KeyRevocationSig,SignatureDirectlyOnAKey])
    _ <- many (signeduid False <|> signeduat False)
    _ <- concatMany (raworsignedorrevokedsecsubkey False <|> brokensecsubkey)
    return Nothing
brokentk _ = fail "Unexpected broken packet type"

pkpayload :: Parser [Pkt] (PKPayload, Maybe SKAddendum)
pkpayload = do [PublicKeyPkt p] <- satisfy isPKP
               return (p, Nothing)
    where
        isPKP [PublicKeyPkt _] = True
        isPKP _ = False

signature :: Bool -> [SigType] -> Parser [Pkt] [SignaturePayload]
signature intolerant rts = if intolerant then signature' else (signature' <|> brokensig')
    where
        signature' = do [SignaturePkt sp] <- satisfy (isSP intolerant)
                        return $! (if intolerant then id else filter isSP') [sp]
        brokensig' = const [] <$> broken 2
        isSP True [SignaturePkt sp@(SigV3 {})] = isSP' sp
        isSP True [SignaturePkt sp@(SigV4 {})] = isSP' sp
        isSP False [SignaturePkt _] = True
        isSP _ _ = False
	isSP' (SigV3 st _ _ _ _ _ _) = st `elem` rts
	isSP' (SigV4 st _ _ _ _ _ _) = st `elem` rts
	isSP' _ = False

signeduid :: Bool -> Parser [Pkt] (UidOrUat, [SignaturePayload])
signeduid intolerant = do [UserIdPkt u] <- satisfy isUID
                          sigs <- concatMany (signature intolerant [GenericCert, PersonaCert, CasualCert, PositiveCert, CertRevocationSig])
                          return (I u, sigs)
    where
        isUID [UserIdPkt _] = True
        isUID _ = False

signeduat :: Bool -> Parser [Pkt] (UidOrUat, [SignaturePayload])
signeduat intolerant = do [UserAttributePkt us] <- satisfy isUAt
                          sigs <- concatMany (signature intolerant [GenericCert, PersonaCert, CasualCert, PositiveCert, CertRevocationSig])
                          return (A us, sigs)
    where
        isUAt [UserAttributePkt _] = True
        isUAt _ = False

signedorrevokedpubsubkey :: Bool -> Parser [Pkt] [(Pkt, [SignaturePayload])]
signedorrevokedpubsubkey intolerant = do [p] <- satisfy isPSKP
                                         sigs <- concatMany (signature intolerant [SubkeyBindingSig, SubkeyRevocationSig])
                                         return [(p, sigs)]
    where
        isPSKP [PublicSubkeyPkt _] = True
        isPSKP _ = False

brokenpubsubkey :: Parser [Pkt] [(Pkt, [SignaturePayload])]
brokenpubsubkey = do _ <- broken 14
                     _ <- concatMany (signature False [SubkeyBindingSig, SubkeyRevocationSig])
                     return []

raworsignedorrevokedsecsubkey :: Bool -> Parser [Pkt] [(Pkt, [SignaturePayload])]
raworsignedorrevokedsecsubkey intolerant = do [p] <- satisfy isSSKP
                                              sigs <- concatMany (signature intolerant [SubkeyBindingSig, SubkeyRevocationSig])
                                              return [(p, sigs)]
    where
        isSSKP [SecretSubkeyPkt _ _] = True
        isSSKP _ = False

brokensecsubkey :: Parser [Pkt] [(Pkt, [SignaturePayload])]
brokensecsubkey = do _ <- broken 7
                     _ <- concatMany (signature False [SubkeyBindingSig, SubkeyRevocationSig])
                     return []

skpayload :: Parser [Pkt] (PKPayload, Maybe SKAddendum)
skpayload = do [SecretKeyPkt p ska] <- satisfy isSKP
               return (p, Just ska)
    where
        isSKP [SecretKeyPkt _ _] = True
        isSKP _ = False

broken :: Int -> Parser [Pkt] Pkt
broken t = do [bp] <- satisfy isBroken
              return bp
    where
        isBroken [BrokenPacketPkt _ a _] = t == fromIntegral a
        isBroken _ = False

sinkKeyringMap :: MonadResource m => Sink TK m Keyring
sinkKeyringMap = CL.fold (flip insert) empty