package uk.me.nxg.unity.util;

/*
 * Scratchy CSV class.
 *
 * SimpleCSVReader is a home-made CSV reader.  I had previously used
 * the CSV reader from opencsv.sourceforge.net, but this seemed to behave oddly
 * when CSV elements started with backslashes, and it seemed easier to roll a quick
 * reader than fight with it.
 */

import java.util.List;
import java.util.Iterator;


/**
 * Makes a CSV file available as a sequence of string lists.
 * The reader can handle quoted cells, and doubled double-quotes within them.
 * It can't handle newlines within quoted cells.
 *
 * <p>Usage:
 * <pre>
 * for (List&lt;String&gt; line : new SimpleCSVReader(reader)) {
 *   ... // process list of entries on one line
 * }
 * </pre>
 */
public class SimpleCSVReader
        implements Iterable<List<String>> {
    final java.io.BufferedReader in;
    final boolean sloppyReader;

    /**
     * Create a new CSV reader
     * @param in the source of the CSV to be parsed
     */
    public SimpleCSVReader(java.io.Reader in) {
        this(in, false);
    }

    /**
     * Create a new CSV reader.  If the second argument is true, then
     * create a 'sloppy reader': this will omit all blank lines in the
     * input, or lines beginning with '#', and will skip trailing
     * whitespace after commas in each line.  This isn't really conformant,
     * but ends up being useful.
     *
     * @param in the source of the CSV to be parsed
     * @param sloppyReader if true, then create a ‘sloppy’ reader
     */
    public SimpleCSVReader(java.io.Reader in, boolean sloppyReader) {
        this.in = new java.io.BufferedReader(in);
        this.sloppyReader = sloppyReader;
    }

    public Iterator<List<String>> iterator() {
        return new CSVIterator();
    }

    class CSVIterator implements Iterator<List<String>> {
        boolean atEOF = false;
        java.io.PushbackReader line;
        String oneLine = null;

        /**
         * Indicates whether there is at least one more line to come.
         * @return true if next() would succeed
         */
        public boolean hasNext() {
            /*
             * This also reads in the next line if it's not currently present.
             * <p>Postcondition: there is either a line in the oneLine
             * variable and we return true; or else atEOF is true and we
             * return false.
             */
            boolean oneLinePresent;

            if (atEOF) {
                oneLinePresent = false;
            } else {
                try {
                    if (sloppyReader) {
                        if (oneLine == null) {
                            do {
                                oneLine = in.readLine();
                            } while (oneLine != null && (oneLine.length() == 0 || oneLine.charAt(0) == '#'));
                        }
                    } else {
                        if (oneLine == null) {
                            oneLine = in.readLine();
                        }
                    }
                } catch (java.io.IOException e) {
                    oneLine = null;
                }
                if (oneLine == null) {
                    atEOF = true;
                }
                oneLinePresent = oneLine != null;
            }

            // either oneline is present, Xor at end of file
            assert atEOF ^ oneLinePresent;

            return oneLinePresent;
        }

        /**
         * Return the next line of input, as a list.
         * <p>Requirement: hasNext should return true (otherwise a NoSuchElementException is thrown)
         * @return non-null list of strings
         */
        public List<String> next() {
            if (! hasNext()) {
                throw new java.util.NoSuchElementException("Attempt to read past end of CSV file" + in);
            }

            assert oneLine != null;

            try {

                line = new java.io.PushbackReader(new java.io.StringReader(oneLine));
                List<String> stringList = new java.util.ArrayList<String>();
                while (true) {
                    int c = line.read();
                    if (c < 0) {
                        // end of line
                        oneLine = null;
                        break;  // JUMP OUT
                    }
                    if (c == '"') {
                        stringList.add(readToQuote(line));
                    } else {
                        line.unread(c);
                        stringList.add(readToCommaOrEnd(line));
                    }
                    c = line.read(); // gobble the trailing comma
                    assert c == ','; // that _was_ a comma, wasn't it?
                }
                assert stringList != null && stringList.size() > 0;
                return stringList;

            } catch (java.io.IOException e) {
                // this shouldn't happen
                throw new java.lang.AssertionError("Odd: uncaught IOException reading CSV file " + in);
            }
        }

        public void remove() {
            throw new UnsupportedOperationException("SimpleCSVReader doesn't support remove()");
        }

        // Read from the current position (which will be immediately
        // after a quote character) up to the next quote character,
        // swallowing but not including the quote character
        // Return the read string.
        // Postcondition (unchecked): the next character in the reader is a comma ','
        private String readToQuote(java.io.PushbackReader r)
                throws java.io.IOException {
            StringBuilder sb = new StringBuilder();
            boolean finishedReadingColumn = false;

            while (! finishedReadingColumn) {
                int c = r.read();
                if (c < 0) {
                    throw new java.io.IOException("Malformed CSV file: EOF in quote");
                } else if (c == '"') {
                    int nextc = r.read();
                    if (nextc < 0 || nextc == ',') {
                        // that's OK -- end of reading, but make sure there's a comma waiting to be read
                        r.unread(',');
                        finishedReadingColumn = true;
                    } else if (nextc == '"') {
                        // read double-quote -- add one double-quote to the output
                        sb.append('"');
                    } else {
                        throw new java.io.IOException("Malformed CSV file: quote not followed by comma or EOL");
                    }
                } else {
                    sb.append((char)c);
                }
            }
            return sb.toString();
        }

        // Read from the current position (which will be immediately
        // after a comma) up to the next comma or EOB.
        // Return the read string.
        // Postcondition (unchecked): the next character in the reader is a comma ','
        private String readToCommaOrEnd(java.io.PushbackReader r)
                throws java.io.IOException {
            StringBuilder sb = new StringBuilder();
            boolean finishedReading = false;

            if (sloppyReader) {
                boolean skipping = true;
                while (skipping) {
                    int c = r.read();
                    if (! Character.isWhitespace(c)) {
                        r.unread(c);
                        skipping = false;
                    }
                }
            }

            while (! finishedReading) {
                int c = r.read();
                if (c == ',' || c < 0) {
                    r.unread(',');
                    finishedReading = true;
                } else {
                    sb.append((char)c);
                }
            }
            return sb.toString();
        }
    }
}
