-- Module  : Data.FIX.Parser
-- License : LGPL-2.1 

{-# OPTIONS_GHC  -fno-warn-missing-signatures #-}

module Data.FIX.Parser 
    ( 
-- * Introduction
-- | In order to get a Parser 'FIXMessage' 'FIXSpec' you can 
--
-- @
--import qualified Data.FIX.Parser as FIX ( nextP, messageP )
--
--FIX.nextP >>= FIX.messageP
-- @
      messageP
    , groupP
    , nextP
    , nextP'
    , toFIXInt
    , toFIXChar
    , toFIXString
    , toFIXDouble
    , toFIXBool
    , toFIXMultipleValueString
    , toFIXTimestamp
    , toFIXTimeOnly
    , toFIXData
    , toFIXDateOnly
    , toFIXMonthYear
    , tBeginString
    , tCheckSum
    , tBodyLength
    , tMsgType
    ) where

import Prelude hiding ( take, null, head, tail )
import Data.FIX.Message 
    ( FIXGroupElement(..), FIXTags, FIXSpec (..), FIXMessage (..)
    , FIXTag (..), FIXValue (..), FIXMessageSpec (..)
    , FIXValues, FIXGroupSpec (..) )
import qualified Data.FIX.Message as FIX ( checksum )
import qualified Data.FIX.Common as FIX ( delimiter )
import Data.FIX.ParserCombinators ( toTag, toString, toInt, toInt', toBool, toChar, toTimestamp, toDateOnly, toMonthYear, toTimeOnly, toDouble )
import Data.Attoparsec ( parseOnly, option, Result(..), Parser, count, string, take )
import Data.Char ( ord )
import Data.ByteString ( ByteString )
import Data.FIX.Arbitrary ()
import qualified Data.ByteString.Char8 as C ( length, pack ) 
import Data.Maybe ( fromMaybe )
import qualified Data.LookupTable as LT ( new, toList, lookup, fromList, insert )
import Control.Applicative ( (<$>) )
import Control.Monad ( liftM )
import Test.QuickCheck ( arbitrary )

-- parse a specific tag and its value
tagP :: FIXTag -> Parser FIXValue
tagP tag = do l <- toTag -- read out tag in message
              if l == tnum tag then -- if the two tags coincide read the value
                tparser tag else fail "wrong tag"

-- parse all the specificed tags and their corresponding values
tagsP :: FIXTags -> Parser FIXValues
tagsP ts = option LT.new (insertValue LT.new)
	where
           insertValue :: FIXValues -> Parser FIXValues
	   insertValue t = do 
	   	l <- nextTag
		val <- tparser l
		let t' = LT.insert (tnum l) val t 
		option t' (insertValue t') 
	   
	   nextTag :: Parser FIXTag
	   nextTag = do 
	   	tag' <- toTag 
		let mtag' = liftM return $ LT.lookup tag' ts 
		fromMaybe (fail "") mtag'

	   {-errorMsg l = error $ "unknown tag " ++ show l ++-}
		   {-"\nknown tags are " ++ show (LT.toList ts)-}

-- parse a value of type FIX group
groupP :: FIXGroupSpec -> Parser FIXValue
groupP spec = let numTag = gsLength spec in 
		 do n <- toInt           -- number of submessages
                    b <- count n submsg  -- parse the submessages
                    return $ FIXGroup n b
              where
                  submsg :: Parser FIXGroupElement
                  submsg = 
                    let sepTag = gsSeperator spec
                    in do s  <- tagP sepTag -- The seperator of the message
		    	  vs <- tagsP (gsBody spec) -- The rest of the message
			  return (FIXGroupElement (tnum sepTag) s vs)

-- | Match the next FIX message (only text) in the stream. The checksum is
-- validated.
nextP :: Parser ByteString
nextP = do 
    (hchksum, len) <- _header'
    msg <- take len 
    let chksum = (hchksum + FIX.checksum msg) `mod` 256  
    c <- _calcChksum'
    if chksum == c then return msg 
        else fail $ "checksum is not valid: is " ++ 
                    show chksum  ++ " should be " ++ show c
    where
        -- Parse header and return checksum and length.
        -- A header always starts with the version tag (8)  
        -- followed by the length tag (9). Note: these 2 tags
        -- are included in the checksum
        _header' :: Parser (Int, Int)
        _header' = do 
            c1 <- FIX.checksum <$> string (C.pack "8=")
            c2 <- FIX.checksum <$> toString
            c3 <- FIX.checksum <$> string (C.pack "9=")
            l <- toString
            let c4 = FIX.checksum l
                c = (c1 + c2 + c3 + c4 + 2 * ord FIX.delimiter) `mod` 256
            return (c, toInt' l)

        _calcChksum' :: Parser Int
        _calcChksum' = string (C.pack "10=") >> toInt

-- | Match the next FIX message (only text) in the stream. The checksum is NOT
-- validated.
nextP' :: Parser ByteString
nextP' = do 
          msg <- take =<< _numBytes
          _ <- toTag >> toInt
          return msg 
    where
        _numBytes = 
            let 
                skipHeader = 
                    string (C.pack "8=") >> toString >> 
                    string (C.pack "9=") 
            in 
                skipHeader >> toInt

-- | Given the FIX specification deserialize the FIX message.
messageP :: FIXSpec -> ByteString -> Parser (FIXMessage FIXSpec)
messageP spec msg = 
    let headerP'  = tagsP $ fsHeader spec  -- parse header
        trailerP' = tagsP $ fsTrailer spec -- parse trailer
        bodyP' mtype =                     -- parse body
            let allSpecs = fsMessages spec
                msgSpec = fromMaybe (error "no message") $
                                LT.lookup mtype allSpecs
                msgBodyTags = msBody msgSpec
            in 
                tagsP msgBodyTags

        fixP' = do
                FIXString mt <- tagP tMsgType
                h <- headerP'
                b <- bodyP' mt
                t <- trailerP' 
                return FIXMessage 
                    { mContext = spec
                    , mType = mt
                    , mHeader = h
                    , mBody = b
                    , mTrailer = t }
    in 
        case parseOnly fixP' msg of
             Left err  -> fail err 
	     Right msg -> return msg


-- FIX value parsers 
toFIXInt = FIXInt <$> toInt
toFIXDouble = FIXDouble <$> toDouble
toFIXBool = FIXBool <$> toBool
toFIXString = FIXString <$> toString
toFIXMultipleValueString = FIXMultipleValueString <$> toString
toFIXData = FIXData <$> toString
toFIXTimestamp = FIXTimestamp <$> toTimestamp
toFIXTimeOnly = FIXTimeOnly <$> toTimeOnly
toFIXChar = FIXChar <$> toChar
toFIXDateOnly = FIXDateOnly <$> toDateOnly
toFIXMonthYear = FIXMonthYear <$> toMonthYear


tBeginString :: FIXTag
tBeginString = FIXTag
    { tName = "BeginString"
    , tnum = 8
    , tparser = toFIXString
    , arbitraryValue = FIXString <$> arbitrary }


tBodyLength :: FIXTag
tBodyLength = FIXTag
    { tName = "BodyLength"
    , tnum = 9
    , tparser = toFIXInt 
    , arbitraryValue = FIXInt <$> arbitrary }

tMsgType :: FIXTag
tMsgType = FIXTag
    { tName = "MsgType"
    , tnum = 35
    , tparser = toFIXString 
    , arbitraryValue = FIXString <$> arbitrary }

tCheckSum :: FIXTag
tCheckSum = FIXTag
    { tName = "CheckSum"
    , tnum = 10
    , tparser = toFIXInt 
    , arbitraryValue = FIXInt <$> arbitrary }