/*
 * This software is distributed under following license based on modified BSD
 * style license.
 * ----------------------------------------------------------------------
 * 
 * Copyright 2009 The Nimbus2 Project. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE NIMBUS PROJECT ``AS IS'' AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
 * NO EVENT SHALL THE NIMBUS PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are
 * those of the authors and should not be interpreted as representing official
 * policies, either expressed or implied, of the Nimbus2 Project.
 */
package jp.ossc.nimbus.util;

import java.util.*;

/**
 * クラスに対する任意のオブジェクトのマッピングツリー。<p>
 * あるクラスに対応する任意のオブジェクトを、クラスの継承関係のツリーでマッピングして管理する。<br>
 * {@link #add(Class, Object)}でクラスと任意のオブジェクトのマッピングを登録できる。{@link #getValue(Class)}でクラスを指定すると対応するオブジェクトを取得できる。ここで言う対応するオブジェクトとは、指定したクラスに一致するクラスにマッピングされているオブジェクト、または、一致するクラスがない場合は、登録されているクラスの中で最も近い継承関係にあるスーパークラスにマッピングされたオブジェクトを指す。また、ここで言うクラスには、インタフェースも含まれる。<br>
 * 
 * @author M.Takata
 */
public class ClassMappingTree<T> implements java.io.Serializable{
    
    private static final long serialVersionUID = 4471328701464216810L;
    
    private final TreeElement<T> rootElement;
    
    private final Map<Class<?>, TreeElement<T>> classMap = new HashMap<Class<?>, TreeElement<T>>();
    
    /**
     * nullを{@link java.lang.Object}クラスにマッピングして、マッピングツリーのルートとして、このクラスのインスタンスを生成する。<p>
     */
    public ClassMappingTree(){
        this(null);
    }
    
    /**
     * 指定されたオブジェクトを{@link java.lang.Object}クラスにマッピングして、マッピングツリーのルートとして、このクラスのインスタンスを生成する。<p>
     *
     * @param root java.lang.Objectクラスに対応するオブジェクト
     */
    public ClassMappingTree(T root){
        rootElement = new TreeElement<T>(java.lang.Object.class, root);
        classMap.put(java.lang.Object.class, rootElement);
    }
    
    /**
     * クラスと任意のオブジェクトのマッピングを登録する。<p>
     *
     * @param clazz クラスオブジェクト
     * @param value 任意のオブジェクト
     */
    public void add(Class<?> clazz, T value){
        add(clazz, value, false);
    }
    
    /**
     * クラスと任意のオブジェクトのマッピングを登録する。<p>
     *
     * @param clazz クラスオブジェクト
     * @param value 任意のオブジェクト
     * @param replace 既に同じクラスに対してマッピングされたオブジェクトがある場合に、それと置き換える場合は、true。クラスに対してマッピングされたオブジェクトを複数許容して、追加する場合は、false
     */
    public void add(Class<?> clazz, T value, boolean replace){
        if(classMap.containsKey(clazz)){
            final TreeElement<T> element = classMap.get(clazz);
            if(replace){
                element.setTarget(value);
            }else{
                element.addTarget(value);
            }
        }else{
            TreeElement<T> element = new TreeElement<T>(clazz, value);
            final TreeElement<T> nearestParent
                 = rootElement.getNearestParentElement(clazz);
            if(nearestParent != null){
                if(nearestParent.hasChild()){
                    for(TreeElement<T> child : nearestParent.getChildElements()){
                        if(child.isChildOf(clazz)){
                            nearestParent.moveChild(element, child);
                        }
                    }
                }
                element = nearestParent.addChild(element);
            }
            classMap.put(clazz, element);
        }
    }
    
    /**
     * 指定されたクラスに対応するオブジェクトを取得する。<p>
     * ここで言う対応するオブジェクトとは、指定したクラスに一致するクラスにマッピングされているオブジェクトを指す。<br>
     * 対応するオブジェクトが複数存在する場合には、最初に登録されたオブジェクトを返す。<br>
     *
     * @param clazz クラスオブジェクト
     * @return 指定されたクラスに対応するオブジェクト
     * @see #getValuesOf(Class)
     */
    public T getValueOf(Class<?> clazz){
        final List<T> values = getValueListOf(clazz);
        return values != null && values.size() > 0 ? values.get(0) : null;
    }
    
    /**
     * 指定されたクラスに対応するオブジェクトリストを取得する。<p>
     *
     * @param clazz クラスオブジェクト
     * @return 指定されたクラスに対応するオブジェクトリスト
     */
    public List<T> getValueListOf(Class<?> clazz){
        if(classMap.containsKey(clazz)){
            return classMap.get(clazz).getTargets();
        }else{
            return null;
        }
    }
    
    /**
     * 指定されたクラスに対応するオブジェクトを取得する。<p>
     * ここで言う対応するオブジェクトとは、指定したクラスに一致するクラスにマッピングされているオブジェクト、または、一致するクラスがない場合は、登録されているクラスの中で最も近い継承関係にあるスーパークラスにマッピングされたオブジェクトを指す。<br>
     * 対応するオブジェクトが複数存在する場合には、最初に登録されたオブジェクトを返す。<br>
     *
     * @param clazz クラスオブジェクト
     * @return 指定されたクラスに対応するオブジェクト
     * @see #getValues(Class)
     */
    public T getValue(Class<?> clazz){
        final List<T> values = getValueList(clazz);
        return values != null && values.size() > 0 ? values.get(0) : null;
    }
    
    /**
     * 指定されたクラスに対応するオブジェクトリストを取得する。<p>
     *
     * @param clazz クラスオブジェクト
     * @return 指定されたクラスに対応するオブジェクトリスト
     */
    public List<T> getValueList(Class<?> clazz){
        if(clazz == null){
            return rootElement.getTargets();
        }
        if(classMap.containsKey(clazz)){
            return classMap.get(clazz).getTargets();
        }else{
            final TreeElement<T> nearestParent
                 = rootElement.getNearestParentElement(clazz);
            if(nearestParent == null){
                return rootElement.getTargets();
            }
            return nearestParent.getTargets();
        }
    }
    
    /**
     * 指定されたクラスにマッピングされている指定オブジェクトを削除する。<p>
     *
     * @param clazz クラスオブジェクト
     * @param value 任意のオブジェクト
     */
    public void remove(Class<?> clazz, T value){
        if(classMap.containsKey(clazz)){
            final TreeElement<T> removeElement
                 = classMap.get(clazz);
            removeElement.removeTarget(value);
            if(removeElement.getTargets().size() == 0){
                remove(clazz);
            }
        }
    }
    
    /**
     * 指定されたクラスにマッピングされている全てのオブジェクトを削除する。<p>
     *
     * @param clazz クラスオブジェクト
     */
    public void remove(Class<?> clazz){
        if(!Object.class.equals(clazz) && classMap.containsKey(clazz)){
            final TreeElement<T> removedElement
                 = classMap.remove(clazz);
            if(removedElement.parent != null){
                removedElement.parent.removeChild(removedElement);
            }
        }
    }
    
    /**
     * マッピングされている全てのオブジェクトを削除する。<p>
     */
    public void clear(){
        final Set<Class<?>> classes = new HashSet<Class<?>>(classMap.keySet());
        for(Class<?> clazz : classes){
            remove(clazz);
        }
    }
    
    /**
     * 登録されているクラスマッピングの文字列表現を返す。<p>
     *
     * @return クラスマッピングの文字列表現
     */
    public String toString(){
        return super.toString() + classMap;
    }
    
    private static class TreeElement<T> implements java.io.Serializable{
        
        private static final long serialVersionUID = 4875545362697617699L;
        
        TreeElement<T> parent;
        private Map<Class<?>, TreeElement<T>> children;
        Class<?> clazz;
        private List<T> targets = new ArrayList<T>();
        public TreeElement(Class<?> clazz, T target){
            this(null, clazz, target);
        }
        public TreeElement(TreeElement<T> parent, Class<?> clazz, T target){
            this.parent = parent;
            this.clazz = clazz;
            setTarget(target);
        }
        public TreeElement<T> addChild(TreeElement<T> child){
            if(children == null){
                children = new HashMap<Class<?>, TreeElement<T>>();
            }
            if(children.containsKey(child.clazz)){
                final TreeElement<T> element
                     = children.get(child.clazz);
                element.addTargets(child.getTargets());
                return element;
            }else{
                children.put(child.clazz, child);
                child.parent = this;
                return child;
            }
        }
        @SuppressWarnings("unused")
		public TreeElement<T> getChild(Class<?> clazz){
            if(children == null){
                return null;
            }
            return children.get(clazz);
        }
        public Collection<TreeElement<T>> getChildElements(){
            return children == null ? null : new HashSet<TreeElement<T>>(children.values());
        }
        @SuppressWarnings("unused")
		public int childrenNumber(){
            return children == null ? 0 : children.size();
        }
        public void moveChild(TreeElement<T> newParent, TreeElement<T> child){
            children.remove(child.clazz);
            newParent.addChild(child);
        }
        public void removeChild(TreeElement<T> child){
            final TreeElement<T> removedChild
                 = children.remove(child.clazz);
            if(removedChild != null && removedChild.hasChild()
                && parent != null && parent.children != null){
                parent.children.putAll(removedChild.children);
            }
        }
        public void setTarget(T target){
            targets.clear();
            if(target != null){
                targets.add(target);
            }
        }
        public void addTarget(T target){
            if(target != null){
                targets.add(target);
            }
        }
        public void addTargets(List<T> targets){
            targets.addAll(targets);
        }
        public List<T> getTargets(){
            return targets;
        }
        public void removeTarget(T target){
            targets.remove(target);
        }
        public boolean hasChild(){
            return children != null && children.size() != 0;
        }
        public TreeElement<T> getNearestParentElement(Class<?> clazz){
            if(!isParentOf(clazz)){
                return null;
            }
            if(!hasChild()){
                return this;
            }
            TreeElement<T> nearestElement = null;
            for(TreeElement<T> element : children.values()){
                nearestElement = element.getNearestParentElement(clazz);
                if(nearestElement != null){
                    return nearestElement;
                }
            }
            return this;
        }
        public boolean isChildOf(Class<?> clazz){
            return clazz.isAssignableFrom(this.clazz);
        }
        public boolean isParentOf(Class<?> clazz){
            return this.clazz.isAssignableFrom(clazz);
        }
        @SuppressWarnings("unused")
		public T getTarget(){
            return targets.size() == 0 ? null : targets.get(0);
        }
        @SuppressWarnings("unchecked")
        @Override
        public boolean equals(Object obj){
            if(obj == null){
                return false;
            }
            if(obj == this){
                return true;
            }
            if(!(obj instanceof TreeElement)){
                return false;
            }
            final TreeElement<T> element = (TreeElement<T>)obj;
            if(clazz == null){
                return element.clazz == null;
            }
            return clazz.equals(element.clazz);
        }
        @Override
        public int hashCode(){
            return clazz == null ? 0 : clazz.hashCode();
        }
        @Override
        public String toString(){
            return targets.toString();
        }
    }
}
