/*
 * Copyright 1999-2005 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.fo;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import org.apache.fop.apps.FOPException;
import org.apache.fop.datatypes.PercentBase;
import org.apache.fop.fo.flow.Marker;
import org.apache.fop.fo.properties.PropertyMaker;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;

/**
 * Base class for representation of formatting objects and their processing.
 */
public abstract class FObj extends FONode implements Constants {
    public static PropertyMaker[] propertyListTable = null;
    
    /** The immediate child nodes of this node. */
    public ArrayList childNodes = null;

    /** Used to indicate if this FO is either an Out Of Line FO (see rec)
        or a descendant of one.  Used during validateChildNode() FO 
        validation.
    */
    private boolean isOutOfLineFODescendant = false;

    /** Markers added to this element. */
    protected Map markers = null;

    /** Dynamic layout dimension. Used to resolve relative lengths. */
    protected Map layoutDimension = null;

    /**
     * Create a new formatting object.
     * All formatting object classes extend this class.
     *
     * @param parent the parent node
     * @todo move propertyListTable initialization someplace else?
     */
    public FObj(FONode parent) {
        super(parent);
        
        // determine if isOutOfLineFODescendant should be set
        if (parent != null && parent instanceof FObj) {
            if (((FObj)parent).getIsOutOfLineFODescendant() == true) {
                isOutOfLineFODescendant = true;
            } else {
                int foID = getNameId();
                if (foID == FO_FLOAT || foID == FO_FOOTNOTE
                    || foID == FO_FOOTNOTE_BODY) {
                        isOutOfLineFODescendant = true;
                }
            }
        }
        
        if (propertyListTable == null) {
            propertyListTable = new PropertyMaker[Constants.PROPERTY_COUNT + 1];
            PropertyMaker[] list = FOPropertyMapping.getGenericMappings();
            for (int i = 1; i < list.length; i++) {
                if (list[i] != null) {
                    propertyListTable[i] = list[i];
                }
            }
        }
    }

    /**
     * @see org.apache.fop.fo.FONode#clone(FONode, boolean)
     */
    public FONode clone(FONode parent, boolean removeChildren)
        throws FOPException {
        FObj fobj = (FObj) super.clone(parent, removeChildren);
        if (removeChildren) {
            fobj.childNodes = null;
        }
        return fobj;
    }

    /**
     * @see org.apache.fop.fo.FONode#processNode
     */
    public void processNode(String elementName, Locator locator, 
                            Attributes attlist, PropertyList pList) throws FOPException {
        setLocator(locator);
        pList.addAttributesToList(attlist);
        pList.setWritingMode();
        bind(pList);
    }

    /**
     * Create a default property list for this element. 
     */
    protected PropertyList createPropertyList(PropertyList parent, 
                    FOEventHandler foEventHandler) throws FOPException {
        return foEventHandler.getPropertyListMaker().make(this, parent);
    }

    /**
     * Bind property values from the property list to the FO node.
     * Must be overridden in all FObj subclasses that have properties
     * applying to it.
     * @param pList the PropertyList where the properties can be found.
     * @throws FOPException
     */
    public void bind(PropertyList pList) throws FOPException {
    }

    /**
     * Setup the id for this formatting object.
     * Most formatting objects can have an id that can be referenced.
     * This methods checks that the id isn't already used by another
     * fo and sets the id attribute of this object.
     */
     protected void checkId(String id) throws ValidationException {
        if (!id.equals("")) {
            Set idrefs = getFOEventHandler().getIDReferences();
            if (!idrefs.contains(id)) {
                idrefs.add(id);
            } else {
                throw new ValidationException("Property id \"" + id + 
                        "\" previously used; id values must be unique" +
                        " in document.", locator);
            }
        }
    }

    /**
     * Returns Out Of Line FO Descendant indicator.
     * @return true if Out of Line FO or Out Of Line descendant, false otherwise
     */
    public boolean getIsOutOfLineFODescendant() {
        return isOutOfLineFODescendant;
    }

    /**
     * @see org.apache.fop.fo.FONode#addChildNode(FONode)
     */
    protected void addChildNode(FONode child) throws FOPException {
        if (PropertySets.canHaveMarkers(getNameId()) && 
            child.getNameId() == FO_MARKER) {
                addMarker((Marker) child);
        } else {
            if (childNodes == null) {
                childNodes = new ArrayList();
            }
            childNodes.add(child);
        }
    }

    /** @see org.apache.fop.fo.FONode#removeChild(org.apache.fop.fo.FONode) */
    public void removeChild(FONode child) {
        if (childNodes != null) {
            childNodes.remove(child);
        }
    }
    
    /**
     * Find the nearest parent, grandparent, etc. FONode that is also an FObj
     * @return FObj the nearest ancestor FONode that is an FObj
     */
    public FObj findNearestAncestorFObj() {
        FONode par = parent;
        while (par != null && !(par instanceof FObj)) {
            par = par.parent;
        }
        return (FObj) par;
    }

    /* This section is the implemenation of the property context. */

    /**
     * Assign the size of a layout dimension to the key. 
     * @param key the Layout dimension, from PercentBase.
     * @param dimension The layout length.
     */
    public void setLayoutDimension(PercentBase.LayoutDimension key, int dimension) {
        if (layoutDimension == null) {
            layoutDimension = new HashMap();
        }
        layoutDimension.put(key, new Integer(dimension));
    }
    
    /**
     * Assign the size of a layout dimension to the key. 
     * @param key the Layout dimension, from PercentBase.
     * @param dimension The layout length.
     */
    public void setLayoutDimension(PercentBase.LayoutDimension key, float dimension) {
        if (layoutDimension == null) {
            layoutDimension = new HashMap();
        }
        layoutDimension.put(key, new Float(dimension));
    }
    
    /**
     * Return the size associated with the key.
     * @param key The layout dimension key.
     * @return the length.
     */
    public Number getLayoutDimension(PercentBase.LayoutDimension key) {
        if (layoutDimension != null) {
            Number result = (Number) layoutDimension.get(key);
            if (result != null) {
                return result;
            }
        }
        if (parent != null) {
            return ((FObj) parent).getLayoutDimension(key);
        }
        return new Integer(0);
    }

    /**
     * Check if this formatting object generates reference areas.
     * @return true if generates reference areas
     * @todo see if needed
     */
    public boolean generatesReferenceAreas() {
        return false;
    }

    /**
     * @see org.apache.fop.fo.FONode#getChildNodes()
     */
    public ListIterator getChildNodes() {
        if (childNodes != null) {
            return childNodes.listIterator();
        }
        return null;
    }

    /**
     * Return an iterator over the object's childNodes starting
     * at the passed-in node.
     * @param childNode First node in the iterator
     * @return A ListIterator or null if childNode isn't a child of
     * this FObj.
     */
    public ListIterator getChildNodes(FONode childNode) {
        if (childNodes != null) {
            int i = childNodes.indexOf(childNode);
            if (i >= 0) {
                return childNodes.listIterator(i);
            }
        }
        return null;
    }

    /**
     * Notifies a FObj that one of it's children is removed.
     * This method is subclassed by Block to clear the firstInlineChild variable.
     * @param node the node that was removed
     */
    protected void notifyChildRemoval(FONode node) {
        //nop
    }
    
    /**
     * Add the marker to this formatting object.
     * If this object can contain markers it checks that the marker
     * has a unique class-name for this object and that it is
     * the first child.
     * @param marker Marker to add.
     */
    protected void addMarker(Marker marker) {
        String mcname = marker.getMarkerClassName();
        if (childNodes != null) {
            // check for empty childNodes
            for (Iterator iter = childNodes.iterator(); iter.hasNext();) {
                FONode node = (FONode)iter.next();
                if (node instanceof FOText) {
                    FOText text = (FOText)node;
                    if (text.willCreateArea()) {
                        getLogger().error("fo:marker must be an initial child: " + mcname);
                        return;
                    } else {
                        iter.remove();
                        notifyChildRemoval(node);
                    }
                } else {
                    getLogger().error("fo:marker must be an initial child: " + mcname);
                    return;
                }
            }
        }
        if (markers == null) {
            markers = new HashMap();
        }
        if (!markers.containsKey(mcname)) {
            markers.put(mcname, marker);
        } else {
            getLogger().error("fo:marker 'marker-class-name' "
                    + "must be unique for same parent: " + mcname);
        }
    }

    /**
     * @return true if there are any Markers attached to this object
     */
    public boolean hasMarkers() {
        return markers != null && !markers.isEmpty();
    }

    /**
     * @return th collection of Markers attached to this object
     */
    public Map getMarkers() {
        return markers;
    }

    /*
     * Return a string representation of the fo element.
     * Deactivated in order to see precise ID of each fo element created
     *    (helpful for debugging)
     */
/*    public String toString() {
        return getName() + " at line " + line + ":" + column;
    }
*/    

    /**
     * Convenience method for validity checking.  Checks if the
     * incoming node is a member of the "%block;" parameter entity
     * as defined in Sect. 6.2 of the XSL 1.0 & 1.1 Recommendations
     * @param nsURI namespace URI of incoming node
     * @param lName local name (i.e., no prefix) of incoming node 
     * @return true if a member, false if not
     */
    protected boolean isBlockItem(String nsURI, String lName) {
        return (nsURI == FO_URI && 
            (lName.equals("block") 
            || lName.equals("table") 
            || lName.equals("table-and-caption") 
            || lName.equals("block-container")
            || lName.equals("list-block") 
            || lName.equals("float")
            || isNeutralItem(nsURI, lName)));
    }

    /**
     * Convenience method for validity checking.  Checks if the
     * incoming node is a member of the "%inline;" parameter entity
     * as defined in Sect. 6.2 of the XSL 1.0 & 1.1 Recommendations
     * @param nsURI namespace URI of incoming node
     * @param lName local name (i.e., no prefix) of incoming node 
     * @return true if a member, false if not
     */
    protected boolean isInlineItem(String nsURI, String lName) {
        return (nsURI == FO_URI && 
            (lName.equals("bidi-override") 
            || lName.equals("character") 
            || lName.equals("external-graphic") 
            || lName.equals("instream-foreign-object")
            || lName.equals("inline") 
            || lName.equals("inline-container")
            || lName.equals("leader") 
            || lName.equals("page-number") 
            || lName.equals("page-number-citation")
            || lName.equals("basic-link")
            || (lName.equals("multi-toggle")
                && (getNameId() == FO_MULTI_CASE || findAncestor(FO_MULTI_CASE) > 0))
            || (lName.equals("footnote") && !isOutOfLineFODescendant)
            || isNeutralItem(nsURI, lName)));
    }

    /**
     * Convenience method for validity checking.  Checks if the
     * incoming node is a member of the "%block;" parameter entity
     * or "%inline;" parameter entity
     * @param nsURI namespace URI of incoming node
     * @param lName local name (i.e., no prefix) of incoming node 
     * @return true if a member, false if not
     */
    protected boolean isBlockOrInlineItem(String nsURI, String lName) {
        return (isBlockItem(nsURI, lName) || isInlineItem(nsURI, lName));
    }

    /**
     * Convenience method for validity checking.  Checks if the
     * incoming node is a member of the neutral item list
     * as defined in Sect. 6.2 of the XSL 1.0 & 1.1 Recommendations
     * @param nsURI namespace URI of incoming node
     * @param lName local name (i.e., no prefix) of incoming node 
     * @return true if a member, false if not
     */
    protected boolean isNeutralItem(String nsURI, String lName) {
        return (nsURI == FO_URI && 
            (lName.equals("multi-switch") 
            || lName.equals("multi-properties")
            || lName.equals("wrapper") 
            || (!isOutOfLineFODescendant && lName.equals("float"))
            || lName.equals("retrieve-marker")));
    }
    
    /**
     * Convenience method for validity checking.  Checks if the
     * current node has an ancestor of a given name.
     * @param ancestorID -- Constants ID of node name to check for (e.g., FO_ROOT)
     * @return number of levels above FO where ancestor exists, 
     *    -1 if not found
     */
    protected int findAncestor(int ancestorID) {
        int found = 1;
        FONode temp = getParent();
        while (temp != null) {
            if (temp.getNameId() == ancestorID) {
                return found;
            }
            found += 1;
            temp = temp.getParent();
        }
        return -1;
    }
}

