/*
 * Copyright 1999-2004 The Apache Software Foundation.
 * 
 * Licensed 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.apps;

// Java
import java.util.Map;
import java.io.IOException;
import java.util.Set;
import java.util.HashSet;


// FOP
import org.apache.fop.apps.FOUserAgent;

import org.apache.fop.area.AreaTree;
import org.apache.fop.area.AreaTreeControl;
import org.apache.fop.area.AreaTreeModel;

import org.apache.fop.fo.extensions.Bookmarks;
import org.apache.fop.fo.FOInputHandler;
import org.apache.fop.fo.FOTreeControl;
import org.apache.fop.fo.FOTreeEvent;
import org.apache.fop.fo.FOTreeListener;
import org.apache.fop.fo.pagination.PageSequence;
import org.apache.fop.fonts.Font;
import org.apache.fop.fonts.FontMetrics;
import org.apache.fop.layout.LayoutStrategy;

// SAX
import org.xml.sax.SAXException;

// Avalon
import org.apache.avalon.framework.logger.Logger;

/**
 * Class storing information for the FOP Document being processed, and managing
 * the processing of it.
 */
public class Document implements FOTreeControl, FOTreeListener,
        AreaTreeControl {

    /** The parent Driver object */
    private Driver driver;

    /** Map containing fonts that have been used */
    private Map usedFonts;

    /** look up a font-triplet to find a font-name */
    private Map triplets;

    /** look up a font-name to get a font (that implements FontMetrics at least) */
    private Map fonts;

    /**
     * the LayoutStrategy to be used to process this document
     * TODO: this actually belongs in the RenderContext class, when it is
     * created
     */
    private LayoutStrategy layoutStrategy = null;

    /** The current AreaTree for the PageSequence being rendered. */
    public AreaTree areaTree;
    /** The AreaTreeModel for the PageSequence being rendered. */
    public AreaTreeModel atModel;

    private Bookmarks bookmarks = null;

    /**
     * The current set of id's in the FO tree.
     * This is used so we know if the FO tree contains duplicates.
     */
    private Set idReferences = new HashSet();

    /**
     * Structure handler used to notify structure events
     * such as start end element.
     */
    public FOInputHandler foInputHandler;

    /**
     * Main constructor
     * @param driver the Driver object that is the "parent" of this Document
     */
    public Document(Driver driver) {
        this.driver = driver;
        this.triplets = new java.util.HashMap();
        this.fonts = new java.util.HashMap();
        this.usedFonts = new java.util.HashMap();
    }

    /**
     * Checks if the font setup is valid (At least the ultimate fallback font
     * must be registered.)
     * @return True if valid
     */
    public boolean isSetupValid() {
        return triplets.containsKey(Font.DEFAULT_FONT);
    }

    /**
     * Adds a new font triplet.
     * @param name internal key
     * @param family font family name
     * @param style font style (normal, italic, oblique...)
     * @param weight font weight
     */
    public void addFontProperties(String name, String family, String style,
                                  int weight) {
        /*
         * add the given family, style and weight as a lookup for the font
         * with the given name
         */

        String key = createFontKey(family, style, weight);
        this.triplets.put(key, name);
    }

    /**
     * Adds font metrics for a specific font.
     * @param name internal key
     * @param metrics metrics to register
     */
    public void addMetrics(String name, FontMetrics metrics) {
        // add the given metrics as a font with the given name

        this.fonts.put(name, metrics);
    }

    /**
     * Lookup a font.
     * <br>
     * Locate the font name for a given family, style and weight.
     * The font name can then be used as a key as it is unique for
     * the associated document.
     * This also adds the font to the list of used fonts.
     * @param family font family
     * @param style font style
     * @param weight font weight
     * @return internal key
     */
    public String fontLookup(String family, String style,
                             int weight) {
        String key;
        // first try given parameters
        key = createFontKey(family, style, weight);
        String f = (String)triplets.get(key);
        if (f == null) {
            // then adjust weight, favouring normal or bold
            f = findAdjustWeight(family, style, weight);

            // then try any family with orig weight
            if (f == null) {
                key = createFontKey("any", style, weight);
                f = (String)triplets.get(key);
            }

            // then try any family with adjusted weight
            if (f == null) {
                f = findAdjustWeight(family, style, weight);
            }

            // then use default
            if (f == null) {
                f = (String)triplets.get(Font.DEFAULT_FONT);
            }

        }

        usedFonts.put(f, fonts.get(f));
        return f;
    }

    /**
     * Find a font with a given family and style by trying
     * different font weights according to the spec.
     * @param family font family
     * @param style font style
     * @param weight font weight
     * @return internal key
     */
    public String findAdjustWeight(String family, String style,
                             int weight) {
        String key;
        String f = null;
        int newWeight = weight;
        if (newWeight < 400) {
            while (f == null && newWeight > 0) {
                newWeight -= 100;
                key = createFontKey(family, style, newWeight);
                f = (String)triplets.get(key);
            }
        } else if (newWeight == 500) {
            key = createFontKey(family, style, 400);
            f = (String)triplets.get(key);
        } else if (newWeight > 500) {
            while (f == null && newWeight < 1000) {
                newWeight += 100;
                key = createFontKey(family, style, newWeight);
                f = (String)triplets.get(key);
            }
            newWeight = weight;
            while (f == null && newWeight > 400) {
                newWeight -= 100;
                key = createFontKey(family, style, newWeight);
                f = (String)triplets.get(key);
            }
        }
        if (f == null) {
            key = createFontKey(family, style, 400);
            f = (String)triplets.get(key);
        }

        return f;
    }

    /**
     * Determines if a particular font is available.
     * @param family font family
     * @param style font style
     * @param weight font weight
     * @return True if available
     */
    public boolean hasFont(String family, String style, int weight) {
        String key = createFontKey(family, style, weight);
        return this.triplets.containsKey(key);
    }

    /**
     * Creates a key from the given strings.
     * @param family font family
     * @param style font style
     * @param weight font weight
     * @return internal key
     */
    public static String createFontKey(String family, String style,
                                       int weight) {
        return family + "," + style + "," + weight;
    }

    /**
     * Gets a Map of all registred fonts.
     * @return a read-only Map with font key/FontMetrics pairs
     */
    public Map getFonts() {
        return java.util.Collections.unmodifiableMap(this.fonts);
    }

    /**
     * This is used by the renderers to retrieve all the
     * fonts used in the document.
     * This is for embedded font or creating a list of used fonts.
     * @return a read-only Map with font key/FontMetrics pairs
     */
    public Map getUsedFonts() {
        return this.usedFonts;
    }

    /**
     * Returns the FontMetrics for a particular font
     * @param fontName internal key
     * @return font metrics
     */
    public FontMetrics getMetricsFor(String fontName) {
        usedFonts.put(fontName, fonts.get(fontName));
        return (FontMetrics)fonts.get(fontName);
    }

    /**
     * Set the LayoutStrategy to be used to process this Document
     * @param ls the LayoutStrategy object to be used to process this Document
     */
    public void setLayoutStrategy(LayoutStrategy ls) {
        this.layoutStrategy = ls;
    }

    /**
     * @return this Document's LayoutStrategy object
     */
    public LayoutStrategy getLayoutStrategy () {
        return layoutStrategy;
    }

    /**
     * Public accessor for the parent Driver of this Document
     * @return the parent Driver for this Document
     */
    public Driver getDriver() {
        return driver;
    }

    /**
     * Required by the FOTreeListener interface. It handles an
     * FOTreeEvent that is fired when a PageSequence object has been completed.
     * @param event the FOTreeEvent that was fired
     * @throws FOPException for errors in building the PageSequence
     */
    public void foPageSequenceComplete (FOTreeEvent event) throws FOPException {
        PageSequence pageSeq = event.getPageSequence();
        areaTree.addBookmarksToAreaTree();
        layoutStrategy.format(pageSeq, areaTree);
    }

    /**
     * Required by the FOTreeListener interface. It handles an FOTreeEvent that
     * is fired when the Document has been completely parsed.
     * @param event the FOTreeEvent that was fired
     * @throws SAXException for parsing errors
     */
    public void foDocumentComplete (FOTreeEvent event) throws SAXException {
        //processAreaTree(atModel);
        try {
            areaTree.endDocument();
            driver.getRenderer().stopRenderer();
        } catch (IOException ex) {
            throw new SAXException(ex);
        }
    }

    /**
     * Get the area tree for this layout handler.
     *
     * @return the area tree for this document
     */
    public AreaTree getAreaTree() {
        return areaTree;
    }

    /**
     * Set the Bookmarks object for this Document
     * @param bookmarks the Bookmarks object containing the bookmarks for this
     * Document
     */
    public void setBookmarks(Bookmarks bookmarks) {
        this.bookmarks = bookmarks;
    }

    /**
     * Public accessor for the Bookmarks for this Document
     * @return the Bookmarks for this Document
     */
    public Bookmarks getBookmarks() {
        return bookmarks;
    }

    /**
     * Retuns the set of ID references.
     * @return the ID references
     */
    public Set getIDReferences() {
        return idReferences;
    }

    /**
     * @return the FOInputHandler for parsing this FO Tree
     */
    public FOInputHandler getFOInputHandler() {
        return foInputHandler;
    }

    /**
     * @return the Logger to be used for processing this Document
     */
    public Logger getLogger() {
        return getDriver().getLogger();
    }

    /**
     * @return the FOUserAgent used for processing this document
     */
    public FOUserAgent getUserAgent() {
        return getDriver().getUserAgent();
    }

}
