/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2001-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.util;

import java.io.FilterWriter;
import java.io.IOException;
import java.io.Writer;

/**
 * Writes characters to a stream while replacing various EOL by a unique string. This class catches
 * all occurrences of {@code "\r"}, {@code "\n"} and {@code "\r\n"}, and replaces them by the
 * platform depend EOL string ({@code "\r\n"} on Windows, {@code "\n"} on Unix), or any other EOL
 * explicitly set at construction time. This writer also remove trailing blanks before end of lines,
 * but this behavior can be changed by overriding {@link #isWhitespace}.
 *
 * @version $Id$
 * @author Martin Desruisseaux (IRD)
 * @since 2.0
 */
public class LineWriter extends FilterWriter {
    /** The line separator for End Of Line (EOL). */
    private String lineSeparator;

    /**
     * Tells if the next '\n' character must be ignored. This field is used in order to avoid
     * writing two EOL in place of "\r\n".
     */
    private boolean skipCR;

    /**
     * Temporary buffer containing blanks to write. Whitespaces are put in this buffer before to be
     * written. If whitespaces are followed by a character, they are written to the underlying
     * stream before the character. Otherwise, if whitespaces are followed by a line separator, then
     * they are discarted. The buffer capacity will be expanded as needed.
     */
    private char[] buffer = new char[64];

    /** Number of valid characters in {@link #buffer}. */
    private int count = 0;

    /**
     * Constructs a {@code LineWriter} object that will use the platform dependent line separator.
     *
     * @param out A writer object to provide the underlying stream.
     * @throws IllegalArgumentException if {@code out} is {@code null}.
     */
    public LineWriter(final Writer out) {
        this(out, System.getProperty("line.separator", "\n"));
    }

    /**
     * Constructs a {@code LineWriter} object that will use the specified line separator.
     *
     * @param out A writer object to provide the underlying stream.
     * @param lineSeparator String to use as line separator.
     * @throws IllegalArgumentException if {@code out} or {@code lineSeparator} is {@code null}.
     */
    public LineWriter(final Writer out, final String lineSeparator) {
        super(out);
        this.lineSeparator = lineSeparator;
        if (lineSeparator == null) {
            throw new IllegalArgumentException();
        }
    }

    /**
     * Returns the current line separator.
     *
     * @return The current line separator.
     */
    public String getLineSeparator() {
        return lineSeparator;
    }

    /**
     * Changes the line separator. This is the string to insert in place of every occurences of
     * {@code "\r"}, {@code "\n"} or {@code "\r\n"}.
     *
     * @param lineSeparator The new line separator.
     * @throws IllegalArgumentException If {@code lineSeparator} is {@code null}.
     */
    public void setLineSeparator(final String lineSeparator) {
        if (lineSeparator == null) {
            throw new IllegalArgumentException();
        }
        synchronized (lock) {
            this.lineSeparator = lineSeparator;
        }
    }

    /**
     * Writes a line separator.
     *
     * @throws IOException If an I/O error occurs.
     */
    private void writeEOL() throws IOException {
        assert count == 0 : count;
        // Do NOT call super.write(String).
        out.write(lineSeparator);
    }

    /**
     * Returns {@code true} if {@link #buffer} contains only white spaces. It should always be the
     * case. This method is used for assertions only.
     */
    private boolean bufferBlank() throws IOException {
        for (int i = count; --i >= 0; ) {
            if (!isWhitespace(buffer[i])) {
                return false;
            }
        }
        return true;
    }

    /**
     * Flushs the content of {@link #buffer} to the underlying stream.
     *
     * @throws IOException If an I/O error occurs.
     */
    private void flushBuffer() throws IOException {
        assert bufferBlank();
        if (count != 0) {
            out.write(buffer, 0, count);
            count = 0;
        }
    }

    /**
     * Writes a portion of an array of characters. This portion must <strong>not</strong> contains
     * any line separator.
     */
    private void writeLine(final char[] cbuf, final int lower, int upper) throws IOException {
        while (upper != lower) {
            final char c = cbuf[upper - 1];
            assert (c != '\r' && c != '\n');
            if (isWhitespace(c)) {
                upper--;
                continue;
            }
            flushBuffer();
            out.write(cbuf, lower, upper - lower);
            return;
        }
        assert bufferBlank();
        count = 0;
    }

    /**
     * Writes a portion of an array of characters. This portion must <strong>not</strong> contains
     * any line separator.
     */
    private void writeLine(final String str, final int lower, int upper) throws IOException {
        while (upper != lower) {
            final char c = str.charAt(upper - 1);
            assert (c != '\r' && c != '\n');
            if (isWhitespace(c)) {
                upper--;
                continue;
            }
            flushBuffer();
            out.write(str, lower, upper - lower);
            return;
        }
        assert bufferBlank();
        count = 0;
    }

    /**
     * Writes a single character.
     *
     * @throws IOException If an I/O error occurs.
     */
    @Override
    public void write(final int c) throws IOException {
        synchronized (lock) {
            switch (c) {
                case '\r':
                    {
                        assert bufferBlank();
                        count = 0; // Discard whitespaces
                        writeEOL();
                        skipCR = true;
                        break;
                    }
                case '\n':
                    {
                        if (!skipCR) {
                            assert bufferBlank();
                            count = 0; // Discard whitespaces
                            writeEOL();
                        }
                        skipCR = false;
                        break;
                    }
                default:
                    {
                        if (c >= Character.MIN_VALUE
                                && c <= Character.MAX_VALUE
                                && isWhitespace((char) c)) {
                            if (count >= buffer.length) {
                                buffer = XArray.resize(buffer, count + Math.min(8192, count));
                            }
                            buffer[count++] = (char) c;
                        } else {
                            flushBuffer();
                            out.write(c);
                        }
                        skipCR = false;
                        break;
                    }
            }
        }
    }

    /**
     * Writes a portion of an array of characters.
     *
     * @param cbuf Buffer of characters to be written.
     * @param offset Offset from which to start reading characters.
     * @param length Number of characters to be written.
     * @throws IOException If an I/O error occurs.
     */
    @Override
    public void write(final char[] cbuf, int offset, int length) throws IOException {
        if (offset < 0 || length < 0 || (offset + length) > cbuf.length) {
            throw new IndexOutOfBoundsException();
        }
        if (length == 0) {
            return;
        }
        synchronized (lock) {
            if (skipCR && cbuf[offset] == '\n') {
                offset++;
                length--;
            }
            int upper = offset;
            for (; length != 0; length--) {
                switch (cbuf[upper++]) {
                    case '\r':
                        {
                            writeLine(cbuf, offset, upper - 1);
                            writeEOL();
                            if (length > 1 && cbuf[upper] == '\n') {
                                upper++;
                                length--;
                            }
                            offset = upper;
                            break;
                        }
                    case '\n':
                        {
                            writeLine(cbuf, offset, upper - 1);
                            writeEOL();
                            offset = upper;
                            break;
                        }
                }
            }
            skipCR = (cbuf[upper - 1] == '\r');
            /*
             * Write the remainding characters and
             * put trailing blanks into the buffer.
             */
            for (int i = upper; --i >= offset; ) {
                if (!isWhitespace(cbuf[i])) {
                    writeLine(cbuf, offset, offset = i + 1);
                    break;
                }
            }
            length = upper - offset;
            final int newCount = count + length;
            if (newCount > buffer.length) {
                buffer = XArray.resize(buffer, newCount);
            }
            System.arraycopy(cbuf, offset, buffer, count, length);
            count = newCount;
        }
    }

    /**
     * Writes a portion of an array of a string.
     *
     * @param string String to be written.
     * @param offset Offset from which to start reading characters.
     * @param length Number of characters to be written.
     * @throws IOException If an I/O error occurs.
     */
    @Override
    public void write(final String string, int offset, int length) throws IOException {
        if (offset < 0 || length < 0 || (offset + length) > string.length()) {
            throw new IndexOutOfBoundsException();
        }
        if (length == 0) {
            return;
        }
        synchronized (lock) {
            if (skipCR && string.charAt(offset) == '\n') {
                offset++;
                length--;
            }
            int upper = offset;
            for (; length != 0; length--) {
                switch (string.charAt(upper++)) {
                    case '\r':
                        {
                            writeLine(string, offset, upper - 1);
                            writeEOL();
                            if (length > 1 && string.charAt(upper) == '\n') {
                                upper++;
                                length--;
                            }
                            offset = upper;
                            break;
                        }
                    case '\n':
                        {
                            writeLine(string, offset, upper - 1);
                            writeEOL();
                            offset = upper;
                            break;
                        }
                }
            }
            skipCR = (string.charAt(upper - 1) == '\r');
            /*
             * Write the remainding characters and
             * put trailing blanks into the buffer.
             */
            for (int i = upper; --i >= offset; ) {
                if (!isWhitespace(string.charAt(i))) {
                    writeLine(string, offset, offset = i + 1);
                    break;
                }
            }
            length = upper - offset;
            final int newCount = count + length;
            if (newCount > buffer.length) {
                buffer = XArray.resize(buffer, newCount);
            }
            while (--length >= 0) {
                buffer[count++] = string.charAt(offset++);
            }
            assert count == newCount : newCount;
        }
    }

    /**
     * Flushs the stream's content to the underlying stream. This method flush completly all
     * internal buffers, including any whitespace characters that should have been skipped if the
     * next non-blank character is a line separator.
     *
     * @throws IOException If an I/O error occurs.
     */
    @Override
    public void flush() throws IOException {
        synchronized (lock) {
            flushBuffer();
            super.flush();
        }
    }

    /**
     * Returns {@code true} if the specified character is a white space that can be ignored on end
     * of line. The default implementation returns {@link Character#isSpaceChar(char)}. Subclasses
     * can override this method in order to change the criterion.
     *
     * @param c The character to test.
     * @return {@code true} if {@code c} is a character that can be ignored on end of line.
     * @throws IOException if this method can not determine if the character is ignoreable.
     * @since 2.5
     */
    protected boolean isWhitespace(final char c) throws IOException {
        return Character.isSpaceChar(c);
    }
}
