/*
 * 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: BreakingAlgorithm.java 1805173 2017-08-16 10:50:04Z ssteiner $ */

package org.apache.fop.layoutmgr;

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

import org.apache.fop.fo.Constants;

/**
 * The set of nodes is sorted into lines indexed into activeLines.
 * The nodes in each line are linked together in a single linked list by the
 * {@link KnuthNode#next} field. The activeLines array contains a link to the head of
 * the linked list in index 'line*2' and a link to the tail at index 'line*2+1'.
 * <p>
 * The set of active nodes can be traversed by
 * <pre>
 * for (int line = startLine; line &lt; endLine; line++) {
 *     for (KnuthNode node = getNode(line); node != null; node = node.next) {
 *         // Do something with 'node'
 *     }
 * }
 * </pre>
 */
public abstract class BreakingAlgorithm {

    /** the logger for the class */
    protected static final Log log = LogFactory.getLog(BreakingAlgorithm.class);

    /** Maximum adjustment ration */
    protected static final int INFINITE_RATIO = 1000;

    private static final int MAX_RECOVERY_ATTEMPTS = 5;

    // constants identifying a subset of the feasible breaks
    /** All feasible breaks are ok. */
    public static final int ALL_BREAKS = 0;
    /** This forbids hyphenation. */
    public static final int NO_FLAGGED_PENALTIES = 1;
    /** wrap-option = "no-wrap". */
    public static final int ONLY_FORCED_BREAKS = 2;

    /** Holder for symbolic literals for the fitness classes */
    static final class FitnessClasses {

        private FitnessClasses() {
        }

        static final int VERY_TIGHT = 0;
        static final int TIGHT = 1;
        static final int LOOSE = 2;
        static final int VERY_LOOSE = 3;

        static final String[] NAMES = {
            "VERY TIGHT", "TIGHT", "LOOSE", "VERY LOOSE"
        };

        /**
         * Figure out the fitness class of this line (tight, loose,
         * very tight or very loose).
         * See the section on "More Bells and Whistles" in Knuth's
         * "Breaking Paragraphs Into Lines".
         *
         * @param adjustRatio the adjustment ratio
         * @return the fitness class
         */
        static int computeFitness(double adjustRatio) {
            if (adjustRatio < -0.5) {
                return FitnessClasses.VERY_TIGHT;
            } else if (adjustRatio <= 0.5) {
                return FitnessClasses.TIGHT;
            } else if (adjustRatio <= 1.0) {
                return FitnessClasses.LOOSE;
            } else {
                return FitnessClasses.VERY_LOOSE;
            }
        }
    }

    // parameters of Knuth's algorithm:
    /** Demerit for consecutive lines ending at flagged penalties. */
    protected int repeatedFlaggedDemerit = KnuthPenalty.FLAGGED_PENALTY;
    /** Demerit for consecutive lines belonging to incompatible fitness classes . */
    protected int incompatibleFitnessDemerit = KnuthPenalty.FLAGGED_PENALTY;
    /** Maximum number of consecutive lines ending with a flagged penalty.
     * Only a value &gt;= 1 is a significant limit.
     */
    protected int maxFlaggedPenaltiesCount;

    /**
     * The threshold for considering breaks to be acceptable. The adjustment ratio must be
     * inferior to this threshold.
     */
    private double threshold;

    /**
     * The paragraph of KnuthElements.
     */
    protected KnuthSequence par;

    /**
     * The width of a line (or height of a column in page-breaking mode).
     * -1 indicates that the line widths are different for each line.
     */
    protected int lineWidth = -1;
    /** Force the algorithm to find a set of breakpoints, even if no feasible breakpoints
     * exist.
     */
    private boolean force;
    /** If set to true, doesn't ignore break possibilities which are definitely too short. */
    protected boolean considerTooShort;

    /** When in forced mode, the best node leading to a too long line. The line will be
     * too long anyway, but this one will lead to a paragraph with fewest demerits.
     */
    private KnuthNode lastTooLong;
    /** When in forced mode, the best node leading to a too short line. The line will be
     * too short anyway, but this one will lead to a paragraph with fewest demerits.
     */
    private KnuthNode lastTooShort;
    /** The node to be reactivated if no set of feasible breakpoints can be found for this
     * paragraph.
     */
    private KnuthNode lastDeactivated;

    /** Alignment of the paragraph/page. One of EN_START, EN_JUSTIFY, etc. */
    protected int alignment;
    /** Alignment of the paragraph's last line. */
    protected int alignmentLast;
    /** Used to handle the text-indent property (indent the first line of a paragraph). */
    protected boolean indentFirstPart;

    /**
     * The set of active nodes in ascending line order. For each line l, activeLines[2l] contains a
     * link to l's first active node, and activeLines[2l+1] a link to l's last active node. The
     * line number l corresponds to the number of the line ending at the node's breakpoint.
     */
    protected KnuthNode[] activeLines;

    /**
     * The number of active nodes.
     */
    protected int activeNodeCount;

    /**
     * The lowest available line in the set of active nodes.
     */
    protected int startLine;

    /**
     * The highest + 1 available line in the set of active nodes.
     */
    protected int endLine;

    /**
     * The total width of all elements handled so far.
     */
    protected int totalWidth;

    /**
     * The total stretch of all elements handled so far.
     */
    protected int totalStretch;

    /**
     * The total shrink of all elements handled so far.
     */
    protected int totalShrink;

    /**
     * Best records.
     */
    protected BestRecords best;

    private boolean partOverflowRecoveryActivated = true;
    private KnuthNode lastRecovered;

    /**
     * Create a new instance.
     *
     * @param align     alignment of the paragraph/page. One of {@link Constants#EN_START},
     *                  {@link Constants#EN_JUSTIFY}, {@link Constants#EN_CENTER},
     *                  {@link Constants#EN_END}.
     *                  For pages, {@link Constants#EN_BEFORE} and {@link Constants#EN_AFTER}
     *                  are mapped to the corresponding inline properties,
     *                  {@link Constants#EN_START} and {@link Constants#EN_END}.
     * @param alignLast alignment of the paragraph's last line
     * @param first     for the text-indent property ({@code true} if the first line
     *                  of a paragraph should be indented)
     * @param partOverflowRecovery  {@code true} if too long elements should be moved to
     *                              the next line/part
     * @param maxFlagCount  maximum allowed number of consecutive lines ending at a flagged penalty
     *                      item
     */
    public BreakingAlgorithm(int align, int alignLast,
                             boolean first, boolean partOverflowRecovery,
                             int maxFlagCount) {
        this.alignment = align;
        this.alignmentLast = alignLast;
        this.indentFirstPart = first;
        this.partOverflowRecoveryActivated = partOverflowRecovery;
        this.best = new BestRecords();
        this.maxFlaggedPenaltiesCount = maxFlagCount;
    }


    /**
     * Class recording all the informations of a feasible breaking point.
     */
    public class KnuthNode {
        /** index of the breakpoint represented by this node */
        public final int position;

        /** number of the line ending at this breakpoint */
        public final int line;

        /** fitness class of the line ending at this breakpoint. One of 0, 1, 2, 3. */
        public final int fitness;

        /** accumulated width of the KnuthElements up to after this breakpoint. */
        public final int totalWidth;

        /** accumulated stretchability of the KnuthElements up to after this breakpoint. */
        public final int totalStretch;

        /** accumulated shrinkability of the KnuthElements up to after this breakpoint. */
        public final int totalShrink;

        /** adjustment ratio if the line ends at this breakpoint */
        public final double adjustRatio;

        /** available stretch of the line ending at this breakpoint */
        public final int availableShrink;

        /** available shrink of the line ending at this breakpoint */
        public final int availableStretch;

        /** difference between target and actual line width */
        public final int difference;

        /** minimum total demerits up to this breakpoint */
        public double totalDemerits;

        /** best node for the preceding breakpoint */
        public KnuthNode previous;

        /** next possible node in the same line */
        public KnuthNode next;

        /**
         * Holds the number of subsequent recovery attempty that are made to get content fit
         * into a line.
         */
        public int fitRecoveryCounter;

        /**
         * Construct node.
         * @param position an integer
         * @param line an integer
         * @param fitness an integer
         * @param totalWidth an integer
         * @param totalStretch an integer
         * @param totalShrink an integer
         * @param adjustRatio a real number
         * @param availableShrink an integer
         * @param availableStretch an integer
         * @param difference an integer
         * @param totalDemerits a real number
         * @param previous a node
         */
        public KnuthNode(int position, int line, int fitness,
                int totalWidth, int totalStretch, int totalShrink,
                double adjustRatio, int availableShrink, int availableStretch,
                int difference, double totalDemerits, KnuthNode previous) {
            this.position = position;
            this.line = line;
            this.fitness = fitness;
            this.totalWidth = totalWidth;
            this.totalStretch = totalStretch;
            this.totalShrink = totalShrink;
            this.adjustRatio = adjustRatio;
            this.availableShrink = availableShrink;
            this.availableStretch = availableStretch;
            this.difference = difference;
            this.totalDemerits = totalDemerits;
            this.previous = previous;
        }

        /** {@inheritDoc} */
        public String toString() {
            return "<KnuthNode at " + position + " "
                    + totalWidth + "+" + totalStretch + "-" + totalShrink
                    + " line:" + line + " prev:" + (previous != null ? previous.position : -1)
                    + " dem:" + totalDemerits
                    + " fitness:" + FitnessClasses.NAMES[fitness] + ">";
        }
    }

    /** Class that stores, for each fitness class, the best active node that could start
     * a line of the corresponding fitness ending at the current element.
     */
    protected class BestRecords {
        private static final double INFINITE_DEMERITS = Double.POSITIVE_INFINITY;

        private final double[] bestDemerits = new double[4];
        private final KnuthNode[] bestNode = new KnuthNode[4];
        private final double[] bestAdjust = new double[4];
        private final int[] bestDifference = new int[4];
        private final int[] bestAvailableShrink = new int[4];
        private final int[] bestAvailableStretch = new int[4];
        /** Points to the fitness class which currently leads to the best demerits. */
        private int bestIndex = -1;

        /** default constructor */
        public BestRecords() {
            reset();
        }

        /** Registers the new best active node for the given fitness class.
         * @param demerits the total demerits of the new optimal set of breakpoints
         * @param node the node starting the line ending at the current element
         * @param adjust adjustment ratio of the current line
         * @param availableShrink how much the current line can be shrinked
         * @param availableStretch how much the current line can be stretched
         * @param difference difference between the width of the considered line and the
         * width of the "real" line
         * @param fitness fitness class of the current line
         */
        public void addRecord(double demerits, KnuthNode node, double adjust,
                              int availableShrink, int availableStretch,
                              int difference, int fitness) {
            if (demerits > bestDemerits[fitness]) {
                log.error("New demerits value greater than the old one");
            }
            bestDemerits[fitness] = demerits;
            bestNode[fitness] = node;
            bestAdjust[fitness] = adjust;
            bestAvailableShrink[fitness] = availableShrink;
            bestAvailableStretch[fitness] = availableStretch;
            bestDifference[fitness] = difference;
            if (bestIndex == -1 || demerits < bestDemerits[bestIndex]) {
                bestIndex = fitness;
            }
        }

        /** @return true if has records (best index not -1) */
        public boolean hasRecords() {
            return (bestIndex != -1);
        }

        /**
         * @param fitness fitness class (0, 1, 2 or 3, i.e. "tight" to "very loose")
         * @return true if there is a set of feasible breakpoints registered for the
         *              given fitness.
         */
        public boolean notInfiniteDemerits(int fitness) {
            return (bestDemerits[fitness] != INFINITE_DEMERITS);
        }

        /**
         * @param fitness to use
         * @return best demerits
         */
        public double getDemerits(int fitness) {
            return bestDemerits[fitness];
        }

        /**
         * @param fitness to use
         * @return best node
         */
        public KnuthNode getNode(int fitness) {
            return bestNode[fitness];
        }

        /**
         * @param fitness to use
         * @return adjustment
         */
        public double getAdjust(int fitness) {
            return bestAdjust[fitness];
        }

        /**
         * @param fitness to use
         * @return available shrink
         */
        public int getAvailableShrink(int fitness) {
            return bestAvailableShrink[fitness];
        }

        /**
         * @param fitness to use
         * @return available stretch
         */
        public int getAvailableStretch(int fitness) {
            return bestAvailableStretch[fitness];
        }

        /**
         * @param fitness to use
         * @return difference
         */
        public int getDifference(int fitness) {
            return bestDifference[fitness];
        }

        /** @return minimum demerits */
        public double getMinDemerits() {
            if (bestIndex != -1) {
                return getDemerits(bestIndex);
            } else {
                // anyway, this should never happen
                return INFINITE_DEMERITS;
            }
        }

        /** Reset when a new breakpoint is being considered. */
        public void reset() {
            for (int i = 0; i < 4; i++) {
                bestDemerits[i] = INFINITE_DEMERITS;
                // there is no need to reset the other arrays
            }
            bestIndex = -1;
        }
    }

    /**
     * @return the number of times the algorithm should try to move overflowing content to the
     * next line/page.
     */
    protected int getMaxRecoveryAttempts() {
        return MAX_RECOVERY_ATTEMPTS;
    }

    /**
     * Controls the behaviour of the algorithm in cases where the first element of a part
     * overflows a line/page.
     * @return true if the algorithm should try to send the element to the next line/page.
     */
    protected boolean isPartOverflowRecoveryActivated() {
        return this.partOverflowRecoveryActivated;
    }

    protected KnuthNode getLastTooLong() {
        return lastTooLong;
    }

    /**
     * Empty method, hook for subclasses. Called before determining the optimal
     * breakpoints corresponding to a given active node.
     * @param total number of lines for the active node
     * @param demerits total demerits of the paragraph for the active node
     */
    public abstract void updateData1(int total, double demerits);

    /**
     * Empty method, hook for subclasses. Called when determining the optimal breakpoints
     * for a given active node.
     * @param bestActiveNode a node in the chain of best active nodes, corresponding to
     * one of the optimal breakpoints
     * @param sequence the corresponding paragraph
     * @param total the number of lines into which the paragraph will be broken
     */
    public abstract void updateData2(KnuthNode bestActiveNode,
                                     KnuthSequence sequence,
                                     int total);

    /** @param lineWidth the line width */
    public void setConstantLineWidth(int lineWidth) {
        this.lineWidth = lineWidth;
    }

    /**
     * @param par           the paragraph to break
     * @param threshold     upper bound of the adjustment ratio
     * @param force         {@code true} if a set of breakpoints must be found, even
     *                      if there are no feasible ones
     * @param allowedBreaks the type(s) of breaks allowed. One of {@link #ONLY_FORCED_BREAKS},
     *                      {@link #NO_FLAGGED_PENALTIES} or {@link #ALL_BREAKS}.
     *
     * @return  the number of effective breaks
     * @see #findBreakingPoints(KnuthSequence, int, double, boolean, int)
     */
    public int findBreakingPoints(KnuthSequence par,
                                  double threshold,
                                  boolean force,
                                  int allowedBreaks) {
        return findBreakingPoints(par, 0, threshold, force, allowedBreaks);
    }

    /**
     * Finds an optimal set of breakpoints for the given paragraph.
     *
     * @param par           the paragraph to break
     * @param startIndex    index of the Knuth element at which the breaking must start
     * @param threshold     upper bound of the adjustment ratio
     * @param force         {@code true} if a set of breakpoints must be found, even
     *                      if there are no feasible ones
     * @param allowedBreaks the type(s) of breaks allowed. One of {@link #ONLY_FORCED_BREAKS},
     *                      {@link #NO_FLAGGED_PENALTIES} or {@link #ALL_BREAKS}.
     *
     * @return  the number of effective breaks
     */
    public int findBreakingPoints(KnuthSequence par, int startIndex,
                                  double threshold, boolean force,
                                  int allowedBreaks) {
        this.par = par;
        this.threshold = threshold;
        this.force = force;

        // initialize the algorithm
        initialize();

        // previous element in the paragraph is a KnuthBox?
        boolean previousIsBox = false;

        // index of the first KnuthBox in the sequence, in case of non-centered
        // alignment. For centered alignment, we need to take into account preceding
        // penalties+glues used for the filler spaces
        int previousPosition = startIndex;
        if (alignment != Constants.EN_CENTER) {
            int firstBoxIndex = par.getFirstBoxIndex(startIndex);
            previousPosition = (firstBoxIndex >= par.size()) ? startIndex : firstBoxIndex - 1;
        }
        previousPosition = (previousPosition < 0) ? 0 : previousPosition;

        // create an active node representing the starting point
        addNode(0, createNode(previousPosition, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, null));
        KnuthNode lastForced = getNode(0);

        if (log.isTraceEnabled()) {
            log.trace("Looping over " + (par.size() - startIndex) + " elements");
            log.trace(par);
        }

        // main loop
        for (int elementIndex = startIndex; elementIndex < par.size(); elementIndex++) {

            previousIsBox = handleElementAt(
                    elementIndex, previousIsBox, allowedBreaks).isBox();

            if (activeNodeCount == 0) {
                if (handlingFloat()) {
                    return handleFloat();
                }
                if (getIPDdifference() != 0) {
                    return handleIpdChange();
                }
                if (!force) {
                    log.debug("Could not find a set of breaking points " + threshold);
                    return 0;
                }

                // lastDeactivated was a "good" break, while lastTooShort and lastTooLong
                // were "bad" breaks since the beginning;
                // if it is not the node we just restarted from, lastDeactivated can
                // replace either lastTooShort or lastTooLong
                if (lastDeactivated != null
                        && lastDeactivated != lastForced) {
                    replaceLastDeactivated();
                }

                if (lastTooShort == null
                        || lastForced.position == lastTooShort.position) {
                    lastForced = recoverFromOverflow();
                } else {
                    lastForced = lastTooShort;
                    this.lastRecovered = null;
                }
                elementIndex = restartFrom(lastForced, elementIndex);
            }

        }

        finish();

        // there is at least one set of breaking points
        // select one or more active nodes, removing the others from the list
        int line = filterActiveNodes();

        // for each active node, create a set of breaking points
        for (int i = startLine; i < endLine; i++) {
            for (KnuthNode node = getNode(i); node != null; node = node.next) {
                updateData1(node.line, node.totalDemerits);
                calculateBreakPoints(node, par, node.line);
            }
        }

        activeLines = null;
        return line;
    }

    /**
     * obtain ipd difference
     * @return an integer
     */
    protected int getIPDdifference() {
        return 0;
    }

    /**
     * handle ipd change
     * @return an integer
     */
    protected int handleIpdChange() {
        throw new IllegalStateException();
    }

    /**
     * Recover from a {@link KnuthNode} leading to a line that is too long.
     * The default implementation creates a new node corresponding to a break
     * point after the previous node that led to a line that was too short.
     *
     * @param lastTooLong   the node that leads to a "too long" line
     * @return  node corresponding to a breakpoint after the previous "too short" line
     */
    protected KnuthNode recoverFromTooLong(KnuthNode lastTooLong) {
        if (log.isDebugEnabled()) {
            log.debug("Recovering from too long: " + lastTooLong);
        }

        // if lastTooLong would be the very first break in the blockList, and
        // the first element in the paragraph is not a penalty, add an auxiliary
        // penalty now to make it possible to create a genuine 'empty' node that
        // represents a break before the first box/glue
        if (lastTooLong.previous.previous == null) {
            ListElement el = (ListElement)this.par.get(0);
            if (!el.isPenalty()) {
                this.par.add(0, KnuthPenalty.DUMMY_ZERO_PENALTY);
            }
        }

        // content would overflow, insert empty line/page and try again
        return createNode(
                lastTooLong.previous.position, lastTooLong.previous.line + 1, 1,
                0, 0, 0,
                0, 0, 0,
                0, 0, lastTooLong.previous);
    }

    /** Initializes the algorithm's variables. */
    protected void initialize() {
        this.totalWidth = 0;
        this.totalStretch = 0;
        this.totalShrink = 0;
        this.lastTooShort = null;
        this.lastTooLong = null;
        this.startLine = 0;
        this.endLine = 0;
        this.activeLines = new KnuthNode[20];
    }

    /**
     * Creates a new active node for a feasible breakpoint at the given position. Only
     * called in forced mode.
     *
     * @param position index of the element in the Knuth sequence
     * @param line number of the line ending at the breakpoint
     * @param fitness fitness class of the line ending at the breakpoint. One of 0, 1, 2, 3.
     * @param totalWidth accumulated width of the KnuthElements up to after the breakpoint
     * @param totalStretch accumulated stretchability of the KnuthElements up to after the
     * breakpoint
     * @param totalShrink accumulated shrinkability of the KnuthElements up to after the
     * breakpoint
     * @param adjustRatio adjustment ratio if the line ends at this breakpoint
     * @param availableShrink available stretch of the line ending at this breakpoint
     * @param availableStretch available shrink of the line ending at this breakpoint
     * @param difference difference between target and actual line width
     * @param totalDemerits minimum total demerits up to the breakpoint
     * @param previous active node for the preceding breakpoint
     * @return a new node
     */
    protected KnuthNode createNode(int position, int line, int fitness,
            int totalWidth, int totalStretch, int totalShrink,
            double adjustRatio, int availableShrink, int availableStretch,
            int difference, double totalDemerits, KnuthNode previous) {
        return new KnuthNode(position, line, fitness,
                             totalWidth, totalStretch, totalShrink,
                             adjustRatio, availableShrink, availableStretch,
                             difference, totalDemerits, previous);
    }

    /** Creates a new active node for a break from the best active node of the given
     * fitness class to the element at the given position.
     * @param position index of the element in the Knuth sequence
     * @param line number of the line ending at the breakpoint
     * @param fitness fitness class of the line ending at the breakpoint. One of 0, 1, 2, 3.
     * @param totalWidth accumulated width of the KnuthElements up to after the breakpoint
     * @param totalStretch accumulated stretchability of the KnuthElements up to after the
     * breakpoint
     * @param totalShrink accumulated shrinkability of the KnuthElements up to after the
     * breakpoint
     * @return a new node
     * @see #createNode(int, int, int, int, int, int, double, int, int, int, double,
     * org.apache.fop.layoutmgr.BreakingAlgorithm.KnuthNode)
     * @see BreakingAlgorithm.BestRecords
     */
    protected KnuthNode createNode(int position, int line, int fitness,
                                   int totalWidth, int totalStretch, int totalShrink) {
        return new KnuthNode(position, line, fitness,
                             totalWidth, totalStretch, totalShrink, best.getAdjust(fitness),
                             best.getAvailableShrink(fitness), best.getAvailableStretch(fitness),
                             best.getDifference(fitness), best.getDemerits(fitness),
                             best.getNode(fitness));
    }

    /**
     * Return the last node that yielded a too short line.
     * @return  the node corresponding to the last too short line
     */
    protected final KnuthNode getLastTooShort() {
        return this.lastTooShort;
    }

    /**
     * Generic handler for a {@link KnuthElement} at the given {@code position},
     * taking into account whether the preceding element was a box, and which
     * type(s) of breaks are allowed.
     * Non-overridable. This method simply serves to route the call to one of the
     * more specific handlers ({@link #handleBox(KnuthBox)},
     * {@link #handleGlueAt(KnuthGlue,int,boolean,int)} or
     * {@link #handlePenaltyAt(KnuthPenalty,int,int)}. The specialized handlers
     * can be overridden by subclasses to add to or modify the default behavior
     * for the different types of elements.
     *
     * @param position      the position index of the element in the paragraph
     * @param previousIsBox {@code true} if the previous element is a box
     * @param allowedBreaks the type(s) of breaks allowed; should be one
     *                      of {@link #ALL_BREAKS}, {@link #NO_FLAGGED_PENALTIES}
     *                      or {@link #ONLY_FORCED_BREAKS}
     * @return  the handled element
     */
    protected final KnuthElement handleElementAt(int position,
                                                 boolean previousIsBox,
                                                 int allowedBreaks) {
        KnuthElement element = getElement(position);
        if (element.isBox()) {
            handleBox((KnuthBox) element);
        } else if (element.isGlue()) {
            handleGlueAt((KnuthGlue) element, position, previousIsBox, allowedBreaks);
        } else if (element.isPenalty()) {
            handlePenaltyAt((KnuthPenalty) element, position, allowedBreaks);
        } else {
            throw new IllegalArgumentException(
                    "Unknown KnuthElement type: expecting KnuthBox, KnuthGlue or KnuthPenalty");
        }
        return element;
    }

    /**
     * Handle a {@link KnuthBox}.
     * <br><em>Note: default implementation just adds the box's width
     * to the total content width. Subclasses that do not keep track
     * of this themselves, but override this method, should remember
     * to call {@code super.handleBox(box)} to avoid unwanted side-effects.</em>
     *
     * @param box   the {@link KnuthBox} to handle
     */
    protected void handleBox(KnuthBox box) {
        // a KnuthBox object is not a legal line break,
        // just add the width to the total
        totalWidth += box.getWidth();
    }

    /**
     * Handle a {@link KnuthGlue} at the given position,
     * taking into account the additional parameters.
     *
     * @param glue   the {@link KnuthGlue} to handle
     * @param position   the position of the glue in the list
     * @param previousIsBox {@code true} if the preceding element is a box
     * @param allowedBreaks the type of breaks that are allowed
     */
    protected void handleGlueAt(KnuthGlue glue, int position,
                                boolean previousIsBox, int allowedBreaks) {
        // a KnuthGlue object is a legal line break
        // only if the previous object is a KnuthBox
        // consider these glues according to the value of allowedBreaks
        if (previousIsBox
            && !(allowedBreaks == ONLY_FORCED_BREAKS)) {
            considerLegalBreak(glue, position);
        }
        totalWidth += glue.getWidth();
        totalStretch += glue.getStretch();
        totalShrink += glue.getShrink();
    }

    /**
     * Handle a {@link KnuthPenalty} at the given position,
     * taking into account the type of breaks allowed.
     *
     * @param penalty   the {@link KnuthPenalty} to handle
     * @param position  the position of the penalty in the list
     * @param allowedBreaks the type of breaks that are allowed
     */
    protected void handlePenaltyAt(KnuthPenalty penalty, int position,
                                   int allowedBreaks) {
        // a KnuthPenalty is a legal line break
        // only if its penalty is not infinite;
        // consider all penalties, non-flagged penalties or non-forcing penalties
        // according to the value of allowedBreaks
        if (((penalty.getPenalty() < KnuthElement.INFINITE)
                && (!(allowedBreaks == NO_FLAGGED_PENALTIES) || !penalty.isPenaltyFlagged())
                && (!(allowedBreaks == ONLY_FORCED_BREAKS)
                        || penalty.isForcedBreak()))) {
            considerLegalBreak(penalty, position);
        }
    }

    /**
     * Replace the last too-long or too-short node by the last deactivated
     * node, if applicable.
     */
    protected final void replaceLastDeactivated() {
        if (lastDeactivated.adjustRatio > 0) {
            //last deactivated was too short
            lastTooShort = lastDeactivated;
        } else {
            //last deactivated was too long or exactly the right width
            lastTooLong = lastDeactivated;
        }
    }

    /**
     * Recover from an overflow condition.
     *
     * @return  the new {@code lastForced} node
     */
    protected KnuthNode recoverFromOverflow() {
        KnuthNode lastForced;
        if (isPartOverflowRecoveryActivated()) {
            if (lastRecovered == null) {
                lastRecovered = lastTooLong;
                if (log.isDebugEnabled()) {
                    log.debug("Recovery point: " + lastRecovered);
                }
            }
            KnuthNode node = recoverFromTooLong(lastTooLong);
            lastForced = node;
            node.fitRecoveryCounter = lastTooLong.previous.fitRecoveryCounter + 1;
            if (log.isDebugEnabled()) {
                log.debug("first part doesn't fit into line, recovering: "
                        + node.fitRecoveryCounter);
            }
            if (node.fitRecoveryCounter > getMaxRecoveryAttempts()) {
                while (lastForced.fitRecoveryCounter > 0
                        && lastForced.previous != null) {
                    lastForced = lastForced.previous;
                    lastDeactivated = lastForced.previous;
                }
                lastForced = lastRecovered;
                lastRecovered = null;
                startLine = lastForced.line;
                endLine = lastForced.line;
                log.debug("rolled back...");
            }
        } else {
            lastForced = lastTooLong;
        }
        return lastForced;
    }

    /**
     * Restart from the given node at the given index.
     *
     * @param restartingNode    the {@link KnuthNode} to restart from
     * @param currentIndex      the current position index
     * @return  the index of the restart point
     */
    protected int restartFrom(KnuthNode restartingNode, int currentIndex) {
        if (log.isDebugEnabled()) {
            log.debug("Restarting at node " + restartingNode);
        }

        restartingNode.totalDemerits = 0;
        addNode(restartingNode.line, restartingNode);
        startLine = restartingNode.line;
        endLine = startLine + 1;
        totalWidth = restartingNode.totalWidth;
        totalStretch = restartingNode.totalStretch;
        totalShrink = restartingNode.totalShrink;
        lastTooShort = null;
        lastTooLong = null;
        // the width, stretch and shrink already include the width,
        // stretch and shrink of the suppressed glues;
        // advance in the sequence in order to avoid taking into account
        // these elements twice
        int restartingIndex = restartingNode.position;
        while (restartingIndex + 1 < par.size()
               && !(getElement(restartingIndex + 1).isBox())) {
            restartingIndex++;
        }
        return restartingIndex;
    }

    /**
     * Determines if the given breakpoint is a feasible breakpoint. That is, if a decent
     * line may be built between one of the currently active nodes and this breakpoint.
     * @param element the paragraph's element to consider
     * @param elementIdx the element's index inside the paragraph
     */
    protected void considerLegalBreak(KnuthElement element, int elementIdx) {

        if (log.isTraceEnabled()) {
            log.trace("considerLegalBreak() at " + elementIdx
                    + " (" + totalWidth + "+" + totalStretch + "-" + totalShrink
                    + "), parts/lines: " + startLine + "-" + endLine);
            log.trace("\tCurrent active node list: " + activeNodeCount + " " + this.toString("\t"));
        }

        lastDeactivated = null;
        lastTooLong = null;
        for (int line = startLine; line < endLine; line++) {
            for (KnuthNode node = getNode(line); node != null; node = node.next) {
                if (node.position == elementIdx) {
                    continue;
                }
                int difference = computeDifference(node, element, elementIdx);
                if (!elementCanEndLine(element, endLine, difference)) {
                    log.trace("Skipping legal break");
                    break;
                }

                double r = computeAdjustmentRatio(node, difference);
                int availableShrink = totalShrink - node.totalShrink;
                int availableStretch = totalStretch - node.totalStretch;

                if (log.isTraceEnabled()) {
                    log.trace("\tr=" + r + " difference=" + difference);
                    log.trace("\tline=" + line);
                }

                if (element.isForcedBreak() && handlingFloat()) {
                    disableFloatHandling(); // so that we do not create a float edge position later
                }
                // The line would be too long.
                if (r < -1 || element.isForcedBreak() || handlingFloat()) {
                    deactivateNode(node, line);
                }

                int fitnessClass = FitnessClasses.computeFitness(r);
                double demerits = computeDemerits(node, element, fitnessClass, r);
                // The line is within the available shrink and the threshold.
                if (r >= -1 && r <= threshold) {
                    activateNode(node, difference, r,
                            demerits, fitnessClass, availableShrink, availableStretch);
                }

                // The line is way too short/long, but we are in forcing mode, so a node is
                // calculated and stored in lastValidNode.
                if (force && (r <= -1 || r > threshold)) {
                    forceNode(node, line, elementIdx, difference, r,
                            demerits, fitnessClass, availableShrink, availableStretch);
                }
            }
            addBreaks(line, elementIdx);
        }
    }

    /**
     * Check if the given {@link KnuthElement} can end the line with the given
     * number.
     * @param element   the element
     * @param line      the line number
     * @param difference an integer
     * @return  {@code true} if the element can end the line
     */
    protected boolean elementCanEndLine(KnuthElement element, int line, int difference) {
        return (!element.isPenalty()
                || element.getPenalty() < KnuthElement.INFINITE);
    }

    /**
     * Force the given {@link KnuthNode}, and register it.
     *
     * @param node  the node
     * @param line  the line number
     * @param elementIdx    the position index of the element
     * @param difference    the difference between content-length and available width
     * @param r     the adjustment ratio
     * @param demerits  demerits produced by the node
     * @param fitnessClass  the fitness class
     * @param availableShrink   the available amount of shrink
     * @param availableStretch  tha available amount of stretch
     */
    protected void forceNode(KnuthNode node,
                             int line,
                             int elementIdx,
                             int difference,
                             double r,
                             double demerits,
                             int fitnessClass,
                             int availableShrink,
                             int availableStretch) {

        int newWidth = totalWidth;
        int newStretch = totalStretch;
        int newShrink = totalShrink;

        // add the width, stretch and shrink of glue elements after
        // the break
        // this does not affect the dimension of the line / page, only
        // the values stored in the node; these would be as if the break
        // was just before the next box element, thus ignoring glues and
        // penalties between the "real" break and the following box
        for (int i = elementIdx; i < par.size(); i++) {
            KnuthElement tempElement = getElement(i);
            if (tempElement.isBox()) {
                break;
            } else if (tempElement.isGlue()) {
                newWidth += tempElement.getWidth();
                newStretch += tempElement.getStretch();
                newShrink += tempElement.getShrink();
            } else if (tempElement.isForcedBreak() && i != elementIdx) {
                break;
            }
        }

        createForcedNodes(node, line, elementIdx, difference, r, demerits, fitnessClass, availableShrink,
                availableStretch, newWidth, newStretch, newShrink);
    }

    protected void createForcedNodes(KnuthNode node, int line, int elementIdx, int difference, double r,
            double demerits, int fitnessClass, int availableShrink, int availableStretch, int newWidth,
            int newStretch, int newShrink) {
        if (r <= -1) {
            log.debug("Considering tooLong, demerits=" + demerits);
            if (lastTooLong == null || demerits < lastTooLong.totalDemerits) {
                lastTooLong = createNode(elementIdx, line + 1, fitnessClass,
                        newWidth, newStretch, newShrink,
                        r, availableShrink, availableStretch,
                        difference, demerits, node);
                if (log.isTraceEnabled()) {
                    log.trace("Picking tooLong " + lastTooLong);
                }
            }
        } else {
            if (lastTooShort == null || demerits <= lastTooShort.totalDemerits) {
                if (considerTooShort) {
                    // consider possibilities which are too short
                    best.addRecord(demerits, node, r, availableShrink, availableStretch, difference,
                            fitnessClass);
                }
                lastTooShort = createNode(elementIdx, line + 1, fitnessClass,
                        newWidth, newStretch, newShrink,
                        r, availableShrink, availableStretch,
                        difference, demerits, node);
                if (log.isTraceEnabled()) {
                    log.trace("Picking tooShort " + lastTooShort);
                }
            }
        }
    }

    /**
     * Activate the given node. Will result in the given {@link KnuthNode}
     * being registered as a feasible breakpoint, if the {@code demerits} are better
     * than that of the best node registered for the given {@code fitnessClass}.
     *
     * @param node  the node
     * @param difference    the difference between content-length and available width
     * @param r     the adjustment ratio
     * @param demerits  demerits produced by the node
     * @param fitnessClass  the fitness class
     * @param availableShrink   the available amount of shrink
     * @param availableStretch  the available amount of stretch
     */
    protected void activateNode(KnuthNode node,
                                int difference,
                                double r,
                                double demerits,
                                int fitnessClass,
                                int availableShrink,
                                int availableStretch) {

        if (log.isTraceEnabled()) {
            log.trace("\tDemerits=" + demerits);
            log.trace("\tFitness class=" + FitnessClasses.NAMES[fitnessClass]);
        }

        if (demerits < best.getDemerits(fitnessClass)) {
            // updates best demerits data
            best.addRecord(demerits, node, r, availableShrink, availableStretch,
                           difference, fitnessClass);
            lastTooShort = null;
        }
    }

    /**
     * Deactivate the given node
     *
     * @param node  the node
     * @param line  the line number
     */
    protected void deactivateNode(KnuthNode node, int line) {
        // Deactivate node...
        if (log.isTraceEnabled()) {
            log.trace("Removing " + node);
        }
        removeNode(line, node);
        // ... and remember it, if it was a good candidate
        lastDeactivated = compareNodes(lastDeactivated, node);
    }

    /**
     * Adds new active nodes for breaks at the given element.
     * @param line number of the previous line; this element will end line number (line+1)
     * @param elementIdx the element's index
     */
    private void addBreaks(int line, int elementIdx) {
        if (!best.hasRecords()) {
            return;
        }

        int newWidth = totalWidth;
        int newStretch = totalStretch;
        int newShrink = totalShrink;

        // add the width, stretch and shrink of glue elements after
        // the break
        // this does not affect the dimension of the line / page, only
        // the values stored in the node; these would be as if the break
        // was just before the next box element, thus ignoring glues and
        // penalties between the "real" break and the following box
        for (int i = elementIdx; i < par.size(); i++) {
            KnuthElement tempElement = getElement(i);
            if (tempElement.isBox()) {
                break;
            } else if (tempElement.isGlue()) {
                newWidth += tempElement.getWidth();
                newStretch += tempElement.getStretch();
                newShrink += tempElement.getShrink();
            } else if (tempElement.isForcedBreak() && i != elementIdx) {
                break;
            }
        }

        // add nodes to the active nodes list
        double minimumDemerits = best.getMinDemerits() + incompatibleFitnessDemerit;
        for (int i = 0; i <= 3; i++) {
            if (best.notInfiniteDemerits(i) && best.getDemerits(i) <= minimumDemerits) {
                // the nodes in activeList must be ordered
                // by line number and position;
                if (log.isTraceEnabled()) {
                    log.trace("\tInsert new break in list of " + activeNodeCount
                            + " from fitness class " + FitnessClasses.NAMES[i]);
                }
                KnuthNode newNode = createNode(elementIdx, line + 1, i,
                                               newWidth, newStretch, newShrink);
                addNode(line + 1, newNode);
            }
        }
        best.reset();
    }

    /**
     * Return the difference between the natural width of a line that would be made
     * between the given active node and the given element, and the available width of the
     * real line.
     * @param activeNode    node for the previous breakpoint
     * @param element       currently considered breakpoint
     * @param elementIndex  index of the element that is considered as a breakpoint
     * @return The difference in width. Positive numbers mean extra space in the line,
     * negative number that the line overflows.
     */
    protected int computeDifference(KnuthNode activeNode, KnuthElement element,
                                    int elementIndex) {
        // compute the adjustment ratio
        int actualWidth = totalWidth - activeNode.totalWidth;
        if (element.isPenalty()) {
            actualWidth += element.getWidth();
        }
        return getLineWidth() - actualWidth;
    }

    /**
     * Return the adjustment ratio needed to make up for the difference. A ratio of
     * <ul>
     *    <li>0 means that the break has the exact right width</li>
     *    <li>&gt;= -1 &amp;&amp; &lt; 0  means that the break is wider than the line,
     *        but within the minimim values of the glues.</li>
     *    <li>&gt;0 &amp;&amp; &lt; 1 means that the break is smaller than the line width,
     *        but within the maximum values of the glues.</li>
     *    <li>&gt; 1 means that the break is too small to make up for the glues.</li>
     * </ul>
     * @param activeNode    the currently active node
     * @param difference    the difference between content-length and available width
     * @return The adjustment ratio.
     */
    protected double computeAdjustmentRatio(KnuthNode activeNode, int difference) {
        // compute the adjustment ratio
        if (difference > 0) {
            int maxAdjustment = totalStretch - activeNode.totalStretch;
            if (maxAdjustment > 0) {
                return (double) difference / maxAdjustment;
            } else {
                return INFINITE_RATIO;
            }
        } else if (difference < 0) {
            int maxAdjustment = totalShrink - activeNode.totalShrink;
            if (maxAdjustment > 0) {
                return (double) difference / maxAdjustment;
            } else {
                return -INFINITE_RATIO;
            }
        } else {
            return 0;
        }
    }

    /**
     * Computes the demerits of the current breaking (that is, up to the given element),
     * if the next-to-last chosen breakpoint is the given active node. This adds to the
     * total demerits of the given active node, the demerits of a line starting at this
     * node and ending at the given element.
     * @param activeNode considered preceding line break
     * @param element considered current line break
     * @param fitnessClass fitness of the current line
     * @param r adjustment ratio for the current line
     * @return the demerit of the current line
     */
    protected double computeDemerits(KnuthNode activeNode, KnuthElement element,
                                  int fitnessClass, double r) {
        double demerits = 0;
        // compute demerits
        double f = Math.abs(r);
        f = 1 + 100 * f * f * f;
        if (element.isPenalty()) {
            double penalty = element.getPenalty();
            if (penalty >= 0) {
                f += penalty;
                demerits = f * f;
            } else if (!element.isForcedBreak()) {
                demerits = f * f - penalty * penalty;
            } else {
                demerits = f * f;
            }
        } else {
            demerits = f * f;
        }

        if (element.isPenalty() && ((KnuthPenalty) element).isPenaltyFlagged()
            && getElement(activeNode.position).isPenalty()
            && ((KnuthPenalty) getElement(activeNode.position)).isPenaltyFlagged()) {
            // add demerit for consecutive breaks at flagged penalties
            demerits += repeatedFlaggedDemerit;
            // there are at least two consecutive lines ending with a flagged penalty;
            // check if the previous line end with a flagged penalty too,
            // and if this situation is allowed
            int flaggedPenaltiesCount = 2;
            for (KnuthNode prevNode = activeNode.previous;
                 prevNode != null && flaggedPenaltiesCount <= maxFlaggedPenaltiesCount;
                 prevNode = prevNode.previous) {
                KnuthElement prevElement = getElement(prevNode.position);
                if (prevElement.isPenalty()
                    && ((KnuthPenalty) prevElement).isPenaltyFlagged()) {
                    // the previous line ends with a flagged penalty too
                    flaggedPenaltiesCount++;
                } else {
                    // the previous line does not end with a flagged penalty,
                    // exit the loop
                    break;
                }
            }
            if (maxFlaggedPenaltiesCount >= 1
                && flaggedPenaltiesCount > maxFlaggedPenaltiesCount) {
                // add infinite demerits, so this break will not be chosen
                // unless there isn't any alternative break
                demerits += BestRecords.INFINITE_DEMERITS;
            }
        }
        if (Math.abs(fitnessClass - activeNode.fitness) > 1) {
            // add demerit for consecutive breaks
            // with very different fitness classes
            demerits += incompatibleFitnessDemerit;
        }
        demerits += activeNode.totalDemerits;
        return demerits;
    }

    /**
     * Hook for subclasses to trigger special behavior after ending the
     * main loop in {@link #findBreakingPoints(KnuthSequence,int,double,boolean,int)}
     */
    protected void finish() {
        if (log.isTraceEnabled()) {
            log.trace("Main loop completed " + activeNodeCount);
            log.trace("Active nodes=" + toString(""));
        }
    }

    /**
     * Return the element at index idx in the paragraph.
     * @param idx index of the element.
     * @return the element at index idx in the paragraph.
     */
    protected KnuthElement getElement(int idx) {
        return (KnuthElement) par.get(idx);
    }

    /**
     * Compare two KnuthNodes and return the node with the least demerit.
     * @param node1 The first knuth node.
     * @param node2 The other knuth node.
     * @return the node with the least demerit.
     */
    protected KnuthNode compareNodes(KnuthNode node1, KnuthNode node2) {
        if (node1 == null || node2.position > node1.position) {
            return node2;
        }
        if (node2.position == node1.position) {
            if (node2.totalDemerits < node1.totalDemerits) {
                return node2;
            }
        }
        return node1;
    }

    /**
     * Add a node at the end of the given line's existing active nodes.
     * If this is the first node in the line, adjust endLine accordingly.
     * @param line number of the line ending at the node's corresponding breakpoint
     * @param node the active node to add
     */
    protected void addNode(int line, KnuthNode node) {
        int headIdx = line * 2;
        if (headIdx >= activeLines.length) {
            KnuthNode[] oldList = activeLines;
            activeLines = new KnuthNode[headIdx + headIdx];
            System.arraycopy(oldList, 0, activeLines, 0, oldList.length);
        }
        node.next = null;
        if (activeLines[headIdx + 1] != null) {
            activeLines[headIdx + 1].next = node;
        } else {
            activeLines[headIdx] = node;
            endLine = line + 1;
        }
        activeLines[headIdx + 1] = node;
        activeNodeCount++;
    }

    /**
     * Remove the given active node registered for the given line. If there are no more active nodes
     * for this line, adjust the startLine accordingly.
     * @param line number of the line ending at the node's corresponding breakpoint
     * @param node the node to deactivate
     */
    protected void removeNode(int line, KnuthNode node) {
        int headIdx = line * 2;
        KnuthNode n = getNode(line);
        if (n != node) {
            // nodes could be rightly deactivated in a different order
            KnuthNode prevNode = null;
            while (n != node) {
                prevNode = n;
                n = n.next;
            }
            prevNode.next = n.next;
            if (prevNode.next == null) {
                activeLines[headIdx + 1] = prevNode;
            }
        } else {
            activeLines[headIdx] = node.next;
            if (node.next == null) {
                activeLines[headIdx + 1] = null;
            }
            while (startLine < endLine && getNode(startLine) == null) {
                startLine++;
            }
        }
        activeNodeCount--;
    }

    /**
     * Returns the first active node for the given line.
     * @param line the line/part number
     * @return the requested active node
     */
    protected KnuthNode getNode(int line) {
        return activeLines[line * 2];
    }

    /**
     * Returns the line/part width of a given line/part.
     * @param line the line/part number
     * @return the width/length in millipoints
     */
    protected int getLineWidth(int line) {
        assert lineWidth >= 0;
        return this.lineWidth;
    }

    /** @return the constant line/part width or -1 if there is no such value */
    protected int getLineWidth() {
        return this.lineWidth;
    }

    /**
     * Creates a string representation of the active nodes. Used for debugging.
     * @param prepend a string to prepend on each entry
     * @return the requested string
     */
    public String toString(String prepend) {
        StringBuffer sb = new StringBuffer();
        sb.append("[\n");
        for (int i = startLine; i < endLine; i++) {
            for (KnuthNode node = getNode(i); node != null; node = node.next) {
                sb.append(prepend).append('\t').append(node).append(",\n");
            }
        }
        sb.append(prepend).append("]");
        return sb.toString();
    }

    /**
     * Filter active nodes.
     * @return an integer
     */
    protected abstract int filterActiveNodes();

    /**
     * Determines the set of optimal breakpoints corresponding to the given active node.
     * @param node the active node
     * @param par the corresponding paragraph
     * @param total the number of lines into which the paragraph will be broken
     */
    protected void calculateBreakPoints(KnuthNode node, KnuthSequence par,
                                      int total) {
        KnuthNode bestActiveNode = node;
        // use bestActiveNode to determine the optimum breakpoints
        for (int i = node.line; i > 0; i--) {
            updateData2(bestActiveNode, par, total);
            bestActiveNode = bestActiveNode.previous;
        }
    }

    /** @return the alignment for normal lines/parts */
    public int getAlignment() {
        return this.alignment;
    }

    /** @return the alignment for the last line/part */
    public int getAlignmentLast() {
        return this.alignmentLast;
    }

    protected boolean handlingFloat() {
        return false;
    }

    protected int handleFloat() {
        throw new IllegalStateException();
    }

    protected void disableFloatHandling() {
        throw new IllegalStateException();
    }
}
