/* Copyright (c) Jython Developers */
package org.python.modules._csv;

import org.python.core.Py;
import org.python.core.PyException;
import org.python.core.PyObject;
import org.python.core.PyString;
import org.python.core.PyType;
import org.python.expose.ExposedType;
import org.python.expose.ExposedMethod;
import org.python.expose.ExposedGet;

/**
 * CSV file writer.
 *
 * Analogous to CPython's _csv.c::WriterObj struct.
 */
@ExposedType(name = "_csv.writer")
public class PyWriter extends PyObject {

    public static final PyType TYPE = PyType.fromClass(PyWriter.class);

    /** Parsing dialect. */
    @ExposedGet
    public PyDialect dialect;

    /** Output lines writer callable. */
    private PyObject writeline;

    /** Buffer for parser.join. */
    private StringBuffer rec;

    /** Length of record. */
    private int rec_len = 0;

    /** Number of fields in record. */
    private int num_fields = 0;

    /** Whether field should be quoted during a join. */
    private boolean quoted = false;

    public PyWriter(PyObject writeline, PyDialect dialect) {
        this.writeline = writeline;
        this.dialect = dialect;
    }

    public static PyString __doc__writerows = Py.newString(
            "writerows(sequence of sequences)\n" +
            "\n" +
            "Construct and write a series of sequences to a csv file.  Non-string\n" +
            "elements will be converted to string.");

    public void writerows(PyObject seqseq) {
        writer_writerows(seqseq);
    }

    @ExposedMethod
    final void writer_writerows(PyObject seqseq) {
        PyObject row_iter;
        PyObject row_obj;
        boolean result;

        row_iter = seqseq.__iter__();
        if (row_iter == null) {
            throw _csv.Error("writerows() argument must be iterable");
        }

        while ((row_obj = row_iter.__iternext__()) != null) {
            result = writerow(row_obj);
            if (!result) {
                break;
            }
        }
    }

    public static PyString __doc__writerow = Py.newString(
            "writerow(sequence)\n" +
            "\n" +
            "Construct and write a CSV record from a sequence of fields.  Non-string\n" +
            "elements will be converted to string."
            );

    public boolean writerow(PyObject seq) {
        return writer_writerow(seq);
    }

    @ExposedMethod
    final boolean writer_writerow(PyObject seq) {
        int len;
        int i;

        if (!seq.isSequenceType()) {
            throw _csv.Error("sequence expected");
        }

        len = seq.__len__();
        if (len < 0) {
            return false;
        }

        // Join all fields in internal buffer.
        join_reset();
        for (i = 0; i < len; i++) {
            PyObject field;
            boolean append_ok;
            quoted = false;

            field = seq.__getitem__(i);
            if (field == null) {
                return false;
            }

            switch (dialect.quoting) {
                case QUOTE_NONNUMERIC:
                    try {
                        field.__float__();
                    } catch (PyException ex) {
                        quoted = true;
                    }
                    break;
                case QUOTE_ALL:
                    quoted = true;
                    break;
                default:
                    quoted = false;
            }

            if (field instanceof PyString) {
                append_ok = join_append(field.toString(), len == 1);
            } else if (field == Py.None) {
                append_ok = join_append("", len == 1);
            } else {
                PyObject str = field.__str__();
                if (str == null) {
                    return false;
                }

                append_ok = join_append(str.toString(), len == 1);
            }
            if (!append_ok) {
                return false;
            }
        }

        // Add line terminator.
        if (!join_append_lineterminator()) {
            return false;
        }

        writeline.__call__(new PyString(rec.toString()));
        return true;
    }

    private void join_reset() {
        rec_len = 0;
        num_fields = 0;
        quoted = false;
        rec = new StringBuffer();
    }

    private boolean join_append_lineterminator() {
        rec.append(dialect.lineterminator);
        return true;
    }

    private boolean join_append(String field, boolean quote_empty) {
        int rec_len;

        rec_len = join_append_data(field, quote_empty, false);
        if (rec_len < 0) {
            return false;
        }

        this.rec_len = join_append_data(field, quote_empty, true);
        num_fields++;

        return true;
    }

    /**
     * This method behaves differently depending on the value of copy_phase: if copy_phase
     * is false, then the method determines the new record length. If copy_phase is true
     * then the new field is appended to the record.
     */
    private int join_append_data(String field, boolean quote_empty, boolean copy_phase) {
        int i;

        // If this is not the first field we need a field separator.
        if (num_fields > 0) {
            addChar(dialect.delimiter, copy_phase);
        }

        // Handle preceding quote
        if (copy_phase && quoted) {
            addChar(dialect.quotechar, copy_phase);
        }

        // parsing below is based on _csv.c which expects all strings to be terminated
        // with a nul byte.
        field += '\0';

        // Copy/count field data.
        for (i = 0;; i++) {
            char c = field.charAt(i);
            boolean want_escape = false;

            if (c == '\0') {
                break;
            }
            if (c == dialect.delimiter || c == dialect.escapechar || c == dialect.quotechar
                || dialect.lineterminator.indexOf(c) > -1) {
                if (dialect.quoting == QuoteStyle.QUOTE_NONE) {
                    want_escape = true;
                } else {
                    if (c == dialect.quotechar) {
                        if (dialect.doublequote) {
                            addChar(dialect.quotechar, copy_phase);
                        } else {
                            want_escape = true;
                        }
                    }
                    if (!want_escape) {
                        quoted = true;
                    }
                }
                if (want_escape) {
                    if (dialect.escapechar == '\0') {
                        throw _csv.Error("need to escape, but no escapechar set");
                    }
                    addChar(dialect.escapechar, copy_phase);
                }
            }

            // Copy field character into record buffer.
            addChar(c, copy_phase);
        }

        // If field is empty check if it needs to be quoted.
        if (i == 0 && quote_empty) {
            if (dialect.quoting == QuoteStyle.QUOTE_NONE) {
                throw _csv.Error("single empty field record must be quoted");
            } else {
                quoted = true;
            }
        }

        // Handle final quote character on field.
        if (quoted) {
            if (copy_phase) {
                addChar(dialect.quotechar, copy_phase);
            } else {
                // Didn't know about leading quote until we found it necessary in field
                // data - compensate for it now.
                rec_len += 2;
            }
        }

        return rec_len;
    }

    private void addChar(char c, boolean copy_phase) {
        if (copy_phase) {
            rec.append(c);
        }
        rec_len++;
    }
}
