/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sis.io;

import java.util.Objects;
import java.io.IOException;
import java.io.CharConversionException;
import static org.apache.sis.util.Characters.isLineOrParagraphSeparator;


/**
 * Base class for writing filtered characters to another {@link Appendable}.
 * This base class performs a work similar to the {@link java.io.FilterWriter} work,
 * except for the following:
 *
 * <ul>
 *   <li>The filtered output is sent to an arbitrary {@link Appendable} instead of
 *       to the {@link java.io.Writer} sub-type.</li>
 *   <li>No synchronization is performed.</li>
 * </ul>
 *
 * If needed, this {@code Appender} can be viewed as a synchronized
 * {@link java.io.Writer} by invoking the {@link IO#asWriter(Appendable)} method.
 *
 * <h2>Flushing and closing the stream</h2>
 * Subclasses implement the {@link java.io.Flushable} interface only if they
 * hold data in an internal buffer before to send them to the wrapped {@code Appendable}.
 * This is the case of {@link TableAppender} and {@link LineAppender} for instance.
 * For unconditionally flushing or closing an {@code Appendable} and its underlying stream,
 * see {@link IO#flush(Appendable)} and {@link IO#close(Appendable)}.
 *
 * @author  Martin Desruisseaux (Geomatys)
 *
 * @see java.io.FilterWriter
 */
abstract class Appender implements Appendable {
    /**
     * The underlying character output stream or buffer.
     */
    protected final Appendable out;

    /**
     * If the last character given to {@link #toCodePoint(char)} if it was a high surrogate,
     * or 0 otherwise.
     */
    private char highSurrogate;

    /**
     * Creates a new appender which will send its output to the given stream or buffer.
     *
     * @param out  the underlying character output stream or buffer.
     */
    protected Appender(final Appendable out) {
        this.out = Objects.requireNonNull(out);
    }

    /**
     * Finds the line separator used in the given character sequence portion, or returns
     * {@code null} if unknown. This method is designed for invocation at the beginning
     * of {@code append(CharSequence, ...)}, before the characters are effectively read.
     */
    final String lineSeparator(final CharSequence sequence, int start, final int end) {
        if (isHighSurrogate()) {
            start++;                                        // Skip invalid character.
        }
        while (start < end) {
            final int c = Character.codePointAt(sequence, start);
            final int b = start;
            start += Character.charCount(c);
            if (isLineOrParagraphSeparator(c)) {
                if (c == '\r' && (start < end) && sequence.charAt(start) == '\n') {
                    start++;
                }
                return sequence.subSequence(b, start).toString();
            }
        }
        return null;
    }

    /**
     * Returns the code point for the given character, or -1 if we need to wait for the next
     * character. This method computes the code point from the given character and the character
     * given to the previous call of this method. This works only if this method is consistently
     * invoked for every characters.
     */
    final int toCodePoint(final char c) {
        final char h = highSurrogate;
        if (h != 0) {
            highSurrogate = 0;
            if (Character.isLowSurrogate(c)) {
                return Character.toCodePoint(h, c);
            }
            /*
             * If we reach this point, we have unpaired surrogate.  This is usually an error,
             * but this not the fault of this class since we are processing data supplied by
             * the user. Be lenient.
             */
        }
        if (Character.isHighSurrogate(c)) {
            highSurrogate = c;
            return -1;
        }
        return c;
    }

    /**
     * Returns {@code true} if the last character given to {@link #toCodePoint(char)}
     * is a {@linkplain Character#isHighSurrogate(char) high surrogate}.
     */
    final boolean isHighSurrogate() {
        return highSurrogate != 0;
    }

    /**
     * If the given sequence begins with a low surrogate completing a previous high surrogate,
     * delegates to {@link #append(char)} and returns {@code start+1}. The intent is to avoid
     * processing a character sequence which starts by an invalid code point.
     *
     * @param  sequence  the character sequence to write.
     * @param  start     index of the first character to write by this method or by the caller.
     * @param  end       index after the last character to be written by the caller.
     * @return index of the first character which need to be written by the caller.
     */
    final int appendSurrogate(final CharSequence sequence, int start, final int end) throws IOException {
        if (start != end && highSurrogate != 0) {
            final char c = sequence.charAt(start);
            if (Character.isLowSurrogate(c)) {
                append(c);
                start++;
            } else {
                throw new CharConversionException();
            }
        }
        return start;
    }

    /**
     * Appends the given code point to the underlying {@link #out} stream or buffer.
     *
     * @param  c  the code point to append.
     * @throws IOException if an error occurred while appending the code point.
     */
    final void appendCodePoint(final int c) throws IOException {
        if (Character.isBmpCodePoint(c)) {
            out.append((char) c);
        } else if (Character.isSupplementaryCodePoint(c)) {
            out.append(Character.highSurrogate(c))
               .append(Character. lowSurrogate(c));
        } else {
            throw new CharConversionException();
        }
    }

    /**
     * Appends the specified character sequence.
     * The default implementation delegates to {@link #append(CharSequence, int, int)}.
     *
     * @param  sequence  the character sequence to append, or {@code null}.
     * @return a reference to this {@code Appendable}.
     * @throws IOException if an I/O error occurred.
     */
    @Override
    public Appendable append(CharSequence sequence) throws IOException {
        if (sequence == null) {
            sequence = "null";
        }
        return append(sequence, 0, sequence.length());
    }

    /**
     * Returns the content of this {@code Appendable} as a string if possible,
     * or the localized <q>Unavailable content</q> string otherwise.
     *
     * @return the content of this {@code Appendable}, or a localized message for unavailable content.
     *
     * @see IO#content(Appendable)
     */
    @Override
    public String toString() {
        return IO.toString(this);
    }
}
