{-# Language OverloadedStrings #-}

-- Copyright 2012 UserEvents, Inc.
-- 
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
-- 
--    http://www.apache.org/licenses/LICENSE-2.0
-- 
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Contributors:
--    Robin Bate Boerop <me@robinbb.com>

module Text.ParseCSV
   ( CSV
   , parseCSV
   ) where

import Prelude hiding (concat, takeWhile)
import Control.Applicative ((<$>), (<|>), (<*>), (<*), (*>), many)
import Control.Monad (void)
import Data.Attoparsec.Text
import qualified Data.Text as T (Text, concat, cons, append)

type CSV = [[T.Text]]

lineEnd :: Parser ()
lineEnd =
   void (char '\n') <|> void (string "\r\n")
   <?> "end of line"

unquotedField :: Parser T.Text
unquotedField =
   takeWhile (`notElem` ",\n\r\"")
   <?> "unquoted field"

insideQuotes :: Parser T.Text
insideQuotes =
   T.append <$> takeWhile (/= '"')
            <*> (T.concat <$> many (T.cons <$> dquotes <*> insideQuotes))
   <?> "inside of double quotes"
   where
      dquotes =
         string "\"\"" >> return '"'
         <?> "paired double quotes"

quotedField :: Parser T.Text
quotedField =
   char '"' *> insideQuotes <* char '"'
   <?> "quoted field"

field :: Parser T.Text
field =
   quotedField <|> unquotedField
   <?> "field"

record :: Parser [T.Text]
record =
   field `sepBy1` char ','
   <?> "record"

file :: Parser CSV
file =
   (:) <$> record
       <*> manyTill (lineEnd *> record)
                    (endOfInput <|> lineEnd *> endOfInput)
   <?> "file"

parseCSV :: T.Text -> Either String CSV
parseCSV =
   parseOnly file