//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   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 de.ugoe.cs.autoquest.jfcmonitor;

import java.awt.Component;
import java.awt.Container;
import java.awt.event.ContainerListener;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.accessibility.AccessibleContext;

import de.ugoe.cs.util.StringTools;

/**
 * <p>
 * This class manages information about the current GUI. It always contains the current GUI
 * hierarchy.
 * </p>
 * 
 * @author Steffen Herbold 
 * @author Fabian Glaser
 * @version 1.0
 */
public class JFCComponent {

    /**
     * <p>
     * Map of all known GUI components.
     * </p>
     */
    private static Map<Component, JFCComponent> knownComponents =
        new HashMap<Component, JFCComponent>();
    
    /**
     * <p>
     * List of PropertyChangeListeners that are registered on the components.
     * </p>
     */
    private static List<PropertyChangeListener> propertyChangeListeners = 
    	new ArrayList<PropertyChangeListener>();
    
    /**
     * <p>
     * List of ContainerListeners that are registered on the components.
     * </p>
     */
    private static List<ContainerListener> containerListeners =
    		new ArrayList<ContainerListener>();

    /**
     * <p>
     * Adds a AWT component to the GUI hierarchy. If the component already exists in the hierarchy,
     * it is not added a second time.
     * </p>
     * 
     * @param component
     *            component that is added
     */
    public static void add(Component component) {
        add(component, find(component.getParent()));
    }
    
    /**
     * <p>
     * Adds a new PropertyChangeListener to the components.
     * </p> 
     * @param list 
     * 			the PropertyChangeListener
     */
    public static void addPropertyChangeListener(PropertyChangeListener list){
    	propertyChangeListeners.add(list);
    }
    
    /**
     * <p> 
     * Adds a new ContainerListener to the components.
     * </p> 
     * @param list
     * 			the ContainerListener
     */
    public static void addContainerListener(ContainerListener list){
    	containerListeners.add(list);
    }
    
    /**
     * <p>
     * Tests if a component is already known.
     * </p>
     * @param component to be tested
     * @return true, if component is already known, false otherwise.
     */
    public static boolean isKnown(Component component){
    	if (knownComponents.containsKey(component))
    		return true;
    	return false;
    }

    /**
     * <p>
     * Adds a AWT component to the GUI hierarchy. If the component already exists in the hierarchy,
     * it is not added a second time.
     * </p>
     * 
     * @param component
     *            component that is added
     * @param parent
     *            parent of the component
     */
    public static void add(Component component, JFCComponent parent) {
        if (!knownComponents.containsKey(component)) {
            knownComponents.put(component, new JFCComponent(component, parent));
        }
    }

    /**
     * <p>
     * Finds a component in the GUI hierarchy and returns the corresponding JFComponent instance.
     * Returns null if the component is not found.
     * </p>
     * 
     * @param component
     *            component that is searched for
     * @return corresponding JFComponent instance; null if the component is not found
     */
    public static JFCComponent find(Component component) {
        return knownComponents.get(component);
    }

    /**
     * <p>
     * Removes a component from the GUI hierarchy. In case the component is not part of the known
     * hierachy, nothing happens.
     * </p>
     * 
     * @param component
     *            component to be removed
     */
    public static void remove(Component component) {
        JFCComponent jfcComponent = knownComponents.remove(component);
        if (jfcComponent != null) {
            jfcComponent.removeFromParent();
            jfcComponent.removeChildren();
        }
    }

    /**
     * <p>
     * Parent of the GUI component. null means, that the component has no parent.
     * </p>
     */
    private JFCComponent parent = null;

    /**
     * <p>
     * Child components of the component.
     * </p>
     */
    private List<JFCComponent> children = new LinkedList<JFCComponent>();

    /**
     * <p>
     * Reference to the actual GUI component.
     * </p>
     */
    private Component component;

    /**
     * <p>
     * Helper attribute that contains the title of the component. Set by {@link #setTitle()}.
     * </p>
     */
    private String title = null;
    
    /**
     * <p>
     * Helper attribute that encodes the source of the title.
     * </p>
     */
    private int titleSource = JFCComponentTitleHierachy.SOURCE_NOT_KNOWN;

    /**
     * <p>
     * Helper attribute that contains the class of the component. Set by {@link #setClass()}.
     * </p>
     */
    private String componentClass = null;

    /**
     * <p>
     * Helper attribute that contains the icon of the component. Set by {@link #setIcon()}.
     * </p>
     */
    private String icon = null;

    /**
     * <p>
     * Helper attribute that contains the icon of the component. Set by {@link #setIndex()}.
     * </p>
     */
    private int index = -1;

    /**
     * <p>
     * Constructor. Creates a new JFCComponent. Only used internally by
     * {@link #add(Component, JFCComponent)}.
     * </p>
     * 
     * @param component
     *            component associated with the JFCComponent
     * @param parent
     *            parent of the component; null if there is no parent
     */
    private JFCComponent(Component component, JFCComponent parent) {
        if (component == null) {
            throw new InvalidParameterException("parameter component must not be null");
        }
        this.component = component;
        
        // add PropertyChangeListeners to AccessibleContext
        AccessibleContext context = component.getAccessibleContext();
        if (context != null){
        	for (PropertyChangeListener listener: propertyChangeListeners)
        		context.addPropertyChangeListener(listener);
        }
        
        // add PropertyChangeListeners to component itself 
        for (PropertyChangeListener listener: propertyChangeListeners)
        	this.component.addPropertyChangeListener(listener);
        
        this.parent = parent;
        if (parent != null) {
            parent.addChild(this);
        }

        if (component instanceof Container) {
        	for (ContainerListener listener: containerListeners)
        		((Container) component).addContainerListener(listener);
        	
            for (Component childComponent : ((Container) component).getComponents()) {
                add(childComponent, this);
            }
        }
    }

    /**
     * <p>
     * Adds a child component to the current component.
     * </p>
     * 
     * @param child
     *            child component to be added
     */
    private void addChild(JFCComponent child) {
        children.add(child);
    }

    /**
     * <p>
     * Returns an XML representation of the component.
     * </p>
     * 
     * @return XLM snippet
     */
    public String getXML() {
        setClass();
        setIcon();
        setIndex();
        setTitle();
        StringBuilder builder = new StringBuilder();
        builder.append("<component hash=\"");
        builder.append(Integer.toHexString(component.hashCode()) + "\" >" + StringTools.ENDLINE);
        // title might contain html-Tags (e.g. in ArgoUML) --> must be escaped
        builder.append(" <param name=\"title\" value=\"" + StringTools.xmlEntityReplacement(title) + "\" />" + StringTools.ENDLINE);
        builder.append(" <param name=\"class\" value=\"" + componentClass + "\" />" +
            StringTools.ENDLINE);
        builder.append(" <param name=\"icon\" value=\"" + icon + "\" />" + StringTools.ENDLINE);
        builder.append(" <param name=\"index\" value=\"" + index + "\" />" + StringTools.ENDLINE);
        if (parent != null){
        	if (!JFCComponent.isKnown(parent.component))
        		throw new AssertionError("Referenced parent is not known.");
        	builder.append(" <param name=\"parent\" value=\"" + Integer.toHexString(parent.component.hashCode()));
        	builder.append("\" />" + StringTools.ENDLINE);
        }
        builder.append(getClassHierarchy());
        builder.append("</component>" + StringTools.ENDLINE);
        
        return builder.toString();
    }
    
    /**
     * <p>
     * Returns an integer that encodes the source of the title.
     * </p>
     * @return int
     */
    public int getTitleSource(){
    	return titleSource;
    }
    
    /**
     * <p>
     * Returns an XML representation of the components children.
     * </p>
     * @return XML representation of children
     */
    public String printChildren(){
    	StringBuilder builder = new StringBuilder();
        for (JFCComponent child: children){
            	builder.append(child.getXML());
            	builder.append(child.printChildren());
        }
    	return builder.toString();
    }

    /**
     * <p>
     * Removes a child component from the current component.
     * </p>
     * 
     * @param child
     *            child component to be removed
     */
    private void removeChild(JFCComponent child) {
        children.remove(child);
    }

    /**
     * <p>
     * Removes the component from the list of children of its parent.
     * </p>
     */
    private void removeFromParent() {
        if (parent != null) {
            parent.removeChild(this);
        }
    }

    /**
     * <p>
     * Triggers the removals of all child components from the GUI hierarchy, i.e., calls
     * {@link #remove(Component)} for all child components.
     * </p>
     */
    private void removeChildren() {
        for (JFCComponent child : children) {
            remove(child.component);
        }
    }

    /**
     * <p>
     * Sets the {@link #title} of the component. The title is defined as follows (first in the list,
     * that is not null):
     * <ul>
     * <li>accessible name of the component if available</li>
     * <li>{@link #icon} of the component</li>
     * <li>name of the component</li>
     * <li>coordinates of the component</li>
     * </ul>
     * </p>
     */
    public void setTitle() {
    	// Note that JFCComponentTitleHierarchy depends on this method. So any changes made
    	// here should be reflected in JFCComponentTitleHierarchy.
    	
        title = null; // reset title

        AccessibleContext accessibleContext = component.getAccessibleContext();
        if (accessibleContext != null) {
            title = accessibleContext.getAccessibleName();
            titleSource = JFCComponentTitleHierachy.ACCESSIBLE_NAME;
        }
        if (title == null) {
            title = icon;
            titleSource = JFCComponentTitleHierachy.ICON;
        }
        if (title == null) {
            title = component.getName();
            titleSource = JFCComponentTitleHierachy.NAME;
        }
        if (title == null) {
            // use coordinates as last resort
            title = "Pos(" + component.getX() + "," + component.getY() + ")";
            titleSource = JFCComponentTitleHierachy.POS;
        }
    }

    /**
     * <p>
     * Sets the {@link #componentClass} of the component.
     * </p>
     */
    private void setClass() {
        componentClass = component.getClass().getName();
    }

    /**
     * <p>
     * Sets the {@link #icon} of the component.
     * </p>
     */
    private void setIcon() {
        icon = null; // reset icon

        Method getIconMethod;
        try {
            getIconMethod = component.getClass().getMethod("getIcon", new Class[0]);
            if (getIconMethod != null) {
                Object iconObject = getIconMethod.invoke(component, new Object[] { });
                if (iconObject != null) {
                    String iconPath = iconObject.toString();
                    if (!iconPath.contains("@")) {
                        System.out.println("iconPath");
                        String[] splitResult =
                            iconPath.split(File.separatorChar == '\\' ? "\\\\" : File.separator);
                        icon = splitResult[splitResult.length - 1];
                    }
                }
            }
        }
        catch (SecurityException e) {}
        catch (NoSuchMethodException e) {}
        catch (IllegalArgumentException e) {}
        catch (IllegalAccessException e) {}
        catch (InvocationTargetException e) {
            System.err.println("Found method with name " + "getIcon" + " but could not access it.");
        }
    }

    /**
     * <p>
     * Sets the {@link #index} of the component as the index in the parent, if it is accessible.
     * </p>
     */
    private void setIndex() {
        index = -1; // reset index

        AccessibleContext accessibleContext = component.getAccessibleContext();
        if (accessibleContext != null) {
            index = accessibleContext.getAccessibleIndexInParent();
        }
    }
    
    /**
     * <p>
     * Constructs a string that represents inheritance of component.
     * </p>
     * @return
     */
    private String getClassHierarchy(){
    	StringBuilder builder = new StringBuilder();
    	Class<? extends Object> classobject = component.getClass();
    	builder.append(" <ancestors>" + StringTools.ENDLINE);
    	while(classobject != null){
    		builder.append("  <ancestor name=\"");
    		builder.append(classobject.getName());
    		builder.append("\" />" + StringTools.ENDLINE);
    		classobject = classobject.getSuperclass();
    	}
    	builder.append(" </ancestors>" + StringTools.ENDLINE);
    	return builder.toString();
    }
}
