/*
 * 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: Page.java 1681384 2015-05-23 21:07:13Z adelmelle $ */

package org.apache.fop.area;

import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import java.util.List;
import java.util.Map;

import org.apache.fop.datatypes.FODimension;
import org.apache.fop.datatypes.LengthBase;
import org.apache.fop.datatypes.SimplePercentBaseContext;
import org.apache.fop.fo.pagination.Region;
import org.apache.fop.fo.pagination.RegionBody;
import org.apache.fop.fo.pagination.SimplePageMaster;
import org.apache.fop.fo.properties.CommonMarginBlock;
import org.apache.fop.layoutmgr.TraitSetter;
import org.apache.fop.traits.WritingModeTraitsGetter;

import static org.apache.fop.fo.Constants.EN_ERROR_IF_OVERFLOW;
import static org.apache.fop.fo.Constants.EN_HIDDEN;
import static org.apache.fop.fo.Constants.FO_REGION_AFTER;
import static org.apache.fop.fo.Constants.FO_REGION_BEFORE;
import static org.apache.fop.fo.Constants.FO_REGION_BODY;
import static org.apache.fop.fo.Constants.FO_REGION_END;
import static org.apache.fop.fo.Constants.FO_REGION_START;

/**
 * The page.
 * This holds the contents of the page. Each region is added.
 * The unresolved references area added so that if the page is
 * serialized then it will handle the resolving properly after
 * being reloaded.
 * This is serializable so it can be saved to cache to save
 * memory if there are forward references.
 * The page is cloneable so the page master can make copies of
 * the top level page and regions.
 */
public class Page extends AreaTreeObject implements Serializable {

    private static final long serialVersionUID = 6272157047421543866L;

    // contains before, start, body, end and after regions
    private RegionViewport regionBefore;
    private RegionViewport regionStart;
    private RegionViewport regionBody;
    private RegionViewport regionEnd;
    private RegionViewport regionAfter;

    // temporary map of unresolved objects used when serializing the page
    private Map<String, List<Resolvable>> unresolved;

    /** Set to true to make this page behave as if it were not empty. */
    private boolean fakeNonEmpty;

    /**
     *  Empty constructor
     */
    public Page() { }

    /**
     * Constructor
     * @param spm SimplePageMaster containing the dimensions for this
     *            page-reference-area
     */
    public Page(SimplePageMaster spm) {
        // Width and Height of the page view port
        FODimension pageViewPortDims = new FODimension(spm.getPageWidth().getValue()
                            ,  spm.getPageHeight().getValue());

        // Get absolute margin properties (top, left, bottom, right)
        CommonMarginBlock mProps = spm.getCommonMarginBlock();

        /*
         * Create the page reference area rectangle (0,0 is at top left
         * of the "page media" and y increases
         * when moving towards the bottom of the page.
         * The media rectangle itself is (0,0,pageWidth,pageHeight).
         */
        /* Special rules apply to resolving margins in the page context.
         * Contrary to normal margins in this case top and bottom margin
         * are resolved relative to the height. In the property subsystem
         * all margin properties are configured to using BLOCK_WIDTH.
         * That's why we 'cheat' here and setup a context for the height but
         * use the LengthBase.BLOCK_WIDTH.
         */
        SimplePercentBaseContext pageWidthContext
            = new SimplePercentBaseContext(null, LengthBase.CONTAINING_BLOCK_WIDTH
                                            , pageViewPortDims.ipd);
        SimplePercentBaseContext pageHeightContext
            = new SimplePercentBaseContext(null, LengthBase.CONTAINING_BLOCK_WIDTH
                                            , pageViewPortDims.bpd);

        Rectangle pageRefRect
            =  new Rectangle(mProps.marginLeft.getValue(pageWidthContext)
                            , mProps.marginTop.getValue(pageHeightContext)
                            , pageViewPortDims.ipd
                                - mProps.marginLeft.getValue(pageWidthContext)
                                - mProps.marginRight.getValue(pageWidthContext)
                            , pageViewPortDims.bpd
                                - mProps.marginTop.getValue(pageHeightContext)
                                - mProps.marginBottom.getValue(pageHeightContext));

        // Set up the CTM on the page reference area based on writing-mode
        // and reference-orientation
        FODimension reldims = new FODimension(0, 0);
        CTM pageCTM = CTM.getCTMandRelDims(spm.getReferenceOrientation(),
            spm.getWritingMode(), pageRefRect, reldims);

        // Create a RegionViewport/ reference area pair for each page region
        RegionReference rr;
        for (Region r : spm.getRegions().values()) {
            RegionViewport rvp = makeRegionViewport(r, reldims, pageCTM);
            if (r.getNameId() == FO_REGION_BODY) {
                rr = new BodyRegion((RegionBody) r, rvp);
            } else {
                rr = new RegionReference(r, rvp);
            }
            // set borders and padding traits
            // (a little extensions wrt what prescribed by the specs at 6.4.14)
            TraitSetter.addBorders(rr, r.getCommonBorderPaddingBackground(),
                    false, false, false, false, null);
            TraitSetter.addPadding(rr, r.getCommonBorderPaddingBackground(),
                    false, false, false, false, null);
            setRegionReferencePosition(rr, r, rvp.getViewArea());
            rvp.setRegionReference(rr);
            setRegionViewport(r.getNameId(), rvp);
        }
    }

    /**
     * Call this method to force this page to pretend not to be empty.
     */
    public void fakeNonEmpty() {
        this.fakeNonEmpty = true;
    }

    /**
     * Creates a RegionViewport Area object for this pagination Region.
     * @param r the region the viewport is to be created for
     * @param reldims relative dimensions
     * @param pageCTM page coordinate transformation matrix
     * @return the new region viewport
     */
    private static RegionViewport makeRegionViewport(Region r, FODimension reldims, CTM pageCTM) {
        Rectangle2D relRegionRect = r.getViewportRectangle(reldims);
        Rectangle2D absRegionRect = pageCTM.transform(relRegionRect);
        // Get the region viewport rectangle in absolute coords by
        // transforming it using the page CTM
        RegionViewport rv = new RegionViewport(absRegionRect);
        rv.setBPD((int)relRegionRect.getHeight());
        rv.setIPD((int)relRegionRect.getWidth());
        TraitSetter.addBackground(rv, r.getCommonBorderPaddingBackground(), null);
        rv.setClip(r.getOverflow() == EN_HIDDEN
                || r.getOverflow() == EN_ERROR_IF_OVERFLOW);
        return rv;
    }

    /**
     * Set the region reference position within the region viewport.
     * This sets the transform that is used to place the contents of
     * the region reference.
     *
     * @param rr the region reference area
     * @param r the region-xxx formatting object
     * @param absRegVPRect The region viewport rectangle in "absolute" coordinates
     * where x=distance from left, y=distance from bottom, width=right-left
     * height=top-bottom
     */
    private static void setRegionReferencePosition(RegionReference rr, Region r,
                                  Rectangle2D absRegVPRect) {
        FODimension reldims = new FODimension(0, 0);
        rr.setCTM(CTM.getCTMandRelDims(r.getReferenceOrientation(),
                r.getWritingMode(), absRegVPRect, reldims));
        rr.setIPD(reldims.ipd
                - rr.getBorderAndPaddingWidthStart()
                - rr.getBorderAndPaddingWidthEnd());
        rr.setBPD(reldims.bpd
                - rr.getBorderAndPaddingWidthBefore()
                - rr.getBorderAndPaddingWidthAfter());
    }

    /**
     * Set the region on this page.
     *
     * @param areaclass the area class of the region to set
     * @param port the region viewport to set
     */
    public void setRegionViewport(int areaclass, RegionViewport port) {
        if (areaclass == FO_REGION_BEFORE) {
            regionBefore = port;
        } else if (areaclass == FO_REGION_START) {
            regionStart = port;
        } else if (areaclass == FO_REGION_BODY) {
            regionBody = port;
        } else if (areaclass == FO_REGION_END) {
            regionEnd = port;
        } else if (areaclass == FO_REGION_AFTER) {
            regionAfter = port;
        }
    }

    /**
     * Get the region from this page.
     *
     * @param areaClass the region area class
     * @return the region viewport or null if none
     */
    public RegionViewport getRegionViewport(int areaClass) {
        switch (areaClass) {
        case FO_REGION_BEFORE:
            return regionBefore;
        case FO_REGION_START:
            return regionStart;
        case FO_REGION_BODY:
            return regionBody;
        case FO_REGION_END:
            return regionEnd;
        case FO_REGION_AFTER:
            return regionAfter;
        default:
            throw new IllegalArgumentException("No such area class with ID = " + areaClass);
        }
    }

    /**
     * Indicates whether any FOs have been added to the body region
     *
     * @return whether any FOs have been added to the body region
     */
    public boolean isEmpty() {
        if (fakeNonEmpty) {
            return false;
        } else if (regionBody == null) {
            return true;
        } else {
            return regionBody.getRegionReference().isEmpty();
        }
    }

    /** {@inheritDoc} */
    public Object clone() throws CloneNotSupportedException {
        Page p = (Page) super.clone();
        if (regionBefore != null) {
            p.regionBefore = (RegionViewport)regionBefore.clone();
        }
        if (regionStart != null) {
            p.regionStart = (RegionViewport)regionStart.clone();
        }
        if (regionBody != null) {
            p.regionBody = (RegionViewport)regionBody.clone();
        }
        if (regionEnd != null) {
            p.regionEnd = (RegionViewport)regionEnd.clone();
        }
        if (regionAfter != null) {
            p.regionAfter = (RegionViewport)regionAfter.clone();
        }

        return p;
    }

    /**
     * Set the unresolved references on this page for serializing.
     *
     * @param unres the Map of unresolved objects
     */
    public void setUnresolvedReferences(Map<String, List<Resolvable>> unres) {
        unresolved = unres;
    }

    /**
     * Get the map unresolved references from this page.
     * This should be called after deserializing to retrieve
     * the map of unresolved references that were serialized.
     *
     * @return the de-serialized HashMap of unresolved objects
     */
    public Map<String, List<Resolvable>> getUnresolvedReferences() {
        return unresolved;
    }

    /**
     * Sets the writing mode traits for the region viewports of
     * this page.
     * @param wmtg a WM traits getter
     */
    public void setWritingModeTraits(WritingModeTraitsGetter wmtg) {
        if (regionBefore != null) {
            regionBefore.setWritingModeTraits(wmtg);
        }
        if (regionStart != null) {
            regionStart.setWritingModeTraits(wmtg);
        }
        if (regionBody != null) {
            regionBody.setWritingModeTraits(wmtg);
        }
        if (regionEnd != null) {
            regionEnd.setWritingModeTraits(wmtg);
        }
        if (regionAfter != null) {
            regionAfter.setWritingModeTraits(wmtg);
        }
    }

}


