//   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.eventcore.guimodel;

import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;

/**
 * <p>
 * Skeletal implementation for GUI elements.
 * </p>
 * 
 * @version 1.0
 * @author Patrick Harms
 */
public abstract class AbstractDefaultGUIElement implements IGUIElement {

    /**
     * <p>
     * Id for object serialization.
     * </p>
     */
    public static final long serialVersionUID = 1L;

    /**
     * <p>
     * The reference to equal GUI element manager (needed to preserve singleton behavior, even
     * though the objects are not singleton).
     * </p>
     */
    private static final EqualGUIElementManager equalGUIElementManager =
        new EqualGUIElementManager();

    /**
     * <p>
     * Specification of the GUI element
     * </p>
     */
    private final IGUIElementSpec specification;

    /**
     * <p>
     * Reference to the parent element
     * </p>
     */
    private final IGUIElement parent;
    
    /**
     * <p>
     * Boolean that indicates if a GUIElement was used during a session.
     * </p>
     */
    boolean usageObserved;

    /**
     * <p>
     * Constructor. Creates a new AbstractDefaultGUIElement.
     * </p>
     * 
     * @param specification
     *            specification of the created GUI element
     * @param parent
     *            parent of the created GUI element; null means the element is a top-level window
     */
    public AbstractDefaultGUIElement(IGUIElementSpec specification, IGUIElement parent) {
        this.specification = specification;
        this.parent = parent;
        this.usageObserved = false;
    }

    /*
     * (non-Javadoc)
     * 
     * @see de.ugoe.cs.tasktree.guimodel.GUIElement#getSpecification()
     */
    @Override
    public IGUIElementSpec getSpecification() {
        return specification;
    }

    /*
     * (non-Javadoc)
     * 
     * @see de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElement#getParent()
     */
    @Override
    public IGUIElement getParent() {
        return parent;
    }

    /*
     * (non-Javadoc)
     * 
     * @see de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElement#addEqualGUIElement(IGUIElement)
     */
    @Override
    public void addEqualGUIElement(IGUIElement equalElement) {
        equalGUIElementManager.addEqualGUIElements(this, equalElement);
    }

    /*
     * (non-Javadoc)
     * 
     * @see de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElement#isUsed()
     */
    @Override
    public boolean isUsed() {
        return usageObserved;
    }

    /*
     * (non-Javadoc)
     * 
     * @see de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElement#markUsed()
     */
    @Override
    public void markUsed() {
        this.usageObserved = true;
    }

    /*
     * (non-Javadoc)
     * 
     * @see GUIElement#equals(GUIElement)
     */
    public final boolean equals(Object other) {
        // implement final, as GUI elements are all singletons and they equal only if they are the
        // same object or if they are in the list of equal GUI elements
        return super.equals(other) || equalGUIElementManager.equals(this, other);
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#hashCode()
     */
    @Override
    public final int hashCode() {
        // implement final, as GUI elements are all singletons and they equal only if they are the
        // same object. If there are several GUI element objects that represent the same GUI element
        // then they are stored in the list of equal elements. In this case, the hash code of the
        // list is unique within the system
        return equalGUIElementManager.hashCode(this);
    }
    
    /**
     * <p>
     * This internal helper class manages equal GUI elements. This is necessary, as we often first
     * identify many GUI elements as different and later use a heuristic to determine that they are
     * the same. This class provides the means to preserve the singleton behavior of the GUI
     * elements after we merge two GUI elements.
     * </p>
     * 
     * @version 1.0
     * @author Patrick Harms
     */
    private static class EqualGUIElementManager {

        /**
         * <p>
         * The internal map of GUI elements mapping each registered element to its equal variants.
         * We use the {@link IdentityHashMap} as a normal hash map would not work because of the
         * changing of the hash code of GUI elements at runtime.
         * </p>
         */
        private IdentityHashMap<IGUIElement, List<IGUIElement>> identityHashMap =
            new IdentityHashMap<IGUIElement, List<IGUIElement>>();

        /**
         * <p>
         * Adds a new equals relationship between two {@link IGUIElement}s to the equality manager.
         * </p>
         * 
         * @param guiElement1
         *            first equal GUI element
         * @param guiElement2
         *            second equal GUI element
         */
        private synchronized void addEqualGUIElements(IGUIElement guiElement1,
                                                      IGUIElement guiElement2)
        {
            List<IGUIElement> list1 = identityHashMap.get(guiElement1);
            List<IGUIElement> list2 = identityHashMap.get(guiElement2);

            if (list1 == null) {
                if (list2 == null) {
                    list2 = new LinkedList<IGUIElement>();
                    list2.add(guiElement1);
                    list2.add(guiElement2);
                    identityHashMap.put(guiElement1, list2);
                    identityHashMap.put(guiElement2, list2);
                }
                else {
                    addIfNotContained(list2, guiElement1);
                    identityHashMap.put(guiElement1, list2);
                }
            }
            else {
                if (list2 == null) {
                    addIfNotContained(list1, guiElement2);
                    identityHashMap.put(guiElement2, list1);
                }
                else if (list1 != list2) {
                    list1.addAll(list2);
                    identityHashMap.put(guiElement2, list1);
                    
                    // we also have to set this new list for all other elements for which so
                    // far list2 was registered
                    for (IGUIElement candidate : list2) {
                        identityHashMap.put(candidate, list1);
                    }
                }
                // else
                // in this case, both GUI elements should already be registered with the same
                // lists.
            }
        }

        /**
         * <p>
         * Returns the object hash of a {@link IGUIElement}.
         * </p>
         * 
         * @param guiElement
         *            gui element whose object hash is determined
         * @return the object hash
         */
        private synchronized int hashCode(IGUIElement guiElement) {
            return System.identityHashCode(getEqualElementsList(guiElement));
        }

        /**
         * <p>
         * Determines the equality of two {@link IGUIElement}s based on the information of the
         * identify manager. Two elements are equal, if they have been added as equal using
         * {@link #addEqualGUIElements(IGUIElement, IGUIElement)}.
         * </p>
         * 
         * @param guiElement
         *            GUI element to which the object is compared
         * @param other
         *            object that is compared to the GUI element
         * @return
         */
        private synchronized boolean equals(IGUIElement guiElement, Object other) {
            if (other instanceof IGUIElement) {
                for (IGUIElement candidate : getEqualElementsList(guiElement)) {
                    if (candidate == other) {
                        return true;
                    }
                }
            }

            return false;
        }

        /**
         * <p>
         * Returns the equal {@link IGUIElement} of a given {@link IGUIElement}.
         * </p>
         * 
         * @param guiElement
         *            GUI element of which the equal elements are returned
         * @return the equal GUI elements
         */
        private List<IGUIElement> getEqualElementsList(IGUIElement guiElement) {
            List<IGUIElement> returnValue = identityHashMap.get(guiElement);

            if (returnValue == null) {
                returnValue = new LinkedList<IGUIElement>();
                returnValue.add(guiElement);
                identityHashMap.put(guiElement, returnValue);
            }

            return returnValue;
        }

        /**
         * <p>
         * Adds {@link IGUIElement} as equal to a list of {@link IGUIElement} if and only if it is
         * not already contained.
         * </p>
         * 
         * @param equalElementsList
         *            list of {@link IGUIElement} to which the GUI element is added
         * @param guiElement
         *            GUI element to be added
         */
        private void addIfNotContained(List<IGUIElement> equalElementsList, IGUIElement guiElement)
        {
            for (IGUIElement candidate : equalElementsList) {
                if (candidate == guiElement) {
                    return;
                }
            }

            equalElementsList.add(guiElement);
        }

    }

}
