/*
 * 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: LayoutContext.java 1610839 2014-07-15 20:25:58Z vhennebert $ */

package org.apache.fop.layoutmgr;

import java.util.Collections;
import java.util.List;

import org.apache.fop.fo.Constants;
import org.apache.fop.layoutmgr.inline.AlignmentContext;
import org.apache.fop.layoutmgr.inline.HyphContext;
import org.apache.fop.traits.MinOptMax;
import org.apache.fop.traits.WritingMode;


/**
 * This class is used to pass information to the getNextKnuthElements()
 * method. It is set up by higher level LM and used by lower level LM.
 */
public final class LayoutContext {

    /** Generated break possibility is first in a new area */
    public static final int NEW_AREA = 0x01;

    /**
     * If this flag is set, it indicates that any break-before values other than "auto" should
     * not cause a mandatory break as this break was already handled by a parent layout manager.
     */
    public static final int SUPPRESS_BREAK_BEFORE = 0x02;

    public static final int FIRST_AREA = 0x04;

    public static final int LAST_AREA = 0x08;

    public static final int RESOLVE_LEADING_SPACE = 0x10;

    private static final int TREAT_AS_ARTIFACT = 0x20;

    private int flags; // Contains some set of flags defined above

    /**
     * Total available stacking dimension for a "galley-level" layout
     * manager in block-progression-direction. It is passed by the
     * parent LM.
     * These LM <b>may</b> wish to pass this information down to lower
     * level LM to allow them to optimize returned break possibilities.
     */
    private MinOptMax stackLimitBP;

    /** to keep track of spanning in multi-column layout */
    private int currentSpan = Constants.NOT_SET;
    private int nextSpan = Constants.NOT_SET;

    /** inline-progression-dimension of nearest ancestor reference area */
    private int refIPD;
    //TODO After the split of stackLimit into stackLimitBP and stackLimitIP there's now some
    //overlap with refIPD. Need to investigate how best to refactor that.

    /** the writing mode established by the nearest ancestor reference area */
    private WritingMode writingMode = WritingMode.LR_TB;

    /** Current pending space-after or space-end from preceding area */
    private SpaceSpecifier trailingSpace;

    /** Current pending space-before or space-start from ancestor areas */
    private SpaceSpecifier leadingSpace;

    /**
     * A list of pending marks (border and padding) on the after edge when a page break occurs.
     * May be null.
     */
    private List pendingAfterMarks;

    /**
     * A list of pending marks (border and padding) on the before edge when a page break occurs.
     * May be null.
     */
    private List pendingBeforeMarks;

    /** Current hyphenation context. May be null. */
    private HyphContext hyphContext;

    /** Alignment in BP direction */
    private int bpAlignment = Constants.EN_START;

    /** Stretch or shrink value when making areas. */
    private double ipdAdjust;

    /** Stretch or shrink value when adding spaces. */
    private double dSpaceAdjust;

    private AlignmentContext alignmentContext;

    /** Amount of space before / start */
    private int spaceBefore;
    /** Amount of space after / end */
    private int spaceAfter;

    /** Amount of space to reserve at the beginning of each line */
    private int lineStartBorderAndPaddingWidth;
    /** Amount of space to reserve at the end of each line */
    private int lineEndBorderAndPaddingWidth;

    private int breakBefore;
    private int breakAfter;

    private Keep pendingKeepWithNext = Keep.KEEP_AUTO;
    private Keep pendingKeepWithPrevious = Keep.KEEP_AUTO;

    private int disableColumnBalancing;

    public static LayoutContext newInstance() {
        return new LayoutContext(0);
    }

    public static LayoutContext copyOf(LayoutContext copy) {
        return new LayoutContext(copy);
    }

    /**
     * Returns a descendant of the given layout context. The new context is the same as
     * what would have been created by {@link #newInstance()}, except for inheritable
     * properties that are passed on by the parent. At the moment, the only inheritable
     * property is the value returned by {@link #treatAsArtifact()}.
     */
    public static LayoutContext offspringOf(LayoutContext parent) {
        LayoutContext offspring = new LayoutContext(0);
        offspring.setTreatAsArtifact(parent.treatAsArtifact());
        return offspring;
    }

    private LayoutContext(LayoutContext parentLC) {
        this.flags = parentLC.flags;
        this.refIPD = parentLC.refIPD;
        this.writingMode = parentLC.writingMode;
        setStackLimitBP(parentLC.getStackLimitBP());
        this.leadingSpace = parentLC.leadingSpace; //???
        this.trailingSpace = parentLC.trailingSpace; //???
        this.hyphContext = parentLC.hyphContext;
        this.bpAlignment = parentLC.bpAlignment;
        this.dSpaceAdjust = parentLC.dSpaceAdjust;
        this.ipdAdjust = parentLC.ipdAdjust;
        this.alignmentContext = parentLC.alignmentContext;
        this.lineStartBorderAndPaddingWidth = parentLC.lineStartBorderAndPaddingWidth;
        this.lineEndBorderAndPaddingWidth = parentLC.lineEndBorderAndPaddingWidth;
        copyPendingMarksFrom(parentLC);
        this.pendingKeepWithNext = parentLC.pendingKeepWithNext;
        this.pendingKeepWithPrevious = parentLC.pendingKeepWithPrevious;
        // Copy other fields as necessary.
        this.disableColumnBalancing = parentLC.disableColumnBalancing;
    }

    private LayoutContext(int flags) {
        this.flags = flags;
        this.refIPD = 0;
        stackLimitBP = MinOptMax.ZERO;
        leadingSpace = null;
        trailingSpace = null;
    }

    /** @param source from which pending marks are copied */
    public void copyPendingMarksFrom(LayoutContext source) {
        if (source.pendingAfterMarks != null) {
            this.pendingAfterMarks = new java.util.ArrayList(source.pendingAfterMarks);
        }
        if (source.pendingBeforeMarks != null) {
            this.pendingBeforeMarks = new java.util.ArrayList(source.pendingBeforeMarks);
        }
    }

    /** @param flags to set */
    public void setFlags(int flags) {
        setFlags(flags, true);
    }

    /**
     * @param flags to set or clear
     * @param bSet true to set, false to clear
     */
    public void setFlags(int flags, boolean bSet) {
        if (bSet) {
            this.flags |= flags;
        } else {
            this.flags &= ~flags;
        }
    }

    /** @param flags to clear */
    public void unsetFlags(int flags) {
        setFlags(flags, false);
    }

    /** @return true if new area is set */
    public boolean isStart() {
        return ((this.flags & NEW_AREA) != 0);
    }

    /** @return true if new area is set and leading space is non-null */
    public boolean startsNewArea() {
        return ((this.flags & NEW_AREA) != 0 && leadingSpace != null);
    }

    /** @return true if first area is set */
    public boolean isFirstArea() {
        return ((this.flags & FIRST_AREA) != 0);
    }

    /** @return true if last area is set */
    public boolean isLastArea() {
        return ((this.flags & LAST_AREA) != 0);
    }

    /** @return true if suppress break before is set */
    public boolean suppressBreakBefore() {
        return ((this.flags & SUPPRESS_BREAK_BEFORE) != 0);
    }

    /**
     * Returns the strength of a keep-with-next currently pending.
     * @return the keep-with-next strength
     */
    public Keep getKeepWithNextPending() {
        return this.pendingKeepWithNext;
    }

    /**
     * Returns the strength of a keep-with-previous currently pending.
     * @return the keep-with-previous strength
     */
    public Keep getKeepWithPreviousPending() {
        return this.pendingKeepWithPrevious;
    }

    /**
     * Clears any pending keep-with-next strength.
     */
    public void clearKeepWithNextPending() {
        this.pendingKeepWithNext = Keep.KEEP_AUTO;
    }

    /**
     * Clears any pending keep-with-previous strength.
     */
    public void clearKeepWithPreviousPending() {
        this.pendingKeepWithPrevious = Keep.KEEP_AUTO;
    }

    /**
     * Clears both keep-with-previous and keep-with-next strengths.
     */
    public void clearKeepsPending() {
        clearKeepWithPreviousPending();
        clearKeepWithNextPending();
    }

    /**
     * Updates the currently pending keep-with-next strength.
     * @param keep the new strength to consider
     */
    public void updateKeepWithNextPending(Keep keep) {
        this.pendingKeepWithNext = this.pendingKeepWithNext.compare(keep);
    }

    /**
     * Updates the currently pending keep-with-previous strength.
     * @param keep the new strength to consider
     */
    public void updateKeepWithPreviousPending(Keep keep) {
        this.pendingKeepWithPrevious = this.pendingKeepWithPrevious.compare(keep);
    }

    /**
     * Indicates whether a keep-with-next constraint is pending.
     * @return true if a keep-with-next constraint is pending
     */
    public boolean isKeepWithNextPending() {
        return !getKeepWithNextPending().isAuto();
    }

    /**
     * Indicates whether a keep-with-previous constraint is pending.
     * @return true if a keep-with-previous constraint is pending
     */
    public boolean isKeepWithPreviousPending() {
        return !getKeepWithPreviousPending().isAuto();
    }

    /** @param space leading space */
    public void setLeadingSpace(SpaceSpecifier space) {
        leadingSpace = space;
    }

    /** @return leading space */
    public SpaceSpecifier getLeadingSpace() {
        return leadingSpace;
    }

    /** @return true if resolve leading space is set */
    public boolean resolveLeadingSpace() {
        return ((this.flags & RESOLVE_LEADING_SPACE) != 0);
    }

    /** @param space trailing space */
    public void setTrailingSpace(SpaceSpecifier space) {
        trailingSpace = space;
    }

    /** @return trailing space */
    public SpaceSpecifier getTrailingSpace() {
        return trailingSpace;
    }

    /**
     * Adds a border or padding element to the pending list which will be used to generate
     * the right element list for break possibilities. Conditionality resolution will be done
     * elsewhere.
     * @param element the border, padding or space element
     */
    public void addPendingAfterMark(UnresolvedListElementWithLength element) {
        if (this.pendingAfterMarks == null) {
            this.pendingAfterMarks = new java.util.ArrayList();
        }
        this.pendingAfterMarks.add(element);
    }

    /**
     * @return the pending border and padding elements at the after edge
     * @see #addPendingAfterMark(UnresolvedListElementWithLength)
     */
    public List getPendingAfterMarks() {
        if (this.pendingAfterMarks != null) {
            return Collections.unmodifiableList(this.pendingAfterMarks);
        } else {
            return null;
        }
    }

    /**
     * Clears all pending marks on the LayoutContext.
     */
    public void clearPendingMarks() {
        this.pendingBeforeMarks = null;
        this.pendingAfterMarks = null;
    }

    /**
     * Adds a border or padding element to the pending list which will be used to generate
     * the right element list for break possibilities. Conditionality resolution will be done
     * elsewhere.
     * @param element the border, padding or space element
     */
    public void addPendingBeforeMark(UnresolvedListElementWithLength element) {
        if (this.pendingBeforeMarks == null) {
            this.pendingBeforeMarks = new java.util.ArrayList();
        }
        this.pendingBeforeMarks.add(element);
    }

    /**
     * @return the pending border and padding elements at the before edge
     * @see #addPendingBeforeMark(UnresolvedListElementWithLength)
     */
    public List getPendingBeforeMarks() {
        if (this.pendingBeforeMarks != null) {
            return Collections.unmodifiableList(this.pendingBeforeMarks);
        } else {
            return null;
        }
    }

    /**
     * Sets the stack limit in block-progression-dimension.
     * @param limit the stack limit
     */
    public void setStackLimitBP(MinOptMax limit) {
        stackLimitBP = limit;
    }

    /**
     * Returns the stack limit in block-progression-dimension.
     * @return the stack limit
     */
    public MinOptMax getStackLimitBP() {
        return stackLimitBP;
    }

    /**
     * Sets the inline-progression-dimension of the nearest ancestor reference area.
     * @param ipd of nearest ancestor reference area
     */
    public void setRefIPD(int ipd) {
        refIPD = ipd;
    }

    /**
     * Returns the inline-progression-dimension of the nearest ancestor reference area.
     *
     * @return the inline-progression-dimension of the nearest ancestor reference area
     */
    public int getRefIPD() {
        return refIPD;
    }

    /** @param hyph a hyphenation context */
    public void setHyphContext(HyphContext hyph) {
        hyphContext = hyph;
    }

    /** @return hyphenation context */
    public HyphContext getHyphContext() {
        return hyphContext;
    }

    /**
     * Sets the currently applicable alignment in BP direction.
     * @param alignment one of EN_START, EN_JUSTIFY etc.
     */
    public void setBPAlignment(int alignment) {
        this.bpAlignment = alignment;
    }

    /** @return the currently applicable alignment in BP direction (EN_START, EN_JUSTIFY...) */
    public int getBPAlignment() {
        return this.bpAlignment;
    }

    /** @param adjust space adjustment */
    public void setSpaceAdjust(double adjust) {
        dSpaceAdjust = adjust;
    }

    /** @return space adjustment */
    public double getSpaceAdjust() {
        return dSpaceAdjust;
    }

    /** @param ipdA ipd adjustment */
    public void setIPDAdjust(double ipdA) {
        ipdAdjust = ipdA;
    }

    /** @return ipd adjustment */
    public double getIPDAdjust() {
        return ipdAdjust;
    }

    /** @param alignmentContext alignment context */
    public void setAlignmentContext(AlignmentContext alignmentContext) {
        this.alignmentContext = alignmentContext;
    }

    /** @return alignment context */
    public AlignmentContext getAlignmentContext() {
        return this.alignmentContext;
    }

    /**
     * Reset alignment context.
     */
    public void resetAlignmentContext() {
        if (this.alignmentContext != null) {
            this.alignmentContext = this.alignmentContext.getParentAlignmentContext();
        }
    }

    /**
     * Get the width to be reserved for border and padding at the start of the line.
     * @return the width to be reserved
     */
    public int getLineStartBorderAndPaddingWidth() {
        return lineStartBorderAndPaddingWidth;
    }

    /**
     * Set the width to be reserved for border and padding at the start of the line.
     * @param lineStartBorderAndPaddingWidth the width to be reserved
     */
    public void setLineStartBorderAndPaddingWidth(int lineStartBorderAndPaddingWidth) {
        this.lineStartBorderAndPaddingWidth = lineStartBorderAndPaddingWidth;
    }

    /**
     * Get the width to be reserved for border and padding at the end of the line.
     * @return the width to be reserved
     */
    public int getLineEndBorderAndPaddingWidth() {
        return lineEndBorderAndPaddingWidth;
    }

    /**
     * Set the width to be reserved for border and padding at the end of the line.
     * @param lineEndBorderAndPaddingWidth the width to be reserved
     */
    public void setLineEndBorderAndPaddingWidth(int lineEndBorderAndPaddingWidth) {
        this.lineEndBorderAndPaddingWidth = lineEndBorderAndPaddingWidth;
    }

    /**
     * @return one of: {@link Constants#NOT_SET}, {@link Constants#EN_NONE}
     *                  {@link Constants#EN_ALL}
     */
    public int getNextSpan() {
        return nextSpan;
    }

    /**
     * @return one of: {@link Constants#NOT_SET}, {@link Constants#EN_NONE}
     *                  {@link Constants#EN_ALL}
     */
    public int getCurrentSpan() {
        return (currentSpan == Constants.NOT_SET)
                ? Constants.EN_NONE : currentSpan;
    }

    /**
     * Used to signal the PSLM that the element list ends early because of a span change in
     * multi-column layout.
     * @param span the new span value (legal values: NOT_SET, EN_NONE, EN_ALL)
     */
    public void signalSpanChange(int span) {
        switch (span) {
        case Constants.NOT_SET:
        case Constants.EN_NONE:
        case Constants.EN_ALL:
            this.currentSpan = this.nextSpan;
            this.nextSpan = span;
            break;
        default:
            assert false;
            throw new IllegalArgumentException("Illegal value on signalSpanChange() for span: "
                    + span);
        }
    }

    /**
     * Get the writing mode of the relevant reference area.
     * @return the applicable writing mode
     */
    public WritingMode getWritingMode() {
        return writingMode;
    }

    /**
     * Set the writing mode.
     * @param writingMode the writing mode
     */
    public void setWritingMode(WritingMode writingMode) {
        this.writingMode = writingMode;
    }

    /**
     * Get the current amount of space before / start
     * @return the space before / start amount
     */
    public int getSpaceBefore() {
        return spaceBefore;
    }

    /**
     * Set the amount of space before / start
     * @param spaceBefore the amount of space before / start
     */
    public void setSpaceBefore(int spaceBefore) {
        this.spaceBefore = spaceBefore;
    }

    /**
     * Get the current amount of space after / end
     * @return the space after / end amount
     */
    public int getSpaceAfter() {
        return spaceAfter;
    }

    /**
     * Set the amount of space after / end
     * @param spaceAfter the amount of space after / end
     */
    public void setSpaceAfter(int spaceAfter) {
        this.spaceAfter = spaceAfter;
    }

    /**
     * Returns the value of the break before the element whose
     * {@link LayoutManager#getNextKnuthElements(LayoutContext, int)} method has just been
     * called.
     *
     * @return one of {@link Constants#EN_AUTO}, {@link Constants#EN_COLUMN},
     * {@link Constants#EN_PAGE}, {@link Constants#EN_EVEN_PAGE}, or
     * {@link Constants#EN_ODD_PAGE}
     */
    public int getBreakBefore() {
        return breakBefore;
    }

    /**
     * Sets the value of the break before the current element.
     *
     * @param breakBefore the value of the break-before
     * @see #getBreakBefore()
     */
    public void setBreakBefore(int breakBefore) {
        this.breakBefore = breakBefore;
    }

    /**
     * Returns the value of the break after the element whose
     * {@link LayoutManager#getNextKnuthElements(LayoutContext, int)} method has just been
     * called.
     *
     * @return one of {@link Constants#EN_AUTO}, {@link Constants#EN_COLUMN},
     * {@link Constants#EN_PAGE}, {@link Constants#EN_EVEN_PAGE}, or
     * {@link Constants#EN_ODD_PAGE}
     */
    public int getBreakAfter() {
        return breakAfter;
    }


    /**
     * Sets the value of the break after the current element.
     *
     * @param breakAfter the value of the break-after
     * @see #getBreakAfter()
     */
    public void setBreakAfter(int breakAfter) {
        this.breakAfter = breakAfter;
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return "Layout Context:"
                + "\nStack Limit BPD: \t"
                + (getStackLimitBP() == null ? "null" : getStackLimitBP().toString())
                + "\nTrailing Space: \t"
                + (getTrailingSpace() == null ? "null" : getTrailingSpace().toString())
                + "\nLeading Space: \t"
                + (getLeadingSpace() == null ? "null" : getLeadingSpace().toString())
                + "\nReference IPD: \t" + getRefIPD()
                + "\nSpace Adjust: \t" + getSpaceAdjust()
                + "\nIPD Adjust: \t" + getIPDAdjust()
                + "\nResolve Leading Space: \t" + resolveLeadingSpace()
                + "\nSuppress Break Before: \t" + suppressBreakBefore()
                + "\nIs First Area: \t" + isFirstArea()
                + "\nStarts New Area: \t" + startsNewArea()
                + "\nIs Last Area: \t" + isLastArea()
                + "\nKeeps: \t[keep-with-next=" + getKeepWithNextPending()
                + "][keep-with-previous=" + getKeepWithPreviousPending() + "] pending"
                + "\nBreaks: \tforced [" + (breakBefore != Constants.EN_AUTO ? "break-before" : "") + "]["
                + (breakAfter != Constants.EN_AUTO ? "break-after" : "") + "]";
    }

    /**
     * Returns whether the column balancer should be disabled before a spanning block
     *
     * @return one of {@link Constants#EN_TRUE}, {@link Constants#EN_FALSE}
     */
    public int getDisableColumnBalancing() {
        return disableColumnBalancing;
    }

    /**
     * Sets whether the column balancer should be disabled before a spanning block
     *
     * @param disableColumnBalancing the value of the fox:disable-column-balancing property
     * @see #getDisableColumnBalancing()
     */
    public void setDisableColumnBalancing(int disableColumnBalancing) {
        this.disableColumnBalancing = disableColumnBalancing;
    }

    public boolean treatAsArtifact() {
        return (flags & TREAT_AS_ARTIFACT) != 0;
    }

    public void setTreatAsArtifact(boolean treatAsArtifact) {
        setFlags(TREAT_AS_ARTIFACT, treatAsArtifact);
    }
}

