/*
 * 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: PageSequence.java 1835810 2018-07-13 10:29:57Z ssteiner $ */

package org.apache.fop.fo.pagination;

import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Stack;

import org.xml.sax.Locator;

import org.apache.fop.apps.FOPException;
import org.apache.fop.complexscripts.bidi.DelimitedTextRange;
import org.apache.fop.datatypes.Numeric;
import org.apache.fop.fo.FONode;
import org.apache.fop.fo.PropertyList;
import org.apache.fop.fo.ValidationException;
import org.apache.fop.fo.flow.ChangeBar;
import org.apache.fop.fo.properties.CommonHyphenation;
import org.apache.fop.traits.Direction;
import org.apache.fop.traits.WritingMode;
import org.apache.fop.traits.WritingModeTraits;
import org.apache.fop.traits.WritingModeTraitsGetter;

/**
 * Class modelling the <a href="http://www.w3.org/TR/xsl/#fo_page-sequence">
 * <code>fo:page-sequence</code></a> object.
 */
public class PageSequence extends AbstractPageSequence implements WritingModeTraitsGetter {

    private String masterReference;
    private Numeric referenceOrientation;
    private WritingModeTraits writingModeTraits;

    private Locale locale;

    // There doesn't seem to be anything in the spec requiring flows
    // to be in the order given, only that they map to the regions
    // defined in the page sequence, so all we need is this one hashmap
    // the set of flows includes StaticContent flows also

    /** Map of flows to their flow name (flow-name, Flow) */
    private Map<String, FONode> flowMap;

    /**
     * The currentSimplePageMaster is either the page master for the
     * whole page sequence if master-reference refers to a simple-page-master,
     * or the simple page master produced by the page sequence master otherwise.
     * The pageSequenceMaster is null if master-reference refers to a
     * simple-page-master.
     */
    private SimplePageMaster simplePageMaster;
    private PageSequenceMaster pageSequenceMaster;

    /**
     * The fo:title object for this page-sequence.
     */
    private Title titleFO;

    /**
     * The fo:flow object for this page-sequence.
     */
    private Flow mainFlow;

    /**
     * Active change bars
     */
    private final List<ChangeBar> changeBarList = new LinkedList<ChangeBar>();

    /**
     * Create a PageSequence instance that is a child of the
     * given {@link FONode}.
     *
     * @param parent the parent {@link FONode}
     */
    public PageSequence(FONode parent) {
        super(parent);
    }

    /** {@inheritDoc} */
    public void bind(PropertyList pList) throws FOPException {
        super.bind(pList);
        String country = pList.get(PR_COUNTRY).getString();
        String language = pList.get(PR_LANGUAGE).getString();
        locale = CommonHyphenation.toLocale(language, country);
        masterReference = pList.get(PR_MASTER_REFERENCE).getString();
        referenceOrientation = pList.get(PR_REFERENCE_ORIENTATION).getNumeric();
        writingModeTraits = new WritingModeTraits(
            WritingMode.valueOf(pList.get(PR_WRITING_MODE).getEnum()),
            pList.getExplicit(PR_WRITING_MODE) != null);
        if (masterReference == null || masterReference.equals("")) {
            missingPropertyError("master-reference");
        }
    }

    /** {@inheritDoc} */
    public void startOfNode() throws FOPException {
        super.startOfNode();
        flowMap = new java.util.HashMap<String, FONode>();

        this.simplePageMaster
            = getRoot().getLayoutMasterSet().getSimplePageMaster(masterReference);
        if (simplePageMaster == null) {
            this.pageSequenceMaster
                = getRoot().getLayoutMasterSet().getPageSequenceMaster(masterReference);
            if (pageSequenceMaster == null) {
                getFOValidationEventProducer().masterNotFound(this, getName(),
                        masterReference, getLocator());
            }
        }
        getRoot().addPageSequence(this);
        getFOEventHandler().startPageSequence(this);
    }

    /** {@inheritDoc} */
    public void endOfNode() throws FOPException {
        if (mainFlow == null) {
           missingChildElementError("(title?,static-content*,flow)");
        }

        getFOEventHandler().endPageSequence(this);
    }

    /**
     * {@inheritDoc}
        XSL Content Model: (title?,static-content*,flow)
     */
    protected void validateChildNode(Locator loc, String nsURI, String localName)
                throws ValidationException {
        if (FO_URI.equals(nsURI)) {
            if ("title".equals(localName)) {
                if (titleFO != null) {
                    tooManyNodesError(loc, "fo:title");
                } else if (!flowMap.isEmpty()) {
                    nodesOutOfOrderError(loc, "fo:title", "fo:static-content");
                } else if (mainFlow != null) {
                    nodesOutOfOrderError(loc, "fo:title", "fo:flow");
                }
            } else if ("static-content".equals(localName)) {
                if (mainFlow != null) {
                    nodesOutOfOrderError(loc, "fo:static-content", "fo:flow");
                }
            } else if ("flow".equals(localName)) {
                if (mainFlow != null) {
                    tooManyNodesError(loc, "fo:flow");
                }
            } else {
                invalidChildError(loc, nsURI, localName);
            }
        }
    }

    /**
     * {@inheritDoc}
     * TODO see if addChildNode() should also be called for fo's other than
     *  fo:flow.
     */
    public void addChildNode(FONode child) throws FOPException {
        int childId = child.getNameId();

        switch (childId) {
        case FO_TITLE:
            this.titleFO = (Title)child;
            break;
        case FO_FLOW:
            this.mainFlow = (Flow)child;
            addFlow(mainFlow);
            break;
        case FO_STATIC_CONTENT:
            addFlow((StaticContent)child);
            flowMap.put(((Flow)child).getFlowName(), (Flow)child);
            break;
        default:
            super.addChildNode(child);
        }
    }

    /**
     * Add a flow or static content, mapped by its flow-name.
     * The flow-name is used to associate the flow with a region on a page,
     * based on the region-names given to the regions in the page-master
     * used to generate that page.
     * @param flow  the {@link Flow} instance to be added
     * @throws org.apache.fop.fo.ValidationException if the fo:flow maps
     * to an invalid page-region
     */
    private void addFlow(Flow flow) throws ValidationException {
        String flowName = flow.getFlowName();

        if (hasFlowName(flowName)) {
            getFOValidationEventProducer().duplicateFlowNameInPageSequence(this, flow.getName(),
                    flowName, flow.getLocator());
        }

        if (!hasRegion(flowName) && !flowName.equals("xsl-before-float-separator")
                && !flowName.equals("xsl-footnote-separator")) {
            getFOValidationEventProducer().flowNameNotMapped(this, flow.getName(),
                    flowName, flow.getLocator());
        }
    }

    private boolean hasRegion(String flowName) {
        LayoutMasterSet set = getRoot().getLayoutMasterSet();
        PageSequenceMaster psm = set.getPageSequenceMaster(masterReference);
        return (psm != null) ? psm.getLayoutMasterSet().regionNameExists(flowName)
                : set.getSimplePageMaster(masterReference).regionNameExists(flowName);
    }

    /**
     * Get the static content FO node from the flow map.
     * This gets the static content flow for the given flow name.
     *
     * @param name the flow name to find
     * @return the static content FO node
     */
    public StaticContent getStaticContent(String name) {
        return (StaticContent) flowMap.get(name);
    }

    /**
     * Accessor method for the fo:title associated with this fo:page-sequence
     * @return titleFO for this object
     */
    public Title getTitleFO() {
        return titleFO;
    }

    /**
     * Public accessor for getting the MainFlow to which this PageSequence is
     * attached.
     * @return the MainFlow object to which this PageSequence is attached.
     */
    public Flow getMainFlow() {
        return mainFlow;
    }

    /**
     * Determine if this PageSequence already has a flow with the given flow-name
     * Used for validation of incoming fo:flow or fo:static-content objects
     * @param flowName The flow-name to search for
     * @return true if flow-name already defined within this page sequence,
     *    false otherwise
     */
    public boolean hasFlowName(String flowName) {
        return flowMap.containsKey(flowName);
    }

    /** @return the flow map for this page-sequence */
    public Map<String, FONode> getFlowMap() {
        return this.flowMap;
    }

    /**
     * Public accessor for determining the next page master to use within this page sequence.
     * @param page the page number of the page to be created
     * @param isFirstPage indicator whether this page is the first page of the
     *      page sequence
     * @param isLastPage indicator whether this page is the last page of the
     *      page sequence
     * @param isBlank indicator whether the page will be blank
     * @return the SimplePageMaster to use for this page
     * @throws PageProductionException if there's a problem determining the page master
     */
    public SimplePageMaster getNextSimplePageMaster(
        int page, boolean isFirstPage, boolean isLastPage, boolean isBlank)
        throws PageProductionException {

        if (pageSequenceMaster == null) {
            return simplePageMaster;
        }
        boolean isOddPage = ((page % 2) != 0);
        if (log.isDebugEnabled()) {
            log.debug("getNextSimplePageMaster(page=" + page
                    + " isOdd=" + isOddPage
                    + " isFirst=" + isFirstPage
                    + " isLast=" + isLastPage
                    + " isBlank=" + isBlank + ")");
        }
        return pageSequenceMaster.getNextSimplePageMaster(isOddPage,
            isFirstPage, isLastPage, isBlank, getMainFlow().getFlowName());
    }

    /**
     * Used to set the "cursor position" for the page masters to the previous item.
     * @return true if there is a previous item, false if the current one was the first one.
     */
    public boolean goToPreviousSimplePageMaster() {
        return pageSequenceMaster == null || pageSequenceMaster.goToPreviousSimplePageMaster();
    }

    /** @return true if the page-sequence has a page-master with page-position="last" */
    public boolean hasPagePositionLast() {
        return pageSequenceMaster != null && pageSequenceMaster.hasPagePositionLast();
    }

    /** @return true if the page-sequence has a page-master with page-position="only" */
    public boolean hasPagePositionOnly() {
        return pageSequenceMaster != null && pageSequenceMaster.hasPagePositionOnly();
    }

    /**
     * Get the value of the <code>master-reference</code> trait.
     * @return the "master-reference" trait
     */
    public String getMasterReference() {
        return masterReference;
    }

    /** {@inheritDoc} */
    public String getLocalName() {
        return "page-sequence";
    }

    /**
     * {@inheritDoc}
     * @return {@link org.apache.fop.fo.Constants#FO_PAGE_SEQUENCE}
     */
    public int getNameId() {
        return FO_PAGE_SEQUENCE;
    }

    public Locale getLocale() {
        return locale;
    }

    /**
     * Get the value of the <code>reference-orientation</code> trait.
     * @return the reference orientation trait value
     */
    public int getReferenceOrientation() {
        if (referenceOrientation != null) {
            return referenceOrientation.getValue();
        } else {
            return 0;
        }
    }

    /**
     * {@inheritDoc}
     */
    public Direction getInlineProgressionDirection() {
        if (writingModeTraits != null) {
            return writingModeTraits.getInlineProgressionDirection();
        } else {
            return Direction.LR;
        }
    }

    /**
     * {@inheritDoc}
     */
    public Direction getBlockProgressionDirection() {
        if (writingModeTraits != null) {
            return writingModeTraits.getBlockProgressionDirection();
        } else {
            return Direction.TB;
        }
    }

    /**
     * {@inheritDoc}
     */
    public Direction getColumnProgressionDirection() {
        if (writingModeTraits != null) {
            return writingModeTraits.getColumnProgressionDirection();
        } else {
            return Direction.LR;
        }
    }

    /**
     * {@inheritDoc}
     */
    public Direction getRowProgressionDirection() {
        if (writingModeTraits != null) {
            return writingModeTraits.getRowProgressionDirection();
        } else {
            return Direction.TB;
        }
    }

    /**
     * {@inheritDoc}
     */
    public Direction getShiftDirection() {
        if (writingModeTraits != null) {
            return writingModeTraits.getShiftDirection();
        } else {
            return Direction.TB;
        }
    }

    /**
     * {@inheritDoc}
     */
    public WritingMode getWritingMode() {
        if (writingModeTraits != null) {
            return writingModeTraits.getWritingMode();
        } else {
            return WritingMode.LR_TB;
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean getExplicitWritingMode() {
        if (writingModeTraits != null) {
            return writingModeTraits.getExplicitWritingMode();
        } else {
            return false;
        }
    }

    @Override
    protected Stack<DelimitedTextRange> collectDelimitedTextRanges(Stack<DelimitedTextRange> ranges,
        DelimitedTextRange currentRange) {
        // collect ranges from static content flows
        Map<String, FONode> flows = getFlowMap();
        if (flows != null) {
            for (FONode fn : flows.values()) {
                if (fn instanceof StaticContent) {
                    ranges = ((StaticContent) fn).collectDelimitedTextRanges(ranges);
                }
            }
        }
        // collect ranges in main flow
        Flow main = getMainFlow();
        if (main != null) {
            ranges = main.collectDelimitedTextRanges(ranges);
        }
        return ranges;
    }

    @Override
    protected boolean isBidiBoundary(boolean propagate) {
        return true;
    }

    /**
     * Releases a page-sequence's children after the page-sequence has been fully processed.
     */
    public void releasePageSequence() {
        this.mainFlow = null;
        this.flowMap.clear();
    }

    public SimplePageMaster getLastSimplePageMaster(int page, boolean isFirstPage, boolean isBlank) {
        boolean isOddPage = ((page % 2) != 0); // please findbugs...
        log.debug("getNextSimplePageMaster(page=" + page + " isOdd=" + isOddPage + " isFirst="
                + isFirstPage + " isLast=true" + " isBlank=" + isBlank + ")");
        if (pageSequenceMaster == null) {
            return simplePageMaster;
        }
        return pageSequenceMaster.getLastSimplePageMaster(isOddPage, isFirstPage, isBlank, getMainFlow()
                .getFlowName());
    }

    /**
     * Adds the specified change bar to the active change bar list.
     *
     * @param changeBarBegin The starting change bar element
     */
    public void pushChangeBar(ChangeBar changeBarBegin) {
        changeBarList.add(changeBarBegin);
    }

    /**
     * Removes the couple of the specified change bar from the active change bar list.
     *
     * @param changeBarEnd The ending change bar element
     */
    public void popChangeBar(ChangeBar changeBarEnd) {
        ChangeBar changeBarBegin = getChangeBarBegin(changeBarEnd);
        if (changeBarBegin != null) {
            changeBarList.remove(changeBarBegin);
        }
    }

    /**
     * Returns the starting counterpart of the specified ending change bar.
     *
     * @param changeBarEnd The ending change bar element
     * @return The starting counterpart of the specified ending change bar
     */
    public ChangeBar getChangeBarBegin(ChangeBar changeBarEnd) {
        if (changeBarList.isEmpty()) {
            return null;
        } else {
            String changeBarClass = changeBarEnd.getChangeBarClass();
            for (int i = changeBarList.size() - 1; i >= 0; i--) {
                ChangeBar changeBar = changeBarList.get(i);
                if (changeBar.getChangeBarClass().equals(changeBarClass)) {
                    return changeBar;
                }
            }
        }
        return null;
    }

    /**
     * Tests if there are any active change bars.
     *
     * @return A boolean value true if there are any active change bars
     */
    public boolean hasChangeBars() {
        return !changeBarList.isEmpty();
    }

    /**
     * Returns the list of active change bars.
     *
     * @return The list of active change bars
     */
    public List<ChangeBar> getChangeBarList() {
        return changeBarList;
    }

    /**
     * Returns the copy of active change bars list.
     *
     * @return The list containing a copy of the active change bars
     */
    public List<ChangeBar> getClonedChangeBarList() {
        return new LinkedList<ChangeBar>(changeBarList);
    }
}
