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

package org.apache.fop.complexscripts.bidi;

import java.util.List;
import java.util.Vector;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.fop.fo.CharIterator;
import org.apache.fop.fo.FONode;
import org.apache.fop.fo.FObj;
import org.apache.fop.traits.Direction;
import org.apache.fop.traits.WritingModeTraits;
import org.apache.fop.traits.WritingModeTraitsGetter;
import org.apache.fop.util.CharUtilities;

// CSOFF: LineLengthCheck

/**
 * The <code>DelimitedTextRange</code> class implements the "delimited text range" as described
 * by XML-FO 1.1 §5.8, which contains a flattened sequence of characters. Any FO that generates
 * block areas serves as a delimiter.
 *
 * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
 */
public class DelimitedTextRange {
    private FONode fn;                              // node that generates this text range
    private StringBuffer buffer;                    // flattened character sequence of generating FO nodes
    private List intervals;                         // list of intervals over buffer of generating FO nodes
    /**
     * Primary constructor.
     * @param fn node that generates this text range
     */
    public DelimitedTextRange(FONode fn) {
        this.fn = fn;
        this.buffer = new StringBuffer();
        this.intervals = new Vector();
    }
    /**
     * Obtain node that generated this text range.
     * @return node that generated this text range
     */
    public FONode getNode() {
        return fn;
    }
    /**
     * Append interval using characters from character iterator IT.
     * @param it character iterator
     * @param fn node that generates interval being appended
     */
    public void append(CharIterator it, FONode fn) {
        if (it != null) {
            int s = buffer.length();
            int e = s;
            while (it.hasNext()) {
                char c = it.nextChar();
                buffer.append(c);
                e++;
            }
            intervals.add(new TextInterval(fn, s, e));
        }
    }
    /**
     * Append interval using character C.
     * @param c character
     * @param fn node that generates interval being appended
     */
    public void append(char c, FONode fn) {
        if (c != 0) {
            int s = buffer.length();
            int e = s + 1;
            buffer.append(c);
            intervals.add(new TextInterval(fn, s, e));
        }
    }
    /**
     * Determine if range is empty.
     * @return true if range is empty
     */
    public boolean isEmpty() {
        return buffer.length() == 0;
    }
    /**
     * Resolve bidirectional levels for this range.
     */
    public void resolve() {
        WritingModeTraitsGetter tg;
        if ((tg = WritingModeTraits.getWritingModeTraitsGetter(getNode())) != null) {
            resolve(tg.getInlineProgressionDirection());
        }
    }
    @Override
    public String toString() {
        StringBuffer sb = new StringBuffer("DR: " + fn.getLocalName() + " { <" + CharUtilities.toNCRefs(buffer.toString()) + ">");
        sb.append(", intervals <");
        boolean first = true;
        for (Object interval : intervals) {
            TextInterval ti = (TextInterval) interval;
            if (first) {
                first = false;
            } else {
                sb.append(',');
            }
            sb.append(ti.toString());
        }
        sb.append("> }");
        return sb.toString();
    }
    private void resolve(Direction paragraphEmbeddingLevel) {
        int [] levels;
        if ((levels = UnicodeBidiAlgorithm.resolveLevels(buffer, paragraphEmbeddingLevel)) != null) {
            assignLevels(levels);
            assignBlockLevel(paragraphEmbeddingLevel);
            assignTextLevels();
        }
    }
    /**
     * <p>Assign resolved levels to all text intervals of this delimited text range.</p>
     * <p>Has a possible side effect of replacing the intervals array with a new array
     * containing new text intervals, such that each text interval is associated with
     * a single level run.</p>
     * @param levels array of levels each corresponding to each index of the delimited
     * text range
     */
    private void assignLevels(int[] levels) {
        Vector intervalsNew = new Vector(intervals.size());
        for (Object interval : intervals) {
            TextInterval ti = (TextInterval) interval;
            intervalsNew.addAll(assignLevels(ti, levels));
        }
        if (!intervalsNew.equals(intervals)) {
            intervals = intervalsNew;
        }
    }
    /**
     * <p>Assign resolved levels to a specified text interval over this delimited text
     * range.</p>
     * <p>Returns a list of text intervals containing either (1) the single, input text
     * interval or (2) two or more new text intervals obtained from sub-dividing the input
     * text range into level runs, i.e., runs of text assigned to a single level.</p>
     * @param ti a text interval to which levels are to be assigned
     * @param levels array of levels each corresponding to each index of the delimited
     * text range
     * @return a list of text intervals as described above
     */
    private static final Log log = LogFactory.getLog(BidiResolver.class);
    private List assignLevels(TextInterval ti, int[] levels) {
        Vector tiv = new Vector();
        FONode fn = ti.getNode();
        int fnStart = ti.getStart();                                     // start of node's text in delimited text range
        for (int i = fnStart, n = ti.getEnd(); i < n; ) {
            int s = i;                                              // inclusive start index of interval in delimited text range
            int e = s;                                              // exclusive end index of interval in delimited text range
            int l = levels [ s ];                                   // current run level
            while (e < n) {                                       // skip to end of run level or end of interval
                if (levels [ e ] != l) {
                    break;
                } else {
                    e++;
                }
            }
            if ((ti.getStart() == s) && (ti.getEnd() == e)) {
                ti.setLevel(l);                                       // reuse interval, assigning it single level
            } else {
                ti = new TextInterval(fn, fnStart, s, e, l);     // subdivide interval
            }
            if (log.isDebugEnabled()) {
                log.debug("AL(" + l + "): " + ti);
            }
            tiv.add(ti);
            i = e;
        }
        return tiv;
    }
    /**
     * <p>Assign resolved levels for each interval to source #PCDATA in the associated FOText.</p>
     */
    private void assignTextLevels() {
        for (Object interval : intervals) {
            TextInterval ti = (TextInterval) interval;
            ti.assignTextLevels();
        }
    }
    private void assignBlockLevel(Direction paragraphEmbeddingLevel) {
        int defaultLevel = (paragraphEmbeddingLevel == Direction.RL) ? 1 : 0;
        for (Object interval : intervals) {
            TextInterval ti = (TextInterval) interval;
            assignBlockLevel(ti.getNode(), defaultLevel);
        }
    }
    private void assignBlockLevel(FONode node, int defaultLevel) {
        for (FONode fn = node; fn != null; fn = fn.getParent()) {
            if (fn instanceof FObj) {
                FObj fo = (FObj) fn;
                if (fo.isBidiRangeBlockItem()) {
                    if (fo.getBidiLevel() < 0) {
                        fo.setBidiLevel(defaultLevel);
                    }
                    break;
                }
            }
        }
    }
}

