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

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

import org.apache.fop.area.Trait;
import org.apache.fop.area.inline.FilledArea;
import org.apache.fop.area.inline.InlineArea;
import org.apache.fop.area.inline.Space;
import org.apache.fop.area.inline.TextArea;
import org.apache.fop.fo.FObj;
import org.apache.fop.fo.flow.Leader;
import org.apache.fop.fonts.Font;
import org.apache.fop.fonts.FontInfo;
import org.apache.fop.fonts.FontTriplet;
import org.apache.fop.layoutmgr.InlineKnuthSequence;
import org.apache.fop.layoutmgr.KnuthElement;
import org.apache.fop.layoutmgr.KnuthGlue;
import org.apache.fop.layoutmgr.KnuthPenalty;
import org.apache.fop.layoutmgr.KnuthPossPosIter;
import org.apache.fop.layoutmgr.KnuthSequence;
import org.apache.fop.layoutmgr.LayoutContext;
import org.apache.fop.layoutmgr.LeafPosition;
import org.apache.fop.layoutmgr.ListElement;
import org.apache.fop.layoutmgr.Position;
import org.apache.fop.layoutmgr.PositionIterator;
import org.apache.fop.layoutmgr.TraitSetter;
import org.apache.fop.traits.MinOptMax;

/**
 * LayoutManager for the fo:leader formatting object
 */
public class LeaderLayoutManager extends LeafNodeLayoutManager {
    private Leader fobj;
    private Font font;

    private List<ListElement> contentList;
    private ContentLayoutManager clm;

    private int contentAreaIPD;

    /**
     * Constructor
     *
     * @param node the formatting object that creates this area
     */
    public LeaderLayoutManager(Leader node) {
        super(node);
        fobj = node;
    }

    /** {@inheritDoc} */
    public void initialize() {
        FontInfo fi = fobj.getFOEventHandler().getFontInfo();
        FontTriplet[] fontkeys = fobj.getCommonFont().getFontState(fi);
        font = fi.getFontInstance(fontkeys[0], fobj.getCommonFont().fontSize.getValue(this));
        // the property leader-alignment does not affect vertical positioning
        // (see section 7.21.1 in the XSL Recommendation)
        // setAlignment(node.getLeaderAlignment());
        setCommonBorderPaddingBackground(fobj.getCommonBorderPaddingBackground());
    }

    /**
     * Return the inline area for this leader.
     * @param context the layout context
     * @return the inline area
     */
    public InlineArea get(LayoutContext context) {
        return getLeaderInlineArea(context);
    }

    /**
     * Return the allocated IPD for this area.
     * @param refIPD the IPD of the reference area
     * @return the allocated IPD
     */
    protected MinOptMax getAllocationIPD(int refIPD) {
        return getLeaderAllocIPD(refIPD);
    }

    private MinOptMax getLeaderAllocIPD(int ipd) {
        // length of the leader
        int borderPaddingWidth = 0;
        if (commonBorderPaddingBackground != null) {
            borderPaddingWidth = commonBorderPaddingBackground.getIPPaddingAndBorder(false, this);
        }
        setContentAreaIPD(ipd - borderPaddingWidth);
        int opt = fobj.getLeaderLength().getOptimum(this).getLength().getValue(this)
                    - borderPaddingWidth;
        int min = fobj.getLeaderLength().getMinimum(this).getLength().getValue(this)
                    - borderPaddingWidth;
        int max = fobj.getLeaderLength().getMaximum(this).getLength().getValue(this)
                    - borderPaddingWidth;
        return MinOptMax.getInstance(min, opt, max);
    }

    private InlineArea getLeaderInlineArea(LayoutContext context) {
        InlineArea leaderArea = null;
        int level = fobj.getBidiLevel();
        if (fobj.getLeaderPattern() == EN_RULE) {
            if (fobj.getRuleStyle() != EN_NONE) {
                org.apache.fop.area.inline.Leader leader
                    = new org.apache.fop.area.inline.Leader();
                leader.setRuleStyle(fobj.getRuleStyle());
                leader.setRuleThickness(fobj.getRuleThickness().getValue(this));
                leaderArea = leader;
            } else {
                leaderArea = new Space();
                if (level >= 0) {
                    leaderArea.setBidiLevel(level);
                }
            }
            leaderArea.setBPD(fobj.getRuleThickness().getValue(this));
            leaderArea.addTrait(Trait.COLOR, fobj.getColor());
            if (level >= 0) {
                leaderArea.setBidiLevel(level);
            }
        } else if (fobj.getLeaderPattern() == EN_SPACE) {
            leaderArea = new Space();
            leaderArea.setBPD(fobj.getRuleThickness().getValue(this));
            if (level >= 0) {
                leaderArea.setBidiLevel(level);
            }
        } else if (fobj.getLeaderPattern() == EN_DOTS) {
            TextArea t = new TextArea();
            char dot = '.'; // userAgent.getLeaderDotCharacter();
            int width = font.getCharWidth(dot);
            int[] levels = (level < 0) ? null : new int[] {level};
            t.addWord("" + dot, width, null, levels, null, 0);
            t.setIPD(width);
            t.setBPD(width);
            t.setBaselineOffset(width);
            TraitSetter.addFontTraits(t, font);
            t.addTrait(Trait.COLOR, fobj.getColor());
            Space spacer = null;
            int widthLeaderPattern = fobj.getLeaderPatternWidth().getValue(this);
            if (widthLeaderPattern > width) {
                spacer = new Space();
                spacer.setIPD(widthLeaderPattern - width);
                if (level >= 0) {
                    spacer.setBidiLevel(level);
                }
                width = widthLeaderPattern;
            }
            FilledArea fa = new FilledArea();
            fa.setUnitWidth(width);
            fa.addChildArea(t);
            if (spacer != null) {
                fa.addChildArea(spacer);
            }
            fa.setBPD(t.getBPD());
            leaderArea = fa;
        } else if (fobj.getLeaderPattern() == EN_USECONTENT) {
            if (fobj.getChildNodes() == null) {
                InlineLevelEventProducer eventProducer = InlineLevelEventProducer.Provider.get(
                        getFObj().getUserAgent().getEventBroadcaster());
                eventProducer.leaderWithoutContent(this, getFObj().getLocator());
                return null;
            }

            // child FOs are assigned to the InlineStackingLM
            fobjIter = null;

            // get breaks then add areas to FilledArea
            FilledArea fa = new FilledArea();

            clm = new ContentLayoutManager(fa, this);
            addChildLM(clm);

            InlineLayoutManager lm;
            lm = new InlineLayoutManager(fobj);
            clm.addChildLM(lm);
            lm.initialize();

            LayoutContext childContext = LayoutContext.newInstance();
            childContext.setAlignmentContext(context.getAlignmentContext());
            contentList = clm.getNextKnuthElements(childContext, 0);
            int width = clm.getStackingSize();
            if (width != 0) {
                Space spacer = null;
                if (fobj.getLeaderPatternWidth().getValue(this) > width) {
                    spacer = new Space();
                    spacer.setIPD(fobj.getLeaderPatternWidth().getValue(this) - width);
                    if (level >= 0) {
                        spacer.setBidiLevel(level);
                    }
                    width = fobj.getLeaderPatternWidth().getValue(this);
                }
                fa.setUnitWidth(width);
                if (spacer != null) {
                    fa.addChildArea(spacer);
                }
                leaderArea = fa;
            } else {
                //Content collapsed to nothing, so use a space
                leaderArea = new Space();
                leaderArea.setBPD(fobj.getRuleThickness().getValue(this));
                leaderArea.setBidiLevel(fobj.getBidiLevelRecursive());
            }
        }

        assert leaderArea != null;
        leaderArea.setChangeBarList(getChangeBarList());

        TraitSetter.setProducerID(leaderArea, fobj.getId());
        return leaderArea;
     }

    /** {@inheritDoc} */
    public void addAreas(PositionIterator posIter, LayoutContext context) {
        if (fobj.getLeaderPattern() != EN_USECONTENT) {
            // use LeafNodeLayoutManager.addAreas()
            super.addAreas(posIter, context);
        } else {
            addId();

            widthAdjustArea(curArea, context);

            if (commonBorderPaddingBackground != null) {
                // Add border and padding to area
                TraitSetter.setBorderPaddingTraits(curArea,
                                                   commonBorderPaddingBackground,
                                                   false, false, this);
                TraitSetter.addBackground(curArea, commonBorderPaddingBackground, this);
            }

            // add content areas
            KnuthPossPosIter contentIter = new KnuthPossPosIter(contentList, 0, contentList.size());
            clm.addAreas(contentIter, context);

            parentLayoutManager.addChildArea(curArea);

            while (posIter.hasNext()) {
                posIter.next();
            }
        }
    }

    /** {@inheritDoc} */
    public List<KnuthSequence> getNextKnuthElements(LayoutContext context, int alignment) {
        MinOptMax ipd;
        curArea = get(context);
        KnuthSequence seq = new InlineKnuthSequence();

        if (curArea == null) {
            setFinished(true);
            return null;
        }

        alignmentContext = new AlignmentContext(curArea.getBPD()
                                    , fobj.getAlignmentAdjust()
                                    , fobj.getAlignmentBaseline()
                                    , fobj.getBaselineShift()
                                    , fobj.getDominantBaseline()
                                    , context.getAlignmentContext());

        ipd = getAllocationIPD(context.getRefIPD());
        if (fobj.getLeaderPattern() == EN_USECONTENT && curArea instanceof FilledArea) {
            // If we have user supplied content make it fit if we can
            int unitWidth = ((FilledArea) curArea).getUnitWidth();
            if (ipd.getOpt() < unitWidth && unitWidth <= ipd.getMax()) {
                ipd = MinOptMax.getInstance(ipd.getMin(), unitWidth, ipd.getMax());
            }
        }

        // create the AreaInfo object to store the computed values
        areaInfo = new AreaInfo((short) 0, ipd, false, context.getAlignmentContext());
        curArea.setAdjustingInfo(ipd.getStretch(), ipd.getShrink(), 0);

        addKnuthElementsForBorderPaddingStart(seq);

        // node is a fo:Leader
        seq.add(new KnuthInlineBox(0, alignmentContext, new LeafPosition(this, -1), true));
        seq.add(new KnuthPenalty(0, KnuthElement.INFINITE, false,
                new LeafPosition(this, -1), true));
        if (alignment == EN_JUSTIFY || alignment == 0) {
            seq.add(new KnuthGlue(areaInfo.ipdArea, new LeafPosition(this, 0), false));
        } else {
            seq.add(new KnuthGlue(areaInfo.ipdArea.getOpt(), 0, 0,
                    new LeafPosition(this, 0), false));
        }
        seq.add(new KnuthInlineBox(0, alignmentContext, new LeafPosition(this, -1), true));

        addKnuthElementsForBorderPaddingEnd(seq);

        setFinished(true);
        return Collections.singletonList(seq);
    }

    /** {@inheritDoc} */
    public void hyphenate(Position pos, HyphContext hc) {
        // use the AbstractLayoutManager.hyphenate() null implementation
        super.hyphenate(pos, hc);
    }

    /** {@inheritDoc} */
    public boolean applyChanges(List<ListElement> oldList) {
        setFinished(false);
        return false;
    }

    /** {@inheritDoc} */
    public List<ListElement> getChangedKnuthElements(List<ListElement> oldList, int alignment) {
        if (isFinished()) {
            return null;
        }

        List<ListElement> returnList = new LinkedList<>();

        addKnuthElementsForBorderPaddingStart(returnList);

        returnList.add(new KnuthInlineBox(0, areaInfo.alignmentContext,
                                    new LeafPosition(this, -1), true));
        returnList.add(new KnuthPenalty(0, KnuthElement.INFINITE, false,
                                        new LeafPosition(this, -1), true));
        if (alignment == EN_JUSTIFY || alignment == 0) {
            returnList.add(new KnuthGlue(areaInfo.ipdArea, new LeafPosition(this, 0), false));
        } else {
            returnList.add(new KnuthGlue(areaInfo.ipdArea.getOpt(), 0, 0,
                    new LeafPosition(this, 0), false));
        }
        returnList.add(new KnuthInlineBox(0, areaInfo.alignmentContext,
                                    new LeafPosition(this, -1), true));

        addKnuthElementsForBorderPaddingEnd(returnList);

        setFinished(true);
        return returnList;
    }

    /** {@inheritDoc} */
    public int getBaseLength(int lengthBase, FObj fobj) {
        return getParent().getBaseLength(lengthBase, getParent().getFObj());
    }

    /**
     * Returns the IPD of the content area
     * @return the IPD of the content area
     */
    public int getContentAreaIPD() {
        return contentAreaIPD;
    }

    private void setContentAreaIPD(int contentAreaIPD) {
        this.contentAreaIPD = contentAreaIPD;
    }

    /** {@inheritDoc} */
    public void reset() {
        childLMs.clear();
        super.reset();
    }

}
