{-
    Copyright 2010-2016 Mario Blazevic

    This file is part of the Streaming Component Combinators (SCC) project.

    The SCC project is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
    License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
    version.

    SCC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
    of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along with SCC.  If not, see
    <http://www.gnu.org/licenses/>.
-}

{-# LANGUAGE Haskell2010, BangPatterns, ExistentialQuantification, FlexibleContexts, OverloadedStrings,
  ScopedTypeVariables #-}

module Main (main, parseWhole, parseChunked) where

import Prelude hiding (null, splitAt)
import Control.Applicative (Alternative, (<$>), (<*>), (<*), (*>), (<|>), many, pure)
import Control.Monad (void)
import Data.Foldable (foldl')
import Data.Monoid (Monoid, (<>), mappend, mempty)
import Data.Monoid.Textual (TextualMonoid)
import Data.Monoid.Factorial (FactorialMonoid (splitAt))
import Data.Monoid.Null (MonoidNull (null))
import Text.ParserCombinators.Incremental.LeftBiasedLocal

import Control.DeepSeq (NFData(..))
import Criterion.Main (bench, bgroup, defaultMain, nf)
   
import qualified Data.ByteString as B
import qualified Data.Text.IO as T

import Data.Monoid.Instances.ByteString.UTF8 (ByteStringUTF8(ByteStringUTF8))
import Data.Monoid.Instances.Concat (Concat, extract)

instance NFData ByteStringUTF8 where
  rnf (ByteStringUTF8 b) = rnf b

instance NFData a => NFData (Concat a) where
  rnf s = rnf (extract s)

endOfInput :: MonoidNull s => Parser s ()
endOfInput = eof

char :: TextualMonoid t => Char -> Parser t t
char = satisfyChar . (==)

sepBy1 :: Alternative f => f a -> f s -> f [a]
sepBy1 p q = (:) <$> p <*> many (q *> p)
             
lineEnd :: TextualMonoid t => Parser t ()
lineEnd = void (char '\n') <|> void (string "\r\n")
          <|> void (char '\r')
          <?> "end of line"

unquotedField :: TextualMonoid t => Parser t t
unquotedField = takeCharsWhile (`notElem` (",\n\r\"" :: [Char]))
                <?> "unquoted field"

insideQuotes :: TextualMonoid t => Parser t t
insideQuotes = mappend <$> takeCharsWhile (/= '"')
               <*> concatMany (mappend <$> dquotes <*> insideQuotes)
               <?> "inside of double quotes"

   where dquotes = string "\"\"" >> return "\""
                   <?> "paired double quotes"

quotedField :: TextualMonoid t => Parser t t
quotedField = char '"' *> insideQuotes <* char '"'
              <?> "quoted field"

field :: TextualMonoid t => Parser t t
field = quotedField <|> unquotedField
        <?> "field"

record :: TextualMonoid t => Parser t [t]
record = field `sepBy1` char ','


-- file1 is not incremental because it's fallible
file1 :: TextualMonoid t => Parser t [[t]]
file1 = (:) <$> record
        +<*> manyTill (lineEnd *> ((:[]) <$> record))
                      (moptional lineEnd *> endOfInput)
        <?> "file"

file2 :: forall t. TextualMonoid t => Parser t [[t]]
file2 = (:) <$> record
        +<*> many ((notFollowedBy (moptional lineEnd *> endOfInput) :: Parser t ())
                  *> lineEnd *> record)
        <?> "file"

parseWhole :: TextualMonoid t => Parser t [[t]] -> t -> [([[t]], t)]
parseWhole p s = completeResults (feedEof $ feed s p)

parseChunked :: TextualMonoid t => Parser t [[t]] -> [t] -> [([[t]], t)]
parseChunked p chunks = completeResults (feedEof $ foldl' (flip feed) p $ chunks)

parseIncremental :: (Monoid r, TextualMonoid t) => Parser t [[t]] -> ([[t]] -> r) -> [t] -> r
parseIncremental p0 process chunks = let (inc, p') = foldl' feedInc (mempty, p0) chunks
                                     in case completeResults (feedEof p')
                                        of [] -> inc
                                           (r,_):_ -> inc <> process r
   where feedInc (!inc, !p) chunk = let !(prefix, p') = resultPrefix (feed chunk p)
                                    in (inc <> process prefix, p')

chunksOf :: FactorialMonoid t => Int -> t -> [t]
chunksOf len t
   | null t = []
   | (h, t') <- splitAt len t = h : chunksOf len t'

chunkSize :: Int
chunkSize = 1024

data Input = forall t. (NFData t, TextualMonoid t) => Input String t

main :: IO ()
main = do
   airportsS <- readFile "Benchmarks/airports.dat"
   airportsT <- T.readFile "Benchmarks/airports.dat"
   airportsB <- B.readFile "Benchmarks/airports.dat"
   let inputs = [Input "UTF8" (ByteStringUTF8 airportsB),
                 Input "Text" airportsT,
                 Input "Concat String" (pure airportsS :: Concat String)]
   defaultMain [
      let chunks = chunksOf chunkSize i
      in nf id chunks `seq` bgroup inputName [
         bgroup parserName [
            bench "whole" $ nf (parseWhole p) i,
            bench "chunked" $ nf (parseChunked p) chunks,
            bench "incremental" $ nf (parseIncremental p id) chunks,
            bench "incremental'" $ nf (parseIncremental p (pure :: a -> Concat a)) chunks]
            | (parserName, p) <- [("manyTill", file1), ("many", file2)]]
         | Input inputName i <- inputs]
