// This code is in the public domain.  I can't remember where I found it on the Web, but it
// didn't come with any license.

using System;
using System.Collections;
using System.IO;
using System.Text;

namespace CSVReader {

  /// <summary>
  /// A data-reader style interface for reading CSV files.
  /// </summary>
  public class CSVReader : IDisposable {

    #region Private variables

    private Stream stream;
    private StreamReader reader;

    #endregion

    /// <summary>
    /// Create a new reader for the given stream.
    /// </summary>
    /// <param name="s">The stream to read the CSV from.</param>
    public CSVReader(Stream s) : this(s, null) { }

    /// <summary>
    /// Create a new reader for the given stream and encoding.
    /// </summary>
    /// <param name="s">The stream to read the CSV from.</param>
    /// <param name="enc">The encoding used.</param>
    public CSVReader(Stream s, Encoding enc) {

      this.stream = s;
      if (!s.CanRead) {
        throw new CSVReaderException("Could not read the given CSV stream!");
      }
      reader = (enc != null) ? new StreamReader(s, enc) : new StreamReader(s);
    }

    /// <summary>
    /// Creates a new reader for the given text file path.
    /// </summary>
    /// <param name="filename">The name of the file to be read.</param>
    public CSVReader(string filename) : this(filename, null) { }

    /// <summary>
    /// Creates a new reader for the given text file path and encoding.
    /// </summary>
    /// <param name="filename">The name of the file to be read.</param>
    /// <param name="enc">The encoding used.</param>
    public CSVReader(string filename, Encoding enc) 
      : this(new FileStream(filename, FileMode.Open), enc) { }

    /// <summary>
    /// Returns the fields for the next row of CSV data (or null if at eof)
    /// </summary>
    /// <returns>A string array of fields or null if at the end of file.</returns>
    public string[] GetCSVLine() {

      string data = reader.ReadLine();
      if (data == null) return null;
      if (data.Length == 0) return new string[0];
      
      ArrayList result = new ArrayList();

      ParseCSVFields(result, data);
      
      return (string[])result.ToArray(typeof(string));
    }

    // Parses the CSV fields and pushes the fields into the result arraylist
    private void ParseCSVFields(ArrayList result, string data) {

      int pos = -1;
      while (pos < data.Length)
        result.Add(ParseCSVField(data, ref pos));
    }

    // Parses the field at the given position of the data, modified pos to match
    // the first unparsed position and returns the parsed field
    private string ParseCSVField(string data, ref int startSeparatorPosition) {

      if (startSeparatorPosition == data.Length-1) {
        startSeparatorPosition++;
        // The last field is empty
        return "";
      }

      int fromPos = startSeparatorPosition + 1;

      // Determine if this is a quoted field
      if (data[fromPos] == '"') {
        // If we're at the end of the string, let's consider this a field that
        // only contains the quote
        if (fromPos == data.Length-1) {
          fromPos++;
          return "\"";
        }

        // Otherwise, return a string of appropriate length with double quotes collapsed
        // Note that FSQ returns data.Length if no single quote was found
        int nextSingleQuote = FindSingleQuote(data, fromPos+1);
        startSeparatorPosition = nextSingleQuote+1;
        return data.Substring(fromPos+1, nextSingleQuote-fromPos-1).Replace("\"\"", "\"");
      }

      // The field ends in the next comma or EOL
      int nextComma = data.IndexOf(',', fromPos);
      if (nextComma == -1) {
        startSeparatorPosition = data.Length;
        return data.Substring(fromPos);
      }
      else {
        startSeparatorPosition = nextComma;
        return data.Substring(fromPos, nextComma-fromPos);
      }
    }

    // Returns the index of the next single quote mark in the string 
    // (starting from startFrom)
    private int FindSingleQuote(string data, int startFrom) {

      int i = startFrom-1;
      while (++i < data.Length)
        if (data[i] == '"') {
          // If this is a double quote, bypass the chars
          if (i < data.Length-1 && data[i+1] == '"') {
            i++;
            continue;
          }
          else
            return i;
        }
      // If no quote found, return the end value of i (data.Length)
      return i;
    }

    /// <summary>
    /// Disposes the CSVReader. The underlying stream is closed.
    /// </summary>
    public void Dispose() {
      // Closing the reader closes the underlying stream, too
      if (reader != null) reader.Close();
      else if (stream != null)
        stream.Close(); // In case we failed before the reader was constructed
      GC.SuppressFinalize(this);
    }
  }


  /// <summary>
  /// Exception class for CSVReader exceptions.
  /// </summary>
  public class CSVReaderException : ApplicationException { 
  
    /// <summary>
    /// Constructs a new exception object with the given message.
    /// </summary>
    /// <param name="message">The exception message.</param>
    public CSVReaderException(string message) : base(message) { }
  }
}
