package net.sourceforge.rtf.context;

import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import net.sourceforge.rtf.document.transformer.config.TransformerConfig;
import net.sourceforge.rtf.format.rtfcode.IRTFCode;
import net.sourceforge.rtf.template.IContext;

import org.apache.commons.beanutils.PropertyUtils;

/**
 * Read velocity context to detect if Object JAVA putted into velocity context
 * is list or not. To use this abstract class, you must implement event
 * startReading and endReading.
 * 
 * @version 1.0.0
 * @author <a href="mailto:angelo.zerr@gmail.com">Angelo ZERR</a>
 * 
 */
public abstract class AbstractRTFContextReader {

    public static final String MATCH_PATH_DEFINITIONS = "matchPathDefinitions";

    private String[] matchPaths;
    
	private List excludedPackages = null; // package name to exclude while

    // true if context has circular references and false otherwise
    private boolean circularReferences = false;

    // searching getter/setter of JAVA
    // object.

    /**
     * Default constructor which exclude package where name start with "java."
     * This exlusion is used when getter of one JAVA object of velocity context
     * is computed. For instance when JAVA object is a String, you don't want
     * have getter of String object.
     */
    public AbstractRTFContextReader() {
        this.excludedPackages = new ArrayList();
        excludePackageName("java.");
    }

    /**
     * Constructor to customize you package name to exclude.
     * 
     * @param excludedPackages the excludedPackages.
     */
    public AbstractRTFContextReader(List excludedPackages) {
        this.excludedPackages = excludedPackages;
    }

    public void excludePackageName(String packageName) {
        excludedPackages.add(packageName);
    }

    public void readContext(IContext context,
            TransformerConfig transformerConfig, boolean circularReferences ) {
        this.circularReferences = circularReferences;
        startReading();
        if (transformerConfig != null) {
            // Add Bookmark start loop
            String bookmarkName = transformerConfig
                    .getBookmarkStartLoopPattern();
            addBookmark(bookmarkName,
                    TransformerConfig.START_LOOP_BOOKMARK_TYPE);
            // Add Bookmark end loop
            bookmarkName = transformerConfig.getBookmarkEndLoopPattern();
            addBookmark(bookmarkName, TransformerConfig.END_LOOP_BOOKMARK_TYPE);
            // Add Bookmark IF
            bookmarkName = transformerConfig.getBookmarkIfPattern();
            addBookmark(bookmarkName, TransformerConfig.IF_BOOKMARK_TYPE);

        }
        // Read inner global context
        read(context.getGlobalContext());
        // Read velocity context
        read(context);
        endReading();
    }

    /**
     * Read velocity context, launch event startReading at first and when read
     * is finished launch event endReading.
     * 
     * @param context the context.
     * @param circularReferences the circularReferences.
     */
    public void readContext(IContext context, boolean circularReferences) {
        readContext(context, null, circularReferences);        
    }

    /**
     * Read velocity context
     * 
     * @param context
     */
    private void read(IContext context) {
    	matchPaths = (String[])context.get(MATCH_PATH_DEFINITIONS);
        if (context != null) {
            // Loop for keys of context
            Object[] keys = context.getKeys();
            if (keys != null) {
                for (int kk = 0; kk < keys.length; kk++) {
                    String key = (String) keys[kk];
                    if (!key.equals(MATCH_PATH_DEFINITIONS)) {
                        Object objectValue = context.get(key);
                        String keyWithListInfo = key;
                        if (isList(objectValue)) {
                            keyWithListInfo = "[" + key + "]";
                        }
                        List objectsClassAlreadyParsed = (circularReferences ? new ArrayList()
                                : null);
                        //EMY
                        if (objectsClassAlreadyParsed != null) {
                        	objectsClassAlreadyParsed.add(objectValue.getClass());
                        }
                        parseObject(key, objectValue, keyWithListInfo,
                                objectsClassAlreadyParsed);
                    }
                }
            }
        }
    }

    private void parseObject(String objectName, Object objectValue,
            String objectNameWithListInfo, List objectsClassAlreadyParsed) {
    	if (matchPath(objectName)) {
            // with [] when getter is
            // list
            // Test if objectValue is mergefield list or not
            boolean mergeFieldIsList = isList((objectValue));
            boolean isMergeField = false;
            if (mergeFieldIsList) {
                // Object is List, get the first object of the list
                objectValue = getFirstObject((Collection) objectValue);
                // objectNameWithListInfo = "[" + objectNameWithListInfo + "]";
            }
            isMergeField = (objectValue != null);
            if (isMergeField) {
                processMergeField(objectName, objectValue, objectNameWithListInfo,
    					objectsClassAlreadyParsed, mergeFieldIsList);
            }
    	}
    }

	private boolean matchPath(String objectName) {
		if (matchPaths == null) {
			return true;
		}
		if (objectName.startsWith("$")) {
			objectName = objectName.substring(1);
		}
		for (int i = 0; i < matchPaths.length; i++) {
			if (matchPaths[i].startsWith(objectName)) {
				return true;
			}
		}
		return false;
	}

	private void processMergeField(String objectName, Object objectValue,
			String objectNameWithListInfo, List objectsClassAlreadyParsed,
			boolean mergeFieldIsList) 
	{
		// if object is Type of String, Integer... don't search
		// getter of the object
		if (isJavaObjectToExlude(objectValue)) {
		    processJavaObjectField(objectName, objectValue, objectNameWithListInfo, mergeFieldIsList);
		} else {
		    // Loop for descriptor of getter of object
		    PropertyDescriptor[] origDescriptors = PropertyUtils
		            .getPropertyDescriptors(objectValue);
		    for (int i = 0; i < origDescriptors.length; i++) {
		        PropertyDescriptor currentDescriptor = origDescriptors[i];
		        String getterName = currentDescriptor.getName();
		        if (!discardProperty(getterName)) {
		            // Test if current getter is List
		            Object objectGetterName = null;
		            try {
		                objectGetterName = PropertyUtils.getProperty(objectValue, getterName);
		            } catch (Exception e) {
		            }
		            processField(objectName,
							objectNameWithListInfo,
							objectsClassAlreadyParsed, 
							mergeFieldIsList, getterName, objectGetterName);
		        }
		    }
		}
	}

	private void processJavaObjectField(String objectName, Object objectValue,
			String objectNameWithListInfo, boolean mergeFieldIsList) {
		String mergeFieldName;
		String mergeFieldNameWithListInfo;
		if (objectValue instanceof Map) {
		    processMapField(objectName, objectValue, objectNameWithListInfo, mergeFieldIsList);
		} else {
		    boolean isImage = isImage(objectValue.getClass());
		    mergeFieldName = getMergeFieldName(objectName, "");
		    mergeFieldNameWithListInfo = getMergeFieldName(
		            objectNameWithListInfo, "", true, true);
		    addMergeField(mergeFieldName, mergeFieldIsList,
		            mergeFieldNameWithListInfo, isImage);
		}
	}

	private void processMapField(String objectName, Object objectValue,
			String objectNameWithListInfo, boolean mergeFieldIsList) {
		String mergeFieldName;
		String mergeFieldNameWithListInfo;
		// Context is map
		/**
		 * Map projectMap = new HashMap(); projectMap.put("Name",
		 * "Jakarta Velocity"); // => in the model $project.Name
		 * projectMap.put("name2", "Jakarta Velocity (2)"); // => in
		 * the model $project.name2 context.put("project",
		 * projectMap);
		 */
		Map objectValueMap = (Map) objectValue;
		for (Iterator iter = objectValueMap.keySet().iterator(); iter
		        .hasNext();) {
		    Object element = iter.next();
		    if (element instanceof String) {
		        String elementName = (String) element;
		        boolean isImage = isImage(objectValue);
		        mergeFieldName = getMergeFieldName(objectName,
		                elementName, false, false);
		        mergeFieldNameWithListInfo = getMergeFieldName(
		                objectNameWithListInfo, elementName, false,
		                false);
		        addMergeField(mergeFieldName, mergeFieldIsList,
		                mergeFieldNameWithListInfo, isImage);
		    }
		}
	}

	private void processField(String objectName,
			String objectNameWithListInfo, List objectsClassAlreadyParsed,
			boolean mergeFieldIsList, String getterName,
			Object objectGetterName) {
		
		if (objectGetterName != null && isList(objectGetterName)) {
		    processListField(objectName, objectNameWithListInfo, objectsClassAlreadyParsed, getterName, objectGetterName);
		} else {
		    if (objectGetterName != null && !isJavaObjectToExlude(objectGetterName)) {
		        processComplexField(objectName, objectNameWithListInfo, objectsClassAlreadyParsed, getterName, objectGetterName);
		    } else {
		        processSimpleField(objectName, objectNameWithListInfo, mergeFieldIsList, getterName, objectGetterName);
		    }
		}
	}

	private boolean processSimpleField(String objectName,
			String objectNameWithListInfo, boolean mergeFieldIsList,
			String getterName, Object objectGetterName) {
		String mergeFieldName;
		String mergeFieldNameWithListInfo;
		mergeFieldName = getMergeFieldName(objectName,
		        getterName);
		mergeFieldNameWithListInfo = getMergeFieldName(
		        objectNameWithListInfo, getterName);
		// Test if getter type isInputStream
		boolean isImage = false;
		if (objectGetterName != null)
		    isImage = isImage(objectGetterName
		            .getClass());
		if (mergeFieldNameWithListInfo != null
		        && mergeFieldNameWithListInfo.length() > 0) {
		    mergeFieldIsList = (mergeFieldNameWithListInfo
		            .indexOf("[") != -1);
		}

		addMergeField(mergeFieldName, mergeFieldIsList,
		        mergeFieldNameWithListInfo, isImage);
		return mergeFieldIsList;
	}

	private void processComplexField(String objectName,
			String objectNameWithListInfo, List objectsClassAlreadyParsed,
			String getterName, Object objectGetterName) {
		// Object has getter which have object of object
		// (complex object, graph object)
		if (!circularReferences
		        || (circularReferences && !objectsClassAlreadyParsed
		                .contains(objectGetterName
		                        .getClass()))) {
		    if (circularReferences)
		        objectsClassAlreadyParsed
		                .add(objectGetterName
		                        .getClass());
		    parseObject(objectName + "." + getterName,
		            objectGetterName,
		            objectNameWithListInfo + "."
		                    + getterName,
		            objectsClassAlreadyParsed);
		    if (circularReferences)
		        objectsClassAlreadyParsed
		                .remove(objectGetterName
		                        .getClass());
		}
	}

	private void processListField(String objectName,
			String objectNameWithListInfo, List objectsClassAlreadyParsed,
			String getterName, Object objectGetterName) {
		String mergeFieldName;
		String mergeFieldNameWithListInfo;
		// current getter is List
		// get the first item of the collection
		// Object firstObject = getFirstObject((Collection)
		// objectGetterName);
		// get the name of the
		// getterName =
		// firstObject.getClass().getSimpleName();
		mergeFieldName = getMergeFieldName(objectName,
		        getterName);
		mergeFieldNameWithListInfo = getMergeFieldName(
		        objectNameWithListInfo, getterName, true,
		        true);
		parseObject(mergeFieldName, objectGetterName,
		        mergeFieldNameWithListInfo,
		        objectsClassAlreadyParsed);
	}

	private boolean discardProperty(String getterName) {
		if (getterName.equals("class") || getterName.equals("childPropertyNames")
				|| getterName.equals("changedEntity") || getterName.equals("currentTimestamp")
				|| getterName.equals("deletedEntity") || getterName.equals("internalEntityState")
				|| getterName.equals("lazyEntity") || getterName.equals("lazyPkFilledEntity")
				|| getterName.equals("loadedEntity") || getterName.equals("newEntity")
				|| getterName.equals("newOrLazyEntity") || getterName.equals("newPkFilledEntity")
				|| getterName.equals("objectDump") || getterName.equals("objectKey")
				|| getterName.equals("pkNotNull") || getterName.equals("pkNull")
				|| getterName.equals("this")) {
			return true;
		}
		if (getterName.endsWith("NullSafe") || getterName.endsWith("WithNoLI")
				|| getterName.endsWith("WithNoCache") || getterName.endsWith("AsArray")
				|| getterName.endsWith("ObjectKey") || getterName.endsWith("ObjectDump")) {
			return true;
		}
		return false;
	}

    private Object getFirstObject(Collection objectCollection) {
        Object objectValue = null;
        if (objectCollection.size() > 0) {
            for (Iterator iter = objectCollection.iterator(); iter.hasNext();) {
                objectValue = iter.next();
                break;
            }
        }
        return objectValue;
    }

    /**
     * Return true if objectType is list (Collection , List...) and false
     * otherwise.
     * 
     * @param objectType
     * @return
     */
    private boolean isList(Object objectType) {
        return RTFContextUtil.isList(objectType);
    }

    private String getMergeFieldName(String objectName, String getterName) {
        return getMergeFieldName(objectName, getterName, false, true);
    }

    /**
     * Construct Merge field Name by using objectName and getter getterName with
     * syntax velocity like $objectName.GetterName
     * 
     * @param objectName
     * @param getterName
     * @param isList
     * @return
     */
    private String getMergeFieldName(String objectName, String getterName,
            boolean addArray, boolean mustUpperCaseOnfirstChar) {
        String fullObjectName = "";
        int getterNameLength = getterName.length();
        switch (getterNameLength) {
        case 0: // objectName has no getter (ex : objectName=my_var, we must
            // return $my_var)
            fullObjectName = objectName;
            break;
        case 1: // objectName has getter name with 1 character
            // This must be UpperCase.
            // (ex : objectName=my_obj and getterName = g, we must return
            // $my_obj.G)
            fullObjectName = objectName + "." + getterName.toUpperCase();
            break;
        default:
            // objectName has getter name with more > 1 character
            // The first character of the getter Name must be UpperCase
            // (ex : objectName=my_obj and getterName = Name, we must return
            // $my_obj.Name)
            String characterFirst = getterName.substring(0, 1);
            if (mustUpperCaseOnfirstChar)
                characterFirst = characterFirst.toUpperCase();
            getterName = characterFirst
                    + getterName.substring(1, getterName.length());
            if (addArray) {
                getterName = "[" + getterName + "]";
            }
            fullObjectName = objectName + "." + getterName;
        }
        if (!fullObjectName.startsWith("$"))
            fullObjectName = "$" + fullObjectName;
        return fullObjectName;

    }

    /**
     * Return true if package of object objectValue start with list of package
     * to exclude and false otherwise.
     * 
     * @param objectValue
     * @return
     */
    private boolean isJavaObjectToExlude(Object objectValue) {
        if (objectValue != null) {
            if (objectValue instanceof IRTFCode)
                return true;
            Class objectClass = objectValue.getClass();
            if (objectClass != null && objectClass.getPackage() != null) {
                String packageName = objectClass.getPackage().getName();
                for (Iterator iter = excludedPackages.iterator(); iter
                        .hasNext();) {
                    String excludePackageName = (String) iter.next();
                    if (packageName.startsWith(excludePackageName))
                        return true;
                }
            }
        }
        return false;
    }

    /**
     * Return true if objectValue is type of java.io.InputStream and false
     * otherwise
     * 
     * @param objectValue
     * @return
     */
    private boolean isImage(Object objectValue) {
        return (objectValue instanceof java.io.InputStream);
    }

    protected abstract void startReading();

    protected abstract void endReading();

    protected abstract void addBookmark(String bookmarkName, String type);

    protected abstract void addMergeField(String mergeFieldName,
            boolean mergeFieldIsList, String mergeFieldNameWithListInfo,
            boolean isImage);
}
