/*
 * Copyright 2001-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.
 */
 
package org.apache.fop.fo;

import java.lang.Character;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.BitSet;
import java.util.StringTokenizer;

import org.apache.fop.fo.FOTree;
import org.apache.fop.fo.FONode;
import org.apache.fop.fo.PropNames;
import org.apache.fop.fo.properties.Property;
import org.apache.fop.fo.expr.PropertyException;
import org.apache.fop.datatypes.Numeric;
import org.apache.fop.datatypes.Ints;
import org.apache.fop.datastructs.ROIntArray;
import org.apache.fop.datastructs.ROStringArray;
import org.apache.fop.datastructs.ROBitSet;
import org.apache.fop.datatypes.PropertyValue;

/**
 * This class contains a number of arrays containing values indexed by the
 * property index value, determined from the PropNames class.  These arrays
 * provide a means of accessing information about the nature of a property
 * through the property index value.
 * <p>Most of these arrays are initialised piecemeal as information is
 * required about a particular property.
 * There are also <tt>HashMap</tt>s which encode the various sets of
 * properties which are defined to apply to each of the Flow Objects,
 * and a <tt>BitSet</tt> of those properties which are
 * automatically inherited.  The <tt>HashMap</tt>s provide a convenient
 * means of specifying the relationship between FOs and properties.
 * @author <a href="mailto:pbwest@powerup.com.au">Peter B. West</a>
 */
public class PropertyConsts {

    private static final String tag = "$Name$";
    private static final String revision = "$Revision$";

    private static final String packageName = "org.apache.fop.fo";

    public static final PropertyConsts pconsts;
    static {
        try {
            pconsts = new PropertyConsts();
        } catch (PropertyException e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    public static final PropertyConsts getPropertyConsts() {
        return pconsts;
    }


    /**
     * A Property[] array containing Property objects corresponding to each
     * of the property indices in <tt>PropNames</tt>.
     * Initially empty, entries are filled on demand as calls for details
     * about individual properties are made.
     */
    private final Property[] properties
                        = new Property[PropNames.LAST_PROPERTY_INDEX + 1];

    /**
     * A Class[] array containing Class objects corresponding to each of the
     * class names in the classNames array.  Elements are set
     * in parallel to the creation of the class names in
     * the classNames array.  It can be indexed by the property name
     * constants defined in this file.
     */
    private final Class[] classes
                            = new Class[PropNames.LAST_PROPERTY_INDEX + 1];

    /**
     * A String[] array of the property class names.  This array is
     * effectively 1-based, with the first element being unused.
     * The elements of this array are set by converting the FO
     * property names from the array PropNames.propertyNames into class
     * names by converting the first character of every component word to
     * upper case, and removing all punctuation characters.
     * It can be indexed by the property name constants defined in
     * the PropNames class.
     */
    private final String[] classNames
                            = new String[PropNames.LAST_PROPERTY_INDEX + 1];

    /**
     * A HashMap whose elements are an integer index value keyed by the name
     * of a property class.  The index value is the index of the property
     * class name in the classNames[] array.
     */
    private final HashMap classToIndex
                        = new HashMap(PropNames.LAST_PROPERTY_INDEX + 1);

    /**
     * An <tt>int[]</tt> containing the <i>inherited</i> values from the
     * <tt>Property</tt> classes.
     */
    private final int[] inherited
                            = new int[PropNames.LAST_PROPERTY_INDEX + 1];

    /**
     * A <tt>BitSet</tt> of properties which are normally inherited
     * (strictly, not not inherited).
     * It is defined relative to the set of all properties; i.e. the
     * inheritability of any property can be established by testing the
     * bit in this set that corresponds to the queried property's index.
     * <p>The <tt>BitSet</tt> is private and is the basis for
     * <i>inheritedProperties</i>.
     */
    private final BitSet inheritedprops
                            = new BitSet(PropNames.LAST_PROPERTY_INDEX + 1);

    /**
     * An int[] array of the types of the <i>initialValue</i> field of each
     * property.  The array is indexed by the index value constants that are
     * defined in the PropNames class in parallel to the
     * PropNames.propertyNames[] array.
     */
    private final int[] initialValueTypes
                            = new int[PropNames.LAST_PROPERTY_INDEX + 1];

    /**
     * A <tt>PropertyValue</tt> array containing the initial values of
     * each of the properties.
     */
    private final PropertyValue[] initialValues
                    = new PropertyValue[PropNames.LAST_PROPERTY_INDEX + 1];

    /**
     * An int[] array of the values of the <i>dataTypes</i> field of each
     * property.  The array is indexed by the index value constants that are
     * defined in the PropNames class in parallel to the
     * PropNames.propertyNames[] array.
     * The array elements are set from the values of the
     * <i>dataTypes</i> field in each property class.
     */
    private final int[] datatypes
                            = new int[PropNames.LAST_PROPERTY_INDEX + 1];

    /**
     * An int[] array of the values of the <i>traitMapping</i> field of each
     * property.  The array is indexed by the index value constants that are
     * defined in the PropNames class in parallel to the
     * PropNames.propertyNames[] array.
     * The array elements are set from the values of the
     * <i>traitMapping</i> field in each property class.
     */
    private final int[] traitMappings
                            = new int[PropNames.LAST_PROPERTY_INDEX + 1];

    /**
     * Get the initial value type for a property name.
     * @param property String name of the FO property
     * @return int enumerated initialValueType.  These constants are defined
     * as static final ints in this class.  Note that an undefined property
     * name will return the constant defined as NOTYPE_IT
     */
    public int getInitialValueType(String property)
                    throws PropertyException
    {
        // Get the property index then index into the initialvaluetypes array
        return getInitialValueType(PropNames.getPropertyIndex(property));
    }

    /**
     * get the initial value type for a property index.
     * @param propindex int index of the FO property
     * @return int enumerated initialValueType.  These constants are defined
     * as static final ints in the Property class.
     * @throws PropertyException
     */
    public int getInitialValueType(int propindex)
            throws PropertyException
    {
        setupProperty(propindex);
        //System.out.println("getInitialValueType: " + propindex + " "
                            //+ initialValueTypes[propindex]);
        return initialValueTypes[propindex];
    }

    /**
     * Get the initial value <tt>PropertyValue</tt> for a given property.
     * Note that this is a <b>raw</b> value; if it is
     * an unresolved percentage that value will be returned.
     * @param index - the property index.
     * @return a <tt>PropertyValue</tt> containing the initial property
     * value element for the indexed property.
     * @exception <tt>PropertyException</tt>
     */
    public PropertyValue getInitialValue(int propindex)
            throws PropertyException
    {
        if (initialValues[propindex] != null)
            return initialValues[propindex];
        //System.out.println("PropertyConts.getInitialValue(" + propindex
                           //+ ") " + PropNames.getPropertyName(propindex));
        return
            (initialValues[propindex] =
                    setupProperty(propindex).getInitialValue(propindex));
    }

    /**
     * @param propindex <tt>int</tt> index of the property
     * @param foNode the node whose properties are being constructed.
     * @param value the <tt>PropertyValue</tt> being refined.
     * @return <tt>PropertyValue</tt> constructed by the property's
     * <i>refineParsing</i> method
     * @exception <tt>PropertyException</tt>
     */
    public PropertyValue refineParsing
                        (int propindex, FONode foNode, PropertyValue value)
        throws PropertyException
    {
        Property property = setupProperty(propindex);
        return property.refineParsing(propindex, foNode, value);
    }

    /**
     * @param propindex <tt>int</tt> index of the property
     * @param foNode the node whose properties are being constructed.
     * @param value the <tt>PropertyValue</tt> being refined.
     * @param nested - <tt>boolean</tt> indicating whether this method is
     * called normally (false), or as part of another <i>refineParsing</i>
     * method.
     * @see #refineParsing(FOTree,PropertyValue)
     * @return <tt>PropertyValue</tt> constructed by the property's
     * <i>refineParsing</i> method
     * @exception <tt>PropertyException</tt>
     */
    public PropertyValue refineParsing
        (int propindex, FONode foNode, PropertyValue value, boolean isNested)
        throws PropertyException
    {
        Property property = setupProperty(propindex);
        return property.refineParsing(propindex, foNode, value, isNested);
    }

    /**
     * Get the <tt>Numeric</tt> value corresponding to an enumerated value.
     * @param foNode the <tt>FONode</tt> being built
     * @param propindex int index of the FO property
     * @param enum - the integer equivalent of the enumeration keyword.
     * @return the <tt>Numeric</tt> result.
     * @throws PropertyException.
     */
    public Numeric getMappedNumeric(FONode foNode, int propindex, int enum)
            throws PropertyException
    {
        Property property = setupProperty(propindex);
        if ((datatypes[propindex] & Property.MAPPED_LENGTH) != 0)
            return property.getMappedLength(foNode, enum);
        else
            throw new PropertyException
                ("MAPPED_LENGTH not valid in "
                                    + PropNames.getPropertyName(propindex));
    }

    /**
     * @param propindex int index of the FO property
     * @return int type of inheritance for this property
     * (See constants defined in Properties.)
     * @throws PropertyException.
     */
    public int inheritance(String property) throws PropertyException {
        return inheritance(PropNames.getPropertyIndex(property));
    }

    /**
     * @param propindex int index of the FO property
     * @return int type of inheritance for this property
     * (See constants defined in Property.)
     * @throws PropertyException.
     */
    public int inheritance(int propindex) throws PropertyException {
        setupProperty(propindex);
        return inherited[propindex];
    }

    /**
     * @param propindex int index of the FO property
     * @return <tt>boolean</tt> is property inherited?
     * @throws PropertyException.
     */
    public boolean isInherited(int propindex) throws PropertyException {
        Property property = setupProperty(propindex);
        return inherited[propindex] != Property.NO;
    }

    /**
     * @param property String name of the FO property
     * @return <tt>boolean</tt> is property inherited?
     * @throws PropertyException.
     */
    public boolean isInherited(String property) throws PropertyException {
        return isInherited(PropNames.getPropertyIndex(property));
    }

    /**
     * @param propindex int index of the FO property
     * @return <tt>boolean</tt> is property a shorthand?
     * @throws PropertyException.
     */
    public boolean isShorthand(int propindex) throws PropertyException {
        Property property = setupProperty(propindex);
        return (datatypes[propindex] & Property.SHORTHAND) != 0;
    }

    /**
     * @param property String name of the FO property
     * @return <tt>boolean</tt> is property a shorthand?
     * @throws PropertyException.
     */
    public boolean isShorthand(String property) throws PropertyException {
        return isShorthand(PropNames.getPropertyIndex(property));
    }

    /**
     * @param propertyIndex int index of the FO property
     * @return <tt>boolean</tt> is property a compound?
     * @throws PropertyException.
     */
    public boolean isCompound(int propertyIndex) throws PropertyException {
        Property property = setupProperty(propertyIndex);
        return (datatypes[propertyIndex] & Property.COMPOUND) != 0;
    }

    /**
     * @param property String name of the FO property
     * @return <tt>boolean</tt> is property a compound?
     * @throws PropertyException.
     */
    public boolean isCompound(String property) throws PropertyException {
        return isCompound(PropNames.getPropertyIndex(property));
    }

    /**
     * @param propertyIndex int index of the FO property
     * @return <tt>int</tt> dataTypes value.
     * @throws PropertyException.
     */
    public int getDataTypes(int propertyIndex) throws PropertyException {
        Property property = setupProperty(propertyIndex);
        return datatypes[propertyIndex];
    }

    /**
     * @param property String name of the FO property
     * @return <tt>int</tt> dataTypes value.
     * @throws PropertyException.
     */
    public int getDataTypes(String property) throws PropertyException {
        return getDataTypes(PropNames.getPropertyIndex(property));
    }

    /**
     * Map the integer value of an enum into its mapped value.
     * Only valid when the datatype of the property includes MAPPED_ENUM.
     * <p>Generally, the path will be enumText->enumIndex->mappedEnumText.
     * @param index <tt>int</tt> containing the enumeration index.
     * @param enumMap an <tt>ROStringArray</tt> of the <tt>String</tt>s 
     * with the mapped enumeration values.
     * @return a <tt>String</tt> with the mapped enumeration text.
     */
    public String enumIndexToMapping(int index, ROStringArray enumMap)
    {
        return enumMap.get(index);
    }

    /**
     * @param propindex <tt>int</tt> property index.
     * @param enum <tt>String</tt> containing the enumeration text.
     * @return <tt>int</tt> constant representing the enumeration value.
     * @exception PropertyException
     */
    public int getEnumIndex(int propindex, String enum)
                    throws PropertyException
    {
        Property property = setupProperty(propindex);
        return property.getEnumIndex(enum);
    }

    /**
     * @param propindex <tt>int</tt> property index.
     * @param enumIndex <tt>int</tt> containing the enumeration index.
     * @return <tt>String</tt> containing the enumeration text.
     * @exception PropertyException
     */
    public String getEnumText(int propindex, int enumIndex)
                    throws PropertyException
    {
        Property property = setupProperty(propindex);
        return property.getEnumText(enumIndex);
    }

    /**
     * Set up the details of a single property and return the
     * <tt>Property</tt> object.  If the <tt>Property</tt> object
     * corresponding to the property index has not been resolved before,
     * derive the Class and Property objects, and extract certain field
     * values from the Property.
     * @param propindex - the <tt>int</tt> index.
     * @return - the <tt>Property</tt> corresponding to the index.
     * @throws <tt>PropertyException.
     */
    public Property setupProperty(int propindex)
            throws PropertyException
    {
        String cname = "";
        Class pclass;
        Property property;

        //System.out.println("setupProperty " + propindex + " "
                            //+ PropNames.getPropertyName(propindex));
        if ((property = properties[propindex]) != null) return property;

        // Get the property class name
        StringTokenizer stoke;
        stoke = new StringTokenizer
                            (PropNames.getPropertyName(propindex), "-.:");
        while (stoke.hasMoreTokens()) {
            String token = stoke.nextToken();
            String pname = new Character(
                                Character.toUpperCase(token.charAt(0))
                            ).toString() + token.substring(1);
            cname = cname + pname;
        }
        classNames[propindex] = cname;
        
        // Set up the classToIndex Hashmap with the name of the
        // property class as a key, and the integer index as a value
        if (classToIndex.put(cname, Ints.consts.get(propindex)) != null)
            throw new PropertyException
                ("Duplicate values in classToIndex for key " + cname);

        // Get the class for this property name
        String name = packageName + ".properties." + cname;
        try {
            //System.out.println("classes["+propindex+"] "+name);//DEBUG
            pclass = Class.forName(name);
            classes[propindex] = pclass;

            // Instantiate the class
            property = (Property)(pclass.newInstance());
            properties[propindex] = property;
            //System.out.println
                    //("property name "
                     //+ property.getClass().getName());
            //System.out.println
            //("property name " +
            //properties[propindex].getClass().getName());

            // Set inheritance value
            if ((inherited[propindex]
                                = pclass.getField("inherited").getInt(null))
                    != Property.NO)
                            inheritedprops.set(propindex);
            // Set datatypes
            datatypes[propindex] = pclass.getField("dataTypes").getInt(null);
            //System.out.println("datatypes " + datatypes[propindex] + "\n"
                           //+ Property.listDataTypes(datatypes[propindex]));

            // Set initialValueTypes
            initialValueTypes[propindex] =
                            pclass.getField("initialValueType").getInt(null);
            //System.out.println("initialValueType "
                               //+ initialValueTypes[propindex]);

            traitMappings[propindex] =
                                pclass.getField("traitMapping").getInt(null);

        } catch (ClassNotFoundException e) {
            throw new PropertyException
                    ("ClassNotFoundException" + e.getMessage());
        } catch (IllegalAccessException e) {
            throw new PropertyException
                    ("IllegalAccessException" + e.getMessage());
        } catch (InstantiationException e) {
            throw new PropertyException
                    ("InstantiationException" + e.getMessage());
        }
        catch (NoSuchFieldException e) {
            throw new PropertyException
                    ("NoSuchFieldException" + e.getMessage());
        }

        return property;
    }


    private PropertyConsts () throws PropertyException {}

}