{-# LANGUAGE Trustworthy #-}
module Hails.Database.MongoDB.TCB.Convert ( -- * Converting HTTP requests
                                            -- to 'Labeled' 'Document'
                                            labeledDocI
                                          ) where

import LIO
import LIO.TCB
import qualified Data.Bson as B (Value(..))
import qualified Data.UString as U
import Data.IterIO
import Data.IterIO.Http
import qualified Data.ByteString.Lazy.Char8 as L
import qualified Data.ByteString.Char8 as S
import Data.List
import Data.List.Utils
import Data.UString (pack)
import Hails.Data.LBson.TCB


-- | Trusted transformer that takes a 'Labeled' tuple with 'HttpReq'
-- and the request body as a 'L.ByteString' and returns a 'Labeled'
-- 'Document' with keys and values corresponding to the form fields
-- from the request. The label on the @Labeled@ result is the same as
-- input. Arguments values are parsed in to BSON Strings except if the
-- key is of the form \"key_name[]\" in which case all such arguments
-- will be combined into an array of Strings.
labeledDocI :: (LabelState l p s)
                  => HttpReq a
                  -> Labeled l L.ByteString
                  -> LIO l p s (Labeled l (Document l))
labeledDocI req lbody = do
  let lbl = labelOf lbody
  doc <- enumPure (unlabelTCB lbody) |$ formFolder req
  return $ labelTCB lbl doc

-- | Parases query or request body into a BSON document. Query components
-- become 'Key' 'String' pairs in the BSON doc. If a query argument has the
-- form \"key1[]=blah\" it will be parsed as an array of 'String's and equally
-- named arguments will be combined.
formFolder :: (LabelState l p s)
           => HttpReq a -> Iter L.ByteString (LIO l p s) (Document l)
formFolder req = foldForm req docontrol []
  where docontrol acc field = do
          formVal <- fmap L.unpack pureI
          let k = S.unpack $ ffName field
          if endswith "[]" k then
            return $ appendVal acc k formVal
            else do
              let lfld = pack (S.unpack.ffName $ field) =: formVal
              return $ lfld : acc

-- | Appends the a value to the corresponding field in a document. If the field
-- already exists in the document, appends the value to the array. Otherwise the
-- field is added with the passed in value the only element in the array.
appendVal :: LabelState l p s => Document l -> String -> String -> Document l
appendVal doc k' formVal =
  let k = U.pack $ takeWhile (/= '[') k'
      field = (k := BsonVal (B.Array [B.String $ U.pack formVal]))
  in case find (isKey k) doc of
        Just _ -> map (upsert k) doc
        Nothing -> field:doc
  where upsert k f@(k0 := (BsonVal (B.Array arr))) =
          if k0 == k then
            (k =: (B.String $ U.pack formVal):arr)
            else f
        upsert _ f = f
        isKey kk (k := _) = k == kk