/*
 * @(#)AbstractWriter.java	1.21 03/12/19
 *
 * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package javax.swing.text;

import java.io.Writer;
import java.io.IOException;
import java.util.Enumeration;

/**
 * AbstractWriter is an abstract class that actually
 * does the work of writing out the element tree
 * including the attributes.  In terms of how much is
 * written out per line, the writer defaults to 100.
 * But this value can be set by subclasses.
 *
 * @author Sunita Mani
 * @version 1.21, 12/19/03
 */

public abstract class AbstractWriter {

    private ElementIterator it;
    private Writer out;
    private int indentLevel = 0;
    private int indentSpace = 2;
    private Document doc = null;
    private int maxLineLength = 100;
    private int currLength = 0;
    private int startOffset = 0;
    private int endOffset = 0;
    // If (indentLevel * indentSpace) becomes >= maxLineLength, this will
    // get incremened instead of indentLevel to avoid indenting going greater
    // than line length.
    private int offsetIndent = 0;

    /**
     * String used for end of line. If the Document has the property
     * EndOfLineStringProperty, it will be used for newlines. Otherwise
     * the System property line.separator will be used. The line separator
     * can also be set.
     */
    private String lineSeparator;

    /**
     * True indicates that when writing, the line can be split, false 
     * indicates that even if the line is > than max line length it should
     * not be split.
     */
    private boolean canWrapLines;

    /**
     * True while the current line is empty. This will remain true after
     * indenting.
     */
    private boolean isLineEmpty;

    /**
     * Used when indenting. Will contain the spaces.
     */
    private char[] indentChars;

    /**
     * Used when writing out a string.
     */
    private char[] tempChars;

    /**
     * This is used in <code>writeLineSeparator</code> instead of
     * tempChars. If tempChars were used it would mean write couldn't invoke
     * <code>writeLineSeparator</code> as it might have been passed
     * tempChars.
     */
    private char[] newlineChars;

    /**
     * Used for writing text.
     */
    private Segment segment;

    /**
     * How the text packages models newlines.
     * @see #getLineSeparator
     */
    protected static final char NEWLINE = '\n';


    /**
     * Creates a new AbstractWriter.
     * Initializes the ElementIterator with the default
     * root of the document.
     *
     * @param w a Writer.
     * @param doc a Document
     */
    protected AbstractWriter(Writer w, Document doc) {
	this(w, doc, 0, doc.getLength());
    }
    
    /**
     * Creates a new AbstractWriter.
     * Initializes the ElementIterator with the
     * element passed in.
     *
     * @param w a Writer
     * @param doc an Element
     * @param pos The location in the document to fetch the
     *   content.
     * @param len The amount to write out.
     */
    protected AbstractWriter(Writer w, Document doc, int pos, int len) {
	this.doc = doc;
	it = new ElementIterator(doc.getDefaultRootElement());
	out = w;
	startOffset = pos;
	endOffset = pos + len;
	Object docNewline = doc.getProperty(DefaultEditorKit.
				       EndOfLineStringProperty);
	if (docNewline instanceof String) {
	    setLineSeparator((String)docNewline);
	}
	else {
	    String newline = null;
	    try {
		newline = System.getProperty("line.separator");
	    } catch (SecurityException se) {}
	    if (newline == null) {
		// Should not get here, but if we do it means we could not
		// find a newline string, use \n in this case.
		newline = "\n";
	    }
	    setLineSeparator(newline);
	}
	canWrapLines = true;
    }

    /**
     * Creates a new AbstractWriter.
     * Initializes the ElementIterator with the
     * element passed in.
     *
     * @param w a Writer
     * @param root an Element
     */
    protected AbstractWriter(Writer w, Element root) {
	this(w, root, 0, root.getEndOffset());
    }
     
    /**
     * Creates a new AbstractWriter.
     * Initializes the ElementIterator with the
     * element passed in.
     *
     * @param w a Writer
     * @param root an Element
     * @param pos The location in the document to fetch the
     *   content.
     * @param len The amount to write out.
     */
    protected AbstractWriter(Writer w, Element root, int pos, int len) {
	this.doc = root.getDocument();
	it = new ElementIterator(root);
	out = w;
	startOffset = pos;
	endOffset = pos + len;
	canWrapLines = true;
    }

    /**
     * Returns the first offset to be output.
     *
     * @since 1.3
     */
    public int getStartOffset() {
	return startOffset;
    }

    /**
     * Returns the last offset to be output.
     *
     * @since 1.3
     */
    public int getEndOffset() {
	return endOffset;
    }

    /**
     * Fetches the ElementIterator.
     *
     * @return the ElementIterator.
     */
    protected ElementIterator getElementIterator() {
	return it;
    }

    /**
     * Returns the Writer that is used to output the content.
     *
     * @since 1.3
     */
    protected Writer getWriter() {
	return out;
    }

    /**
     * Fetches the document.
     *
     * @return the Document.
     */
    protected Document getDocument() {
	return doc;
    }

    /**
     * This method determines whether the current element
     * is in the range specified.  When no range is specified,
     * the range is initialized to be the entire document.
     * inRange() returns true if the range specified intersects
     * with the element's range.
     *
     * @param  next an Element.
     * @return boolean that indicates whether the element
     *         is in the range.
     */
    protected boolean inRange(Element next) {
	int startOffset = getStartOffset();
	int endOffset = getEndOffset();
	if ((next.getStartOffset() >= startOffset && 
	     next.getStartOffset()  < endOffset) ||
	    (startOffset >= next.getStartOffset() &&
	     startOffset < next.getEndOffset())) {
	    return true;
	}
	return false;
    }

    /**
     * This abstract method needs to be implemented
     * by subclasses.  Its responsibility is to
     * iterate over the elements and use the write()
     * methods to generate output in the desired format.
     */
    abstract protected void write() throws IOException, BadLocationException;

    /**
     * Returns the text associated with the element.
     * The assumption here is that the element is a
     * leaf element.  Throws a BadLocationException
     * when encountered.
     *
     * @param     elem an <code>Element</code>
     * @exception BadLocationException if pos represents an invalid
     *            location within the document
     * @return    the text as a <code>String</code>
     */
    protected String getText(Element elem) throws BadLocationException {
	return doc.getText(elem.getStartOffset(),
			   elem.getEndOffset() - elem.getStartOffset());
    }


    /**
     * Writes out text.  If a range is specified when the constructor
     * is invoked, then only the appropriate range of text is written
     * out.
     *
     * @param     elem an Element.
     * @exception IOException on any I/O error
     * @exception BadLocationException if pos represents an invalid
     *            location within the document.
     */
    protected void text(Element elem) throws BadLocationException,
	                                     IOException {
	int start = Math.max(getStartOffset(), elem.getStartOffset());
	int end = Math.min(getEndOffset(), elem.getEndOffset());
	if (start < end) {
	    if (segment == null) {
		segment = new Segment();
	    }
	    getDocument().getText(start, end - start, segment);
	    if (segment.count > 0) {
		write(segment.array, segment.offset, segment.count);
	    }
	}
    }

    /**
     * Enables subclasses to set the number of characters they
     * want written per line.   The default is 100.
     *
     * @param l the maximum line length.
     */
    protected void setLineLength(int l) {
	maxLineLength = l;
    }

    /**
     * Returns the maximum line length.
     *
     * @since 1.3
     */
    protected int getLineLength() {
	return maxLineLength;
    }

    /**
     * Sets the current line length.
     *
     * @since 1.3.
     */
    protected void setCurrentLineLength(int length) {
	currLength = length;
	isLineEmpty = (currLength == 0);
    }

    /**
     * Returns the current line length.
     *
     * @since 1.3.
     */
    protected int getCurrentLineLength() {
	return currLength;
    }

    /**
     * Returns true if the current line should be considered empty. This
     * is true when <code>getCurrentLineLength</code> == 0 ||
     * <code>indent</code> has been invoked on an empty line.
     *
     * @since 1.3
     */
    protected boolean isLineEmpty() {
	return isLineEmpty;
    }

    /**
     * Sets whether or not lines can be wrapped. This can be toggled
     * during the writing of lines. For example, outputting HTML might 
     * set this to false when outputting a quoted string.
     *
     * @since 1.3
     */
    protected void setCanWrapLines(boolean newValue) {
	canWrapLines = newValue;
    }

    /**
     * Returns whether or not the lines can be wrapped. If this is false
     * no lineSeparator's will be output.
     *
     * @since 1.3
     */
    protected boolean getCanWrapLines() {
	return canWrapLines;
    }

    /**
     * Enables subclasses to specify how many spaces an indent
     * maps to. When indentation takes place, the indent level
     * is multiplied by this mapping.  The default is 2.
     *
     * @param space an int representing the space to indent mapping.
     */
    protected void setIndentSpace(int space) {
	indentSpace = space;
    }

    /**
     * Returns the amount of space to indent.
     *
     * @since 1.3
     */
    protected int getIndentSpace() {
	return indentSpace;
    }

    /**
     * Sets the String used to reprsent newlines. This is initialized
     * in the constructor from either the Document, or the System property
     * line.separator.
     *
     * @since 1.3
     */
    public void setLineSeparator(String value) {
	lineSeparator = value;
    }

    /**
     * Returns the string used to represent newlines.
     *
     * @since 1.3
     */
    public String getLineSeparator() {
	return lineSeparator;
    }

    /**
     * Increments the indent level. If indenting would cause
     * <code>getIndentSpace()</code> *<code>getIndentLevel()</code> to be >
     * than <code>getLineLength()</code> this will not cause an indent.
     */
    protected void incrIndent() {
	// Only increment to a certain point.
	if (offsetIndent > 0) {
	    offsetIndent++;
	}
	else {
	    if (++indentLevel * getIndentSpace() >= getLineLength()) {
		offsetIndent++;
		--indentLevel;
	    }
	}
    }

    /**
     * Decrements the indent level.
     */
    protected void decrIndent() {
	if (offsetIndent > 0) {
	    --offsetIndent;
	}
	else {
	    indentLevel--;
	}
    }

    /**
     * Returns the current indentation level. That is, the number of times
     * <code>incrIndent</code> has been invoked minus the number of times
     * <code>decrIndent</code> has been invoked.
     *
     * @since 1.3
     */
    protected int getIndentLevel() {
	return indentLevel;
    }

    /**
     * Does indentation. The number of spaces written
     * out is indent level times the space to map mapping. If the current
     * line is empty, this will not make it so that the current line is
     * still considered empty.
     *
     * @exception IOException on any I/O error
     */
    protected void indent() throws IOException {
	int max = getIndentLevel() * getIndentSpace();
	if (indentChars == null || max > indentChars.length) {
	    indentChars = new char[max];
	    for (int counter = 0; counter < max; counter++) {
		indentChars[counter] = ' ';
	    }
	}
	int length = getCurrentLineLength();
	boolean wasEmpty = isLineEmpty();
	output(indentChars, 0, max);
	if (wasEmpty && length == 0) {
	    isLineEmpty = true;
	}
    }

    /**
     * Writes out a character. This is implemented to invoke
     * the <code>write</code> method that takes a char[].
     *
     * @param     ch a char.
     * @exception IOException on any I/O error
     */
    protected void write(char ch) throws IOException {
	if (tempChars == null) {
	    tempChars = new char[128];
	}
	tempChars[0] = ch;
	write(tempChars, 0, 1);
    }

    /**
     * Writes out a string. This is implemented to invoke the
     * <code>write</code> method that takes a char[].
     *
     * @param     content a String.
     * @exception IOException on any I/O error
     */
    protected void write(String content) throws IOException {
	if (content == null) {
	    return;
	}
	int size = content.length();
	if (tempChars == null || tempChars.length < size) {
	    tempChars = new char[size];
	}
	content.getChars(0, size, tempChars, 0);
	write(tempChars, 0, size);
    }

    /**
     * Writes the line separator. This invokes <code>output</code> directly
     * as well as setting the <code>lineLength</code> to 0.
     *
     * @since 1.3
     */
    protected void writeLineSeparator() throws IOException {
	String newline = getLineSeparator();
	int length = newline.length();
	if (newlineChars == null || newlineChars.length < length) {
	    newlineChars = new char[length];
	}
	newline.getChars(0, length, newlineChars, 0);
	output(newlineChars, 0, length);
	setCurrentLineLength(0);
    }

    /**
     * All write methods call into this one. If <code>getCanWrapLines()</code>
     * returns false, this will call <code>output</code> with each sequence
     * of <code>chars</code> that doesn't contain a NEWLINE, followed
     * by a call to <code>writeLineSeparator</code>. On the other hand,
     * if <code>getCanWrapLines()</code> returns true, this will split the
     * string, as necessary, so <code>getLineLength</code> is honored.
     * The only exception is if the current string contains no whitespace,
     * and won't fit in which case the line length will exceed 
     * <code>getLineLength</code>.
     *
     * @since 1.3
     */
    protected void write(char[] chars, int startIndex, int length)
	           throws IOException {
	if (!getCanWrapLines()) {
	    // We can not break string, just track if a newline
	    // is in it.
	    int lastIndex = startIndex;
	    int endIndex = startIndex + length;
	    int newlineIndex = indexOf(chars, NEWLINE, startIndex, endIndex);
	    while (newlineIndex != -1) {
		if (newlineIndex > lastIndex) {
		    output(chars, lastIndex, newlineIndex - lastIndex);
		}
		writeLineSeparator();
		lastIndex = newlineIndex + 1;
		newlineIndex = indexOf(chars, '\n', lastIndex, endIndex);
	    }
	    if (lastIndex < endIndex) {
		output(chars, lastIndex, endIndex - lastIndex);
	    }
	}
	else {
	    // We can break chars if the length exceeds maxLength.
	    int lastIndex = startIndex;
	    int endIndex = startIndex + length;
	    int lineLength = getCurrentLineLength();
	    int maxLength = getLineLength();

	    while (lastIndex < endIndex) {
		int newlineIndex = indexOf(chars, NEWLINE, lastIndex,
					   endIndex);
		boolean needsNewline = false;
                boolean forceNewLine = false;

		lineLength = getCurrentLineLength();
		if (newlineIndex != -1 && (lineLength +
			      (newlineIndex - lastIndex)) < maxLength) {
		    if (newlineIndex > lastIndex) {
			output(chars, lastIndex, newlineIndex - lastIndex);
		    }
		    lastIndex = newlineIndex + 1;
                    forceNewLine = true;
		}
		else if (newlineIndex == -1 && (lineLength +
				(endIndex - lastIndex)) < maxLength) {
		    if (endIndex > lastIndex) {
			output(chars, lastIndex, endIndex - lastIndex);
		    }
		    lastIndex = endIndex;
		}
		else {
		    // Need to break chars, find a place to split chars at,
		    // from lastIndex to endIndex,
		    // or maxLength - lineLength whichever is smaller
		    int breakPoint = -1;
		    int maxBreak = Math.min(endIndex - lastIndex,
					    maxLength - lineLength - 1);
		    int counter = 0;
		    while (counter < maxBreak) {
			if (Character.isWhitespace(chars[counter +
							lastIndex])) {
			    breakPoint = counter;
			}
			counter++;
		    }
		    if (breakPoint != -1) {
			// Found a place to break at.
			breakPoint += lastIndex + 1;
			output(chars, lastIndex, breakPoint - lastIndex);
			lastIndex = breakPoint;
                        needsNewline = true;
		    }
		    else {
			// No where good to break.

                        // find the next whitespace, or write out the
                        // whole string.
			    // maxBreak will be negative if current line too
			    // long.
			    counter = Math.max(0, maxBreak);
			    maxBreak = endIndex - lastIndex;
			    while (counter < maxBreak) {
				if (Character.isWhitespace(chars[counter +
								lastIndex])) {
				    breakPoint = counter;
				    break;
				}
				counter++;
			    }
			    if (breakPoint == -1) {
				output(chars, lastIndex, endIndex - lastIndex);
				breakPoint = endIndex;
			    }
			    else {
				breakPoint += lastIndex;
				if (chars[breakPoint] == NEWLINE) {
				    output(chars, lastIndex, breakPoint++ -
					   lastIndex);
                                forceNewLine = true;
				}
				else {
				    output(chars, lastIndex, ++breakPoint -
					      lastIndex);
                                needsNewline = true;
				}
			    }
			    lastIndex = breakPoint;
			}
		    }
		if (forceNewLine || needsNewline || lastIndex < endIndex) {
		    writeLineSeparator();
                    if (lastIndex < endIndex || !forceNewLine) {
			indent();
		    }
		}
	    }
	}
    }

    /**
     * Writes out the set of attributes as " <name>=<value>"
     * pairs. It throws an IOException when encountered.
     *
     * @param     attr an AttributeSet.
     * @exception IOException on any I/O error
     */
    protected void writeAttributes(AttributeSet attr) throws IOException {

	Enumeration names = attr.getAttributeNames();
	while (names.hasMoreElements()) {
	    Object name = names.nextElement();
	    write(" " + name + "=" + attr.getAttribute(name));
	}
    }

    /**
     * The last stop in writing out content. All the write methods eventually
     * make it to this method, which invokes <code>write</code> on the
     * Writer.
     * <p>This method also updates the line length based on
     * <code>length</code>. If this is invoked to output a newline, the
     * current line length will need to be reset as will no longer be
     * valid. If it is up to the caller to do this. Use
     * <code>writeLineSeparator</code> to write out a newline, which will
     * property update the current line length.
     *
     * @since 1.3
     */
    protected void output(char[] content, int start, int length)
	           throws IOException {
	getWriter().write(content, start, length);
	setCurrentLineLength(getCurrentLineLength() + length);
    }

    /**
     * Support method to locate an occurence of a particular character.
     */
    private int indexOf(char[] chars, char sChar, int startIndex,
			int endIndex) {
	while(startIndex < endIndex) {
	    if (chars[startIndex] == sChar) {
		return startIndex;
	    }
	    startIndex++;
	}
	return -1;
    }
}
