/*
 * 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.area.inline;

import java.io.Serializable;
import java.util.List;

import org.apache.fop.area.Area;
import org.apache.fop.area.LineArea;
import org.apache.fop.area.Trait;
import org.apache.fop.complexscripts.bidi.InlineRun;

/**
 * Inline Area
 * This area is for all inline areas that can be placed
 * in a line area.
 */
public class InlineArea extends Area {

    private static final long serialVersionUID = -8940066479810170980L;

    /**
     * this class stores information about potential adjustments
     * that can be used in order to re-compute adjustments when a
     * page-number or a page-number-citation is resolved
     */
    // @SuppressFBWarnings("SE_INNER_CLASS")
    protected class InlineAdjustingInfo implements Serializable {

        private static final long serialVersionUID = -5601387735459712149L;

        /** stretch of the inline area */
        protected int availableStretch;
        /** shrink of the inline area */
        protected int availableShrink;
        /** total adjustment (= ipd - width of fixed elements) */
        protected int adjustment;

        /**
         * Constructor
         *
         * @param stretch the available space for stretching
         * @param shrink the available space for shrinking
         * @param adj space adjustment type
         */
        protected InlineAdjustingInfo(int stretch, int shrink, int adj) {
            availableStretch = stretch;
            availableShrink = shrink;
            adjustment = adj;
        }

        /**
         * Apply the variation factor
         *
         * @param variationFactor the factor by which the adjustment is to be changed
         * @return the IPD increase
         */
        protected int applyVariationFactor(double variationFactor) {
            int oldAdjustment = adjustment;
            adjustment *= variationFactor;
            return adjustment - oldAdjustment;
        }
    }

    /**
     * offset position from before edge of parent area
     */
    protected int blockProgressionOffset;

    /**
     * parent area
     * it is needed in order to recompute adjust ratio and indents
     * when a page-number or a page-number-citation is resolved
     */
    private Area parentArea;

    /**
     * ipd variation of child areas: if this area has not already
     * been added and cannot notify its parent area, store the variation
     * and wait for the parent area to be set
     */
    private int storedIPDVariation;

    /**
     * The adjustment information object
     */
    protected InlineAdjustingInfo adjustingInfo;

    /**
     * Default constructor for inline area.
     */
    public InlineArea() {
        this (0, -1);
    }

    /**
     * Instantiate inline area.
     * @param blockProgressionOffset a block progression offset or zero
     * @param bidiLevel a resolved bidi level or -1
     */
    protected InlineArea(int blockProgressionOffset, int bidiLevel) {
        this.blockProgressionOffset = blockProgressionOffset;
        setBidiLevel(bidiLevel);
    }

    /**
     * @return the adjustment information object
     */
    public InlineAdjustingInfo getAdjustingInfo() {
        return adjustingInfo;
    }

    /**
     * Create a new adjustment information object
     * @param stretch the available space for stretching
     * @param shrink the available space for shrinking
     * @param adjustment space adjustment type
     */
    public void setAdjustingInfo(int stretch, int shrink, int adjustment) {
        adjustingInfo = new InlineAdjustingInfo(stretch, shrink, adjustment);
    }

    /**
     * Sets the adjustment information from an existing object
     * @param adjustingInfo the existing adjustment object
     */
    public void setAdjustingInfo(InlineAdjustingInfo adjustingInfo) {
        this.adjustingInfo = adjustingInfo;
    }

    /**
     * Modify the adjustment value in the adjustment information object
     * @param adjustment the new adjustment value
     */
    public void setAdjustment(int adjustment) {
        if (adjustingInfo != null) {
            adjustingInfo.adjustment = adjustment;
        }
    }

    /**
     * Increase the inline progression dimensions of this area.
     * This is used for inline parent areas that contain mulitple child areas.
     *
     * @param ipd the inline progression to increase by
     */
    public void increaseIPD(int ipd) {
        this.ipd += ipd;
    }

    /**
     * Set the block progression offset of this inline area.
     * This is used to set the offset of the inline area
     * which is relative to the before edge of the parent area.
     *
     * @param blockProgressionOffset the offset
     */
    public void setBlockProgressionOffset(int blockProgressionOffset) {
        this.blockProgressionOffset = blockProgressionOffset;
    }

    /**
     * Get the block progression offset of this inline area.
     * This returns the offset of the inline area
     * relative to the before edge of the parent area.
     *
     * @return the blockProgressionOffset
     */
    public int getBlockProgressionOffset() {
        return blockProgressionOffset;
    }

    /**
     * @param parentArea The parentArea to set.
     */
    public void setParentArea(Area parentArea) {
        this.parentArea = parentArea;
    }

    /**
     * @return Returns the parentArea.
     */
    public Area getParentArea() {
        return parentArea;
    }

    /**
     * Set the parent for the child area.
     *
     * {@inheritDoc}
     */
    @Override
    public void addChildArea(Area childArea) {
        super.addChildArea(childArea);
        if (childArea instanceof InlineArea) {
            ((InlineArea) childArea).setParentArea(this);
        }
    }

    /** @return true if the inline area is underlined. */
    public boolean hasUnderline() {
        return getTraitAsBoolean(Trait.UNDERLINE);
    }

    /** @return true if the inline area is overlined. */
    public boolean hasOverline() {
        return getTraitAsBoolean(Trait.OVERLINE);
    }

    /** @return true if the inline area has a line through. */
    public boolean hasLineThrough() {
        return getTraitAsBoolean(Trait.LINETHROUGH);
    }

    /** @return true if the inline area is blinking. */
    public boolean isBlinking() {
        return getTraitAsBoolean(Trait.BLINK);
    }

    /**
     * recursively apply the variation factor to all descendant areas
     * @param variationFactor the variation factor that must be applied to adjustments
     * @param lineStretch     the total stretch of the line
     * @param lineShrink      the total shrink of the line
     * @return true if there is an UnresolvedArea descendant
     */
    public boolean applyVariationFactor(double variationFactor,
                                        int lineStretch, int lineShrink) {
        // default behaviour: update the IPD and return false
        if (adjustingInfo != null) {
            setIPD(getIPD() + adjustingInfo.applyVariationFactor(variationFactor));
        }
        return false;
    }

    /**
     * Apply IPD variation.
     * @param ipdVariation the variation
     */
    public void handleIPDVariation(int ipdVariation) {
        if (log.isTraceEnabled()) {
            log.trace("Handling IPD variation for " + getClass().getSimpleName()
                    + ": increase by " + ipdVariation + " mpt.");
        }

        if (ipdVariation != 0) {
            increaseIPD(ipdVariation);
            notifyIPDVariation(ipdVariation);
        }
    }

    /**
     * notify the parent area about the ipd variation of this area
     * or of a descendant area
     * @param ipdVariation the difference between new and old ipd
     */
    protected void notifyIPDVariation(int ipdVariation) {
        Area parentArea = getParentArea();
        if (parentArea instanceof InlineArea) {
            ((InlineArea) parentArea).handleIPDVariation(ipdVariation);
        } else if (parentArea instanceof LineArea) {
            ((LineArea) parentArea).handleIPDVariation(ipdVariation);
        } else if (parentArea == null) {
            // parent area not yet set: store the variations
            storedIPDVariation += ipdVariation;
        }
    }

    /**
     * Returns the offset that this area would have if its offset and size were taking
     * children areas into account. The bpd of an inline area is taken from its nominal
     * font and doesn't depend on the bpds of its children elements. However, in the case
     * of a basic-link element we want the active area to cover all of the children
     * elements.
     *
     * @return the offset that this area would have if the before-edge of its
     * content-rectangle were coinciding with the beforest before-edge of its
     * children allocation-rectangles.
     * @see #getVirtualBPD()
     * @see BasicLinkArea
     */
    int getVirtualOffset() {
        return getBlockProgressionOffset();
    }

    /**
     * Returns the block-progression-dimension that this area would have if it were taking
     * its children elements into account. See {@linkplain #getVirtualOffset()}.
     *
     * @return the bpd
     */
    int getVirtualBPD() {
        return getBPD();
    }

    /**
     * Collection bidi inline runs.
     * @param runs current list of inline runs
     * @return modified list of inline runs, having appended new run
     */
    public List collectInlineRuns(List runs) {
        assert runs != null;
        runs.add(new InlineRun(this, new int[] {getBidiLevel()}));
        return runs;
    }

    /**
     * Determine if inline area IA is an ancestor inline area or same as this area.
     * @param ia inline area to test
     * @return true if specified inline area is an ancestor or same as this area
     */
    public boolean isAncestorOrSelf(InlineArea ia) {
        return (ia == this) || isAncestor(ia);
    }

    /**
     * Determine if inline area IA is an ancestor inline area of this area.
     * @param ia inline area to test
     * @return true if specified inline area is an ancestor of this area
     */
    public boolean isAncestor(InlineArea ia) {
        for (Area p = getParentArea(); p != null;) {
            if (p == ia) {
                return true;
            } else if (p instanceof InlineArea) {
                p = ((InlineArea) p).getParentArea();
            } else {
                p = null;
            }
        }
        return false;
    }

}
