/*
 * 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.
 */

/* $Id: XMLWhiteSpaceHandler.java 1679646 2015-05-15 21:09:32Z adelmelle $ */

package org.apache.fop.fo;

import java.util.List;
import java.util.Stack;

import org.apache.fop.fo.flow.Block;
import org.apache.fop.fo.flow.Float;
import org.apache.fop.util.CharUtilities;

/**
 * Class encapsulating the functionality for white-space-handling
 * during refinement stage.
 * The <code>handleWhiteSpace()</code> methods are called during
 * FOTree-building and marker-cloning:
 * <br>
 * <ul>
 * <li> from <code>FObjMixed.addChildNode()</code></li>
 * <li> from <code>FObjMixed.endOfNode()</code></li>
 * <li> from <code>FObjMixed.handleWhiteSpaceFor()</code></li>
 * </ul>
 * <br>
 * Each time one of the variants is called, white-space is handled
 * for all <code>FOText</code> or <code>Character</code> nodes that
 * were added:
 * <br>
 * <ul>
 * <li> either prior to <code>newChild</code> (and after the previous
 *      non-text child node)</li>
 * <li> or, if <code>newChild</code> is <code>null</code>,
 *      after the previous non-text child</li>
 * </ul>
 * <br>
 * The iteration always starts at <code>firstTextNode</code>,
 * goes on until the last text-node is reached, and deals only
 * with <code>FOText</code> or <code>Character</code> nodes.
 * <br>
 * <em>Note</em>: if the method is called from an inline's endOfNode(),
 *   there is too little context to decide whether trailing
 *   white-space may be removed, so the pending inline is stored
 *   in a List, together with an iterator for which the next()
 *   method returns the first in the trailing sequence of white-
 *   space characters. This List is processed again at the end
 *   of the ancestor block.
 */
public class XMLWhiteSpaceHandler {

    /** True if we are in a run of white space */
    private boolean inWhiteSpace;
    /** True if the last char was a linefeed */
    private boolean afterLinefeed = true;
    /** Counter, increased every time a non-white-space is encountered */
    private int nonWhiteSpaceCount;

    private int linefeedTreatment;
    private int whiteSpaceTreatment;
    private int whiteSpaceCollapse;
    private boolean endOfBlock;
    private boolean nextChildIsBlockLevel;
    private RecursiveCharIterator charIter;

    private List pendingInlines;
    private Stack nestedBlockStack = new java.util.Stack<Block>();
    private CharIterator firstWhiteSpaceInSeq;

    /**
     * Handle white-space for the fo that is passed in, starting at
     * firstTextNode
     * @param fo    the FO for which to handle white-space
     * @param firstTextNode the node at which to start
     * @param nextChild the node that will be added to the list
     *                  after firstTextNode
     */
    public void handleWhiteSpace(FObjMixed fo,
                                 FONode firstTextNode,
                                 FONode nextChild) {

        Block currentBlock = null;
        int foId = fo.getNameId();

        /* set the current block */
        switch (foId) {
            case Constants.FO_BLOCK:
                currentBlock = (Block) fo;
                if (nestedBlockStack.empty() || fo != nestedBlockStack.peek()) {
                    if (nextChild != null) {
                        /* if already in a block, push the current block
                         * onto the stack of nested blocks
                         */
                        nestedBlockStack.push(currentBlock);
                    }
                } else {
                    if (nextChild == null) {
                        nestedBlockStack.pop();
                    }
                }
                break;

            case Constants.FO_RETRIEVE_MARKER:
                /* look for the nearest block ancestor, if any */
                FONode ancestor = fo;
                do {
                    ancestor = ancestor.getParent();
                } while (ancestor.getNameId() != Constants.FO_BLOCK
                        && ancestor.getNameId() != Constants.FO_STATIC_CONTENT);

                if (ancestor.getNameId() == Constants.FO_BLOCK) {
                    currentBlock = (Block) ancestor;
                    nestedBlockStack.push(currentBlock);
                }
                break;

            default:
                if (!nestedBlockStack.empty()) {
                    currentBlock = (Block) nestedBlockStack.peek();
                }
        }

        if (currentBlock != null) {
            linefeedTreatment = currentBlock.getLinefeedTreatment();
            whiteSpaceCollapse = currentBlock.getWhitespaceCollapse();
            whiteSpaceTreatment = currentBlock.getWhitespaceTreatment();
        } else {
            linefeedTreatment = Constants.EN_TREAT_AS_SPACE;
            whiteSpaceCollapse = Constants.EN_TRUE;
            whiteSpaceTreatment = Constants.EN_IGNORE_IF_SURROUNDING_LINEFEED;
        }

        endOfBlock = (nextChild == null && fo == currentBlock);

        if (firstTextNode == null) {
            //no text means no white-space to handle; return early
            afterLinefeed = (fo == currentBlock && fo.firstChild == null);
            nonWhiteSpaceCount = 0;
            if (endOfBlock) {
                handlePendingInlines();
            }
            return;
        }

        charIter = new RecursiveCharIterator(fo, firstTextNode);
        inWhiteSpace = false;
        if (firstTextNode.siblings != null && firstTextNode.siblings[0] != null
                && firstTextNode.siblings[0].getNameId() == Constants.FO_FLOAT) {
            inWhiteSpace = ((Float) firstTextNode.siblings[0]).getInWhiteSpace();
        }

        if (fo == currentBlock
                || currentBlock == null
                || (foId == Constants.FO_RETRIEVE_MARKER
                        && fo.getParent() == currentBlock)) {
            if (firstTextNode == fo.firstChild) {
                afterLinefeed = true;
            } else {
                int previousChildId = firstTextNode.siblings[0].getNameId();
                afterLinefeed = (previousChildId == Constants.FO_BLOCK
                        || previousChildId == Constants.FO_TABLE_AND_CAPTION
                        || previousChildId == Constants.FO_TABLE
                        || previousChildId == Constants.FO_LIST_BLOCK
                        || previousChildId == Constants.FO_BLOCK_CONTAINER);
            }
        }

        if (foId == Constants.FO_WRAPPER) {
            FONode parent = fo.parent;
            int parentId = parent.getNameId();
            while (parentId == Constants.FO_WRAPPER) {
                parent = parent.parent;
                parentId = parent.getNameId();
            }
            if (parentId == Constants.FO_FLOW
                    || parentId == Constants.FO_STATIC_CONTENT
                    || parentId == Constants.FO_BLOCK_CONTAINER
                    || parentId == Constants.FO_TABLE_CELL) {
                endOfBlock = (nextChild == null);
            }
        }

        if (nextChild != null) {
            int nextChildId = nextChild.getNameId();
            nextChildIsBlockLevel = (
                    nextChildId == Constants.FO_BLOCK
                    || nextChildId == Constants.FO_TABLE_AND_CAPTION
                    || nextChildId == Constants.FO_TABLE
                    || nextChildId == Constants.FO_LIST_BLOCK
                    || nextChildId == Constants.FO_BLOCK_CONTAINER);
        } else {
            nextChildIsBlockLevel = false;
        }

        handleWhiteSpace();

        if (fo == currentBlock
                && (endOfBlock || nextChildIsBlockLevel)) {
            handlePendingInlines();
        }

        if (nextChild == null) {
            if (fo != currentBlock) {
                /* current FO is not a block, and is about to end */
                if (nonWhiteSpaceCount > 0 && pendingInlines != null) {
                    /* there is non-white-space text between the pending
                     * inline(s) and the end of the non-block node;
                     * clear list of pending inlines */
                    pendingInlines.clear();
                }
                if (inWhiteSpace) {
                    /* means there is at least one trailing space in the
                       inline FO that is about to end */
                    addPendingInline();
                }
            } else {
                /* end of block: clear the references and pop the
                 * nested block stack */
                if (!nestedBlockStack.empty()) {
                    nestedBlockStack.pop();
                }
                charIter = null;
                firstWhiteSpaceInSeq = null;
            }
        }
        if (nextChild instanceof Float) {
            ((Float) nextChild).setInWhiteSpace(inWhiteSpace);
        }
    }

    /**
     * Reset the handler, release all references
     */
    protected final void reset() {
        if (pendingInlines != null) {
            pendingInlines.clear();
        }
        nestedBlockStack.clear();
        charIter = null;
        firstWhiteSpaceInSeq = null;
    }

    /**
     * Handle white-space for the fo that is passed in, starting at
     * firstTextNode (when a nested FO is encountered)
     * @param fo    the FO for which to handle white-space
     * @param firstTextNode the node at which to start
     */
    public void handleWhiteSpace(FObjMixed fo, FONode firstTextNode) {
        handleWhiteSpace(fo, firstTextNode, null);
    }

    private void handleWhiteSpace() {

        EOLchecker lfCheck = new EOLchecker(charIter);

        nonWhiteSpaceCount = 0;

        while (charIter.hasNext()) {
            if (!inWhiteSpace) {
                firstWhiteSpaceInSeq = charIter.mark();
            }
            char currentChar = charIter.nextChar();
            int currentCharClass = CharUtilities.classOf(currentChar);
            if (currentCharClass == CharUtilities.LINEFEED
                && linefeedTreatment == Constants.EN_TREAT_AS_SPACE) {
                // if we have a linefeed and it is supposed to be treated
                // like a space, that's what we do and continue
                currentChar = '\u0020';
                charIter.replaceChar('\u0020');
                currentCharClass = CharUtilities.classOf(currentChar);
            }
            switch (CharUtilities.classOf(currentChar)) {
                case CharUtilities.XMLWHITESPACE:
                    // Some kind of whitespace character, except linefeed.
                    if (inWhiteSpace
                            && whiteSpaceCollapse == Constants.EN_TRUE) {
                        // We are in a run of whitespace and should collapse
                        // Just delete the char
                        charIter.remove();
                    } else {
                        // Do the white space treatment here
                        boolean bIgnore = false;

                        switch (whiteSpaceTreatment) {
                            case Constants.EN_IGNORE:
                                bIgnore = true;
                                break;
                            case Constants.EN_IGNORE_IF_BEFORE_LINEFEED:
                                bIgnore = lfCheck.beforeLinefeed();
                                break;
                            case Constants.EN_IGNORE_IF_SURROUNDING_LINEFEED:
                                bIgnore = afterLinefeed
                                           || lfCheck.beforeLinefeed();
                                break;
                            case Constants.EN_IGNORE_IF_AFTER_LINEFEED:
                                bIgnore = afterLinefeed;
                                break;
                            case Constants.EN_PRESERVE:
                                //nothing to do now, replacement takes place later
                                break;
                            default:
                                //nop
                        }
                        // Handle ignore and replacement
                        if (bIgnore) {
                            charIter.remove();
                        } else {
                            // this is to retain a single space between words
                            inWhiteSpace = true;
                            if (currentChar != '\u0020') {
                                charIter.replaceChar('\u0020');
                            }
                        }
                    }
                    break;

                case CharUtilities.LINEFEED:
                    // A linefeed
                    switch (linefeedTreatment) {
                        case Constants.EN_IGNORE:
                            charIter.remove();
                            break;
                        case Constants.EN_TREAT_AS_ZERO_WIDTH_SPACE:
                            charIter.replaceChar(CharUtilities.ZERO_WIDTH_SPACE);
                            inWhiteSpace = false;
                            break;
                        case Constants.EN_PRESERVE:
                            lfCheck.reset();
                            inWhiteSpace = false;
                            afterLinefeed = true; // for following whitespace
                            break;
                        default:
                            //nop
                    }
                    break;

                case CharUtilities.EOT:
                    // A "boundary" objects such as non-character inline
                    // or nested block object was encountered. (? can't happen)
                    // If any whitespace run in progress, finish it.
                    // FALL THROUGH

                default:
                    // Any other character
                    inWhiteSpace = false;
                    afterLinefeed = false;
                    nonWhiteSpaceCount++;
                    lfCheck.reset();
                    break;
            }
        }
    }

    private void addPendingInline() {
        if (pendingInlines == null) {
            pendingInlines = new java.util.ArrayList(5);
        }
        pendingInlines.add(new PendingInline(firstWhiteSpaceInSeq));
    }

    private void handlePendingInlines() {
        if (!(pendingInlines == null || pendingInlines.isEmpty())) {
            if (nonWhiteSpaceCount == 0) {
                /* handle white-space for all pending inlines*/
                PendingInline p;
                for (int i = pendingInlines.size(); --i >= 0;) {
                    p = (PendingInline)pendingInlines.get(i);
                    charIter = (RecursiveCharIterator)p.firstTrailingWhiteSpace;
                    handleWhiteSpace();
                    pendingInlines.remove(p);
                }
            } else {
                /* there is non-white-space text between the pending
                 * inline(s) and the end of the block;
                 * clear list of pending inlines */
                pendingInlines.clear();
            }
        }
    }

    /**
     * Helper class, used during white-space handling to look ahead, and
     * see if the next character is a linefeed (or if there will be
     * an equivalent effect during layout, i.e. end-of-block or
     * the following child is a block-level FO)
     */
    private class EOLchecker {
        private boolean nextIsEOL;
        private RecursiveCharIterator charIter;

        EOLchecker(CharIterator charIter) {
            this.charIter = (RecursiveCharIterator) charIter;
        }

        boolean beforeLinefeed() {
            if (!nextIsEOL) {
                CharIterator lfIter = charIter.mark();
                while (lfIter.hasNext()) {
                    int charClass = CharUtilities.classOf(lfIter.nextChar());
                    if (charClass == CharUtilities.LINEFEED) {
                        if (linefeedTreatment == Constants.EN_PRESERVE) {
                            nextIsEOL = true;
                            return nextIsEOL;
                        }
                    } else if (charClass != CharUtilities.XMLWHITESPACE) {
                        return nextIsEOL;
                    }
                }
                // No more characters == end of text run
                // means EOL if there either is a nested block to be added,
                // or if this is the last text node in the current block
                nextIsEOL = nextChildIsBlockLevel || endOfBlock;
            }
            return nextIsEOL;
        }

        void reset() {
            nextIsEOL = false;
        }
    }

    /**
     * Helper class to store unfinished inline nodes together
     * with an iterator that starts at the first white-space
     * character in the sequence of trailing white-space
     */
    private class PendingInline {
        protected CharIterator firstTrailingWhiteSpace;

        PendingInline(CharIterator firstTrailingWhiteSpace) {
            this.firstTrailingWhiteSpace = firstTrailingWhiteSpace;
        }
    }
}
