implementation module Text.CSV

import StdEnv
import Data.Maybe
import Text

BUFF_SIZE :== 1024

readCSVRecord :: !*File -> (!?[String],!*File)
readCSVRecord file = readCSVRecordWith ',' '"' '\\' file

readCSVRecordWith :: !Char !Char !Char !*File -> (!?[String],!*File)
readCSVRecordWith delimitChar quoteChar escapeChar file
	// Read until line delimiter or end of file
	# (remain,fields,file) = read "" 0 0 False False [] file
	| remain == -1
		= (?None,file)
	| otherwise
		// Reset file position to start of new record
		# (_,file) = fseek file (~ remain) FSeekCur
		= (?Just (reverse fields),file)
where
	read :: !{#Char} !Int !Int !Bool !Bool ![{#Char}] !*File -> (!Int,![{#Char}],!*File)
	read buffer start cur quoted escape fields file
		//End of buffer before record ends (add more to the buffer)
		| cur == size buffer
			# (chunk,file) = freads file BUFF_SIZE
			| size chunk == 0 //EOF
				//When nothing was read at all, return -1 to indicate this
				| cur == 0 		= (-1,[],file)
				//An eof if fields have been read already is treated as end of a record
				| otherwise		= (0,[field:fields], file)
			| otherwise
				= read (buffer +++ chunk) start cur quoted escape fields file
		//End of field
		| buffer.[cur] == delimitChar && not quoted && not escape
			= read buffer next next quoted False [field:fields] file 
		//End of line
		| (buffer.[cur] == '\n' || buffer.[cur] == '\r') && not quoted
			= (remain, [field:fields], file)
		//Start of a quoted field
		| buffer.[cur] == quoteChar && cur == start
			= read buffer start next True False fields file
		//End of a quoted field
		| buffer.[cur] == quoteChar && quoted && not escape
			= read buffer start next False False fields file
		//Trigger escape
		| buffer.[cur] == escapeChar && not escape
			= read buffer start next False True fields file
		//Simply advance the cursor
		| otherwise
			= read buffer start next quoted False fields file
	where
		next		= inc cur
		field		= if isQuoted quotedField normalField
		isQuoted	= buffer.[start] == quoteChar && buffer.[cur - 1] == quoteChar 
		normalField = unescape (buffer % (start, cur - 1))
		quotedField = unescape (buffer % (start + 1, cur - 2))
		
		remain		= size buffer - next
		
		//Remove all {escapeChar,quoteChar} and {escapeChar,escapeChar} combinations from a string
		unescape s	= {c \\ c <- (unescape` [u \\ u <-: s])}
		where
			unescape` [char1,char2:rest] 
				| char1 == escapeChar	= unescape` [char2:rest]
										= [char1:unescape` [char2:rest]]
			unescape` [char]			= [char]
			unescape` []				= []
			
readCSVFile :: !*File -> (![[String]],!*File)
readCSVFile file = readCSVFileWith ',' '"' '\\' file

readCSVFileWith :: !Char !Char !Char !*File -> (![[String]],!*File)
readCSVFileWith delimitChar quoteChar escapeChar file
	# (revRows, file) = foldedCsvRowsWith delimitChar quoteChar escapeChar (\row acc -> [row: acc]) [] file
	= (reverse revRows, file)

foldedCsvRowsWith :: !Char !Char !Char !([String] .acc -> .acc) !.acc !*File -> (!.acc, !*File)
foldedCsvRowsWith delimitChar quoteChar escapeChar withRow acc file = foldedCsvRowsWith` withRow acc file
where
	foldedCsvRowsWith` :: !([String] .acc -> .acc) !.acc !*File -> (!.acc, !*File)
	foldedCsvRowsWith` withRow acc file
		# (mbRec, file) = readCSVRecordWith delimitChar quoteChar escapeChar file
		= case mbRec of
			?None     = (acc,file)
			?Just rec = foldedCsvRowsWith` withRow (withRow rec acc) file

writeCSVRecord :: ![String] !*File -> *File
writeCSVRecord fields file = writeCSVRecordWith ',' '"' '\\' fields file

writeCSVRecordWith :: !Char !Char !Char ![String] !*File -> *File
writeCSVRecordWith delimitChar quoteChar escapeChar fields file
	= fwrites line file
where
	line			= (join {delimitChar} [{quoteChar} +++ escape field +++ {quoteChar} \\ field <- fields]) +++ "\r\n"
	escape s		= {c \\ c <- flatten [escape` u \\ u <-: s]}
	where
		escape` c
			| c == escapeChar	= [escapeChar,escapeChar]
			| c == quoteChar	= [escapeChar,quoteChar]
								= [c]
		
writeCSVFile :: ![[String]] !*File -> *File
writeCSVFile fields file = writeCSVFileWith ',' '"' '\\' fields file

writeCSVFileWith :: !Char !Char !Char ![[String]] !*File -> *File
writeCSVFileWith delimitChar quoteChar escapeChar fields file = foldl (flip (writeCSVRecordWith delimitChar quoteChar escapeChar)) file fields
